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 from itertools import chain
10 import psycopg2.extras
12 from place_inserter import PlaceColumn
13 from table_compare import NominatimID, DBRow
15 from nominatim.indexer import indexer
16 from nominatim.tokenizer import factory as tokenizer_factory
18 def check_database_integrity(context):
19 """ Check some generic constraints on the tables.
21 # place_addressline should not have duplicate (place_id, address_place_id)
22 cur = context.db.cursor()
23 cur.execute("""SELECT count(*) FROM
24 (SELECT place_id, address_place_id, count(*) as c
25 FROM place_addressline GROUP BY place_id, address_place_id) x
27 assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
30 ################################ GIVEN ##################################
32 @given("the (?P<named>named )?places")
33 def add_data_to_place_table(context, named):
34 """ Add entries into the place table. 'named places' makes sure that
35 the entries get a random name when none is explicitly given.
37 with context.db.cursor() as cur:
38 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
39 for row in context.table:
40 PlaceColumn(context).add_row(row, named is not None).db_insert(cur)
41 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
43 @given("the relations")
44 def add_data_to_planet_relations(context):
45 """ Add entries into the osm2pgsql relation middle table. This is needed
46 for tests on data that looks up members.
48 with context.db.cursor() as cur:
49 for r in context.table:
55 for m in r['members'].split(','):
58 parts.insert(last_node, int(mid.oid))
62 parts.insert(last_way, int(mid.oid))
65 parts.append(int(mid.oid))
67 members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
71 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
73 cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags)
74 VALUES (%s, %s, %s, %s, %s, %s)""",
75 (r['id'], last_node, last_way, parts, members, list(tags)))
78 def add_data_to_planet_ways(context):
79 """ Add entries into the osm2pgsql way middle table. This is necessary for
80 tests on that that looks up node ids in this table.
82 with context.db.cursor() as cur:
83 for r in context.table:
84 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
85 nodes = [ int(x.strip()) for x in r['nodes'].split(',') ]
87 cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
88 (r['id'], nodes, list(tags)))
90 ################################ WHEN ##################################
93 def import_and_index_data_from_place_table(context):
94 """ Import data previously set up in the place table.
96 context.nominatim.run_nominatim('refresh', '--functions')
97 context.nominatim.run_nominatim('import', '--continue', 'load-data',
98 '--index-noanalyse', '-q')
100 check_database_integrity(context)
102 @when("updating places")
103 def update_place_table(context):
104 """ Update the place table with the given data. Also runs all triggers
105 related to updates and reindexes the new data.
107 context.nominatim.run_nominatim('refresh', '--functions')
108 with context.db.cursor() as cur:
109 for row in context.table:
110 PlaceColumn(context).add_row(row, False).db_insert(cur)
112 context.nominatim.reindex_placex(context.db)
113 check_database_integrity(context)
115 @when("updating postcodes")
116 def update_postcodes(context):
117 """ Rerun the calculation of postcodes.
119 context.nominatim.run_nominatim('refresh', '--postcodes')
121 @when("marking for delete (?P<oids>.*)")
122 def delete_places(context, oids):
123 """ Remove entries from the place table. Multiple ids may be given
124 separated by commas. Also runs all triggers
125 related to updates and reindexes the new data.
127 context.nominatim.run_nominatim('refresh', '--functions')
128 with context.db.cursor() as cur:
129 for oid in oids.split(','):
130 NominatimID(oid).query_osm_id(cur, 'DELETE FROM place WHERE {}')
132 context.nominatim.reindex_placex(context.db)
134 ################################ THEN ##################################
136 @then("(?P<table>placex|place) contains(?P<exact> exactly)?")
137 def check_place_contents(context, table, exact):
138 """ Check contents of place/placex tables. Each row represents a table row
139 and all data must match. Data not present in the expected table, may
140 be arbitry. The rows are identified via the 'object' column which must
141 have an identifier of the form '<NRW><osm id>[:<class>]'. When multiple
142 rows match (for example because 'class' was left out and there are
143 multiple entries for the given OSM object) then all must match. All
144 expected rows are expected to be present with at least one database row.
145 When 'exactly' is given, there must not be additional rows in the database.
147 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
148 expected_content = set()
149 for row in context.table:
150 nid = NominatimID(row['object'])
151 query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype'
152 if table == 'placex':
153 query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
154 query += " FROM %s WHERE {}" % (table, )
155 nid.query_osm_id(cur, query)
156 assert cur.rowcount > 0, "No rows found for " + row['object']
160 expected_content.add((res['osm_type'], res['osm_id'], res['class']))
162 DBRow(nid, res, context).assert_row(row, ['object'])
165 cur.execute('SELECT osm_type, osm_id, class from {}'.format(table))
166 assert expected_content == set([(r[0], r[1], r[2]) for r in cur])
169 @then("(?P<table>placex|place) has no entry for (?P<oid>.*)")
170 def check_place_has_entry(context, table, oid):
171 """ Ensure that no database row for the given object exists. The ID
172 must be of the form '<NRW><osm id>[:<class>]'.
174 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
175 NominatimID(oid).query_osm_id(cur, "SELECT * FROM %s where {}" % table)
176 assert cur.rowcount == 0, \
177 "Found {} entries for ID {}".format(cur.rowcount, oid)
180 @then("search_name contains(?P<exclude> not)?")
181 def check_search_name_contents(context, exclude):
182 """ Check contents of place/placex tables. Each row represents a table row
183 and all data must match. Data not present in the expected table, may
184 be arbitry. The rows are identified via the 'object' column which must
185 have an identifier of the form '<NRW><osm id>[:<class>]'. All
186 expected rows are expected to be present with at least one database row.
188 tokenizer = tokenizer_factory.get_tokenizer_for_db(context.nominatim.get_test_config())
190 with tokenizer.name_analyzer() as analyzer:
191 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
192 for row in context.table:
193 nid = NominatimID(row['object'])
194 nid.row_by_place_id(cur, 'search_name',
195 ['ST_X(centroid) as cx', 'ST_Y(centroid) as cy'])
196 assert cur.rowcount > 0, "No rows found for " + row['object']
199 db_row = DBRow(nid, res, context)
200 for name, value in zip(row.headings, row.cells):
201 if name in ('name_vector', 'nameaddress_vector'):
202 items = [x.strip() for x in value.split(',')]
203 tokens = analyzer.get_word_token_info(items)
206 assert len(tokens) >= len(items), \
207 "No word entry found for {}. Entries found: {!s}".format(value, len(tokens))
208 for word, token, wid in tokens:
210 assert wid not in res[name], \
211 "Found term for {}/{}: {}".format(nid, name, wid)
213 assert wid in res[name], \
214 "Missing term for {}/{}: {}".format(nid, name, wid)
215 elif name != 'object':
216 assert db_row.contains(name, value), db_row.assert_msg(name, value)
218 @then("search_name has no entry for (?P<oid>.*)")
219 def check_search_name_has_entry(context, oid):
220 """ Check that there is noentry in the search_name table for the given
221 objects. IDs are in format '<NRW><osm id>[:<class>]'.
223 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
224 NominatimID(oid).row_by_place_id(cur, 'search_name')
226 assert cur.rowcount == 0, \
227 "Found {} entries for ID {}".format(cur.rowcount, oid)
229 @then("location_postcode contains exactly")
230 def check_location_postcode(context):
231 """ Check full contents for location_postcode table. Each row represents a table row
232 and all data must match. Data not present in the expected table, may
233 be arbitry. The rows are identified via 'country' and 'postcode' columns.
234 All rows must be present as excepted and there must not be additional
237 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
238 cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
239 assert cur.rowcount == len(list(context.table)), \
240 "Postcode table has {} rows, expected {}.".format(cur.rowcount, len(list(context.table)))
244 key = (row['country_code'], row['postcode'])
245 assert key not in results, "Postcode table has duplicate entry: {}".format(row)
246 results[key] = DBRow((row['country_code'],row['postcode']), row, context)
248 for row in context.table:
249 db_row = results.get((row['country'],row['postcode']))
250 assert db_row is not None, \
251 f"Missing row for country '{row['country']}' postcode '{row['postcode']}'."
253 db_row.assert_row(row, ('country', 'postcode'))
255 @then("there are(?P<exclude> no)? word tokens for postcodes (?P<postcodes>.*)")
256 def check_word_table_for_postcodes(context, exclude, postcodes):
257 """ Check that the tokenizer produces postcode tokens for the given
258 postcodes. The postcodes are a comma-separated list of postcodes.
261 nctx = context.nominatim
262 tokenizer = tokenizer_factory.get_tokenizer_for_db(nctx.get_test_config())
263 with tokenizer.name_analyzer() as ana:
264 plist = [ana.normalize_postcode(p) for p in postcodes.split(',')]
268 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
269 if nctx.tokenizer != 'legacy':
270 cur.execute("SELECT word FROM word WHERE type = 'P' and word = any(%s)",
273 cur.execute("""SELECT word FROM word WHERE word = any(%s)
274 and class = 'place' and type = 'postcode'""",
277 found = [row[0] for row in cur]
278 assert len(found) == len(set(found)), f"Duplicate rows for postcodes: {found}"
281 assert len(found) == 0, f"Unexpected postcodes: {found}"
283 assert set(found) == set(plist), \
284 f"Missing postcodes {set(plist) - set(found)}. Found: {found}"
286 @then("place_addressline contains")
287 def check_place_addressline(context):
288 """ Check the contents of the place_addressline table. Each row represents
289 a table row and all data must match. Data not present in the expected
290 table, may be arbitry. The rows are identified via the 'object' column,
291 representing the addressee and the 'address' column, representing the
294 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
295 for row in context.table:
296 nid = NominatimID(row['object'])
297 pid = nid.get_place_id(cur)
298 apid = NominatimID(row['address']).get_place_id(cur)
299 cur.execute(""" SELECT * FROM place_addressline
300 WHERE place_id = %s AND address_place_id = %s""",
302 assert cur.rowcount > 0, \
303 "No rows found for place %s and address %s" % (row['object'], row['address'])
306 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
308 @then("place_addressline doesn't contain")
309 def check_place_addressline_exclude(context):
310 """ Check that the place_addressline doesn't contain any entries for the
311 given addressee/address item pairs.
313 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
314 for row in context.table:
315 pid = NominatimID(row['object']).get_place_id(cur)
316 apid = NominatimID(row['address']).get_place_id(cur, allow_empty=True)
318 cur.execute(""" SELECT * FROM place_addressline
319 WHERE place_id = %s AND address_place_id = %s""",
321 assert cur.rowcount == 0, \
322 "Row found for place %s and address %s" % (row['object'], row['address'])
324 @then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
325 def check_location_property_osmline(context, oid, neg):
326 """ Check that the given way is present in the interpolation table.
328 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
329 cur.execute("""SELECT *, ST_AsText(linegeo) as geomtxt
330 FROM location_property_osmline
331 WHERE osm_id = %s AND startnumber IS NOT NULL""",
335 assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
338 todo = list(range(len(list(context.table))))
341 row = context.table[i]
342 if (int(row['start']) == res['startnumber']
343 and int(row['end']) == res['endnumber']):
347 assert False, "Unexpected row " + str(res)
349 DBRow(oid, res, context).assert_row(row, ('start', 'end'))