1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Tests for migration functions
11 import psycopg2.extras
13 from nominatim.tools import migration
14 from nominatim.errors import UsageError
15 import nominatim.version
19 def update_sql_functions(self, config):
24 def postprocess_mock(monkeypatch):
25 monkeypatch.setattr(migration.refresh, 'create_functions', lambda *args: args)
26 monkeypatch.setattr(migration.tokenizer_factory, 'get_tokenizer_for_db',
27 lambda *args: DummyTokenizer())
30 def test_no_migration_old_versions(temp_db_with_extensions, table_factory, def_config):
31 table_factory('country_name', 'name HSTORE, country_code TEXT')
33 with pytest.raises(UsageError, match='Migration not possible'):
34 migration.migrate(def_config, {})
37 def test_set_up_migration_for_36(temp_db_with_extensions, temp_db_cursor,
38 table_factory, def_config, monkeypatch,
40 psycopg2.extras.register_hstore(temp_db_cursor)
41 # don't actually run any migration, except the property table creation
42 monkeypatch.setattr(migration, '_MIGRATION_FUNCTIONS',
43 [((3, 5, 0, 99), migration.add_nominatim_property_table)])
44 # Use a r/o user name that always exists
45 monkeypatch.setenv('NOMINATIM_DATABASE_WEBUSER', 'postgres')
47 table_factory('country_name', 'name HSTORE, country_code TEXT',
48 (({str(x): 'a' for x in range(200)}, 'gb'),))
50 assert not temp_db_cursor.table_exists('nominatim_properties')
52 assert migration.migrate(def_config, {}) == 0
54 assert temp_db_cursor.table_exists('nominatim_properties')
56 assert 1 == temp_db_cursor.scalar(""" SELECT count(*) FROM nominatim_properties
57 WHERE property = 'database_version'""")
60 def test_already_at_version(def_config, property_table):
62 property_table.set('database_version',
63 '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(nominatim.version.NOMINATIM_VERSION))
65 assert migration.migrate(def_config, {}) == 0
68 def test_no_migrations_necessary(def_config, temp_db_cursor, property_table,
70 oldversion = [x for x in nominatim.version.NOMINATIM_VERSION]
72 property_table.set('database_version',
73 '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(oldversion))
76 monkeypatch.setattr(migration, '_MIGRATION_FUNCTIONS',
77 [(tuple(oldversion), lambda **attr: True)])
79 assert migration.migrate(def_config, {}) == 0
82 def test_run_single_migration(def_config, temp_db_cursor, property_table,
83 monkeypatch, postprocess_mock):
84 oldversion = [x for x in nominatim.version.NOMINATIM_VERSION]
86 property_table.set('database_version',
87 '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(oldversion))
89 done = {'old': False, 'new': False}
91 """ Dummy migration"""
94 def _old_migration(**_):
95 """ Dummy migration"""
99 monkeypatch.setattr(migration, '_MIGRATION_FUNCTIONS',
100 [(tuple(oldversion), _old_migration),
101 (nominatim.version.NOMINATIM_VERSION, _migration)])
103 assert migration.migrate(def_config, {}) == 0
106 assert not done['old']
107 assert property_table.get('database_version') == \
108 '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(nominatim.version.NOMINATIM_VERSION)
111 ###### Tests for specific migrations
113 # Each migration should come with two tests:
114 # 1. Test that migration from old to new state works as expected.
115 # 2. Test that the migration can be rerun on the new state without side effects.
118 @pytest.mark.parametrize('in_attr', ('', 'with time zone'))
119 def test_import_status_timestamp_change(temp_db_conn, temp_db_cursor,
120 table_factory, in_attr):
121 table_factory('import_status',
122 f"""lastimportdate timestamp {in_attr},
126 migration.import_status_timestamp_change(temp_db_conn)
127 temp_db_conn.commit()
129 assert temp_db_cursor.scalar("""SELECT data_type FROM information_schema.columns
130 WHERE table_name = 'import_status'
131 and column_name = 'lastimportdate'""")\
132 == 'timestamp with time zone'
135 def test_add_nominatim_property_table(temp_db_conn, temp_db_cursor,
136 def_config, monkeypatch):
137 # Use a r/o user name that always exists
138 monkeypatch.setenv('NOMINATIM_DATABASE_WEBUSER', 'postgres')
140 assert not temp_db_cursor.table_exists('nominatim_properties')
142 migration.add_nominatim_property_table(temp_db_conn, def_config)
143 temp_db_conn.commit()
145 assert temp_db_cursor.table_exists('nominatim_properties')
148 def test_add_nominatim_property_table_repeat(temp_db_conn, temp_db_cursor,
149 def_config, property_table):
150 assert temp_db_cursor.table_exists('nominatim_properties')
152 migration.add_nominatim_property_table(temp_db_conn, def_config)
153 temp_db_conn.commit()
155 assert temp_db_cursor.table_exists('nominatim_properties')
158 def test_change_housenumber_transliteration(temp_db_conn, temp_db_cursor,
159 word_table, placex_table):
160 placex_table.add(housenumber='3A')
162 temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION make_standard_name(name TEXT)
163 RETURNS TEXT AS $$ SELECT lower(name) $$ LANGUAGE SQL """)
164 temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word TEXT)
165 RETURNS INTEGER AS $$ SELECT 4325 $$ LANGUAGE SQL """)
167 migration.change_housenumber_transliteration(temp_db_conn)
168 temp_db_conn.commit()
170 assert temp_db_cursor.scalar('SELECT housenumber from placex') == '3a'
172 migration.change_housenumber_transliteration(temp_db_conn)
173 temp_db_conn.commit()
175 assert temp_db_cursor.scalar('SELECT housenumber from placex') == '3a'
178 def test_switch_placenode_geometry_index(temp_db_conn, temp_db_cursor, placex_table):
179 temp_db_cursor.execute("""CREATE INDEX idx_placex_adminname
180 ON placex (place_id)""")
182 migration.switch_placenode_geometry_index(temp_db_conn)
183 temp_db_conn.commit()
185 assert temp_db_cursor.index_exists('placex', 'idx_placex_geometry_placenode')
186 assert not temp_db_cursor.index_exists('placex', 'idx_placex_adminname')
189 def test_switch_placenode_geometry_index_repeat(temp_db_conn, temp_db_cursor, placex_table):
190 temp_db_cursor.execute("""CREATE INDEX idx_placex_geometry_placenode
191 ON placex (place_id)""")
193 migration.switch_placenode_geometry_index(temp_db_conn)
194 temp_db_conn.commit()
196 assert temp_db_cursor.index_exists('placex', 'idx_placex_geometry_placenode')
197 assert not temp_db_cursor.index_exists('placex', 'idx_placex_adminname')
198 assert temp_db_cursor.scalar("""SELECT indexdef from pg_indexes
199 WHERE tablename = 'placex'
200 and indexname = 'idx_placex_geometry_placenode'
201 """).endswith('(place_id)')
204 def test_install_legacy_tokenizer(temp_db_conn, temp_db_cursor, project_env,
205 property_table, table_factory, monkeypatch,
207 table_factory('placex', 'place_id BIGINT')
208 table_factory('location_property_osmline', 'place_id BIGINT')
210 # Setting up the tokenizer is problematic
212 def migrate_database(self, config):
215 monkeypatch.setattr(migration.tokenizer_factory, 'create_tokenizer',
216 lambda cfg, **kwargs: MiniTokenizer())
218 migration.install_legacy_tokenizer(temp_db_conn, project_env)
219 temp_db_conn.commit()
223 def test_install_legacy_tokenizer_repeat(temp_db_conn, temp_db_cursor,
224 def_config, property_table):
226 property_table.set('tokenizer', 'dummy')
227 migration.install_legacy_tokenizer(temp_db_conn, def_config)
228 temp_db_conn.commit()
231 def test_create_tiger_housenumber_index(temp_db_conn, temp_db_cursor, table_factory):
232 table_factory('location_property_tiger',
233 'parent_place_id BIGINT, startnumber INT, endnumber INT')
235 migration.create_tiger_housenumber_index(temp_db_conn)
236 temp_db_conn.commit()
238 if temp_db_conn.server_version_tuple() >= (11, 0, 0):
239 assert temp_db_cursor.index_exists('location_property_tiger',
240 'idx_location_property_tiger_housenumber_migrated')
242 migration.create_tiger_housenumber_index(temp_db_conn)
243 temp_db_conn.commit()