2 from itertools import chain
6 from place_inserter import PlaceColumn
7 from table_compare import NominatimID, DBRow
9 from nominatim.indexer import indexer
10 from nominatim.tokenizer import factory as tokenizer_factory
12 def check_database_integrity(context):
13 """ Check some generic constraints on the tables.
15 # place_addressline should not have duplicate (place_id, address_place_id)
16 cur = context.db.cursor()
17 cur.execute("""SELECT count(*) FROM
18 (SELECT place_id, address_place_id, count(*) as c
19 FROM place_addressline GROUP BY place_id, address_place_id) x
21 assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
24 ################################ GIVEN ##################################
26 @given("the (?P<named>named )?places")
27 def add_data_to_place_table(context, named):
28 """ Add entries into the place table. 'named places' makes sure that
29 the entries get a random name when none is explicitly given.
31 with context.db.cursor() as cur:
32 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
33 for row in context.table:
34 PlaceColumn(context).add_row(row, named is not None).db_insert(cur)
35 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
37 @given("the relations")
38 def add_data_to_planet_relations(context):
39 """ Add entries into the osm2pgsql relation middle table. This is needed
40 for tests on data that looks up members.
42 with context.db.cursor() as cur:
43 for r in context.table:
49 for m in r['members'].split(','):
52 parts.insert(last_node, int(mid.oid))
56 parts.insert(last_way, int(mid.oid))
59 parts.append(int(mid.oid))
61 members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
65 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
67 cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags)
68 VALUES (%s, %s, %s, %s, %s, %s)""",
69 (r['id'], last_node, last_way, parts, members, list(tags)))
72 def add_data_to_planet_ways(context):
73 """ Add entries into the osm2pgsql way middle table. This is necessary for
74 tests on that that looks up node ids in this table.
76 with context.db.cursor() as cur:
77 for r in context.table:
78 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
79 nodes = [ int(x.strip()) for x in r['nodes'].split(',') ]
81 cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
82 (r['id'], nodes, list(tags)))
84 ################################ WHEN ##################################
87 def import_and_index_data_from_place_table(context):
88 """ Import data previously set up in the place table.
90 nctx = context.nominatim
92 tokenizer = tokenizer_factory.create_tokenizer(nctx.get_test_config())
93 context.nominatim.copy_from_place(context.db)
95 # XXX use tool function as soon as it is ported
96 with context.db.cursor() as cur:
97 with (context.nominatim.src_dir / 'lib-sql' / 'postcode_tables.sql').open('r') as fd:
98 cur.execute(fd.read())
100 INSERT INTO location_postcode
101 (place_id, indexed_status, country_code, postcode, geometry)
102 SELECT nextval('seq_place'), 1, country_code,
103 upper(trim (both ' ' from address->'postcode')) as pc,
104 ST_Centroid(ST_Collect(ST_Centroid(geometry)))
106 WHERE address ? 'postcode' AND address->'postcode' NOT SIMILAR TO '%(,|;)%'
107 AND geometry IS NOT null
108 GROUP BY country_code, pc""")
110 # Call directly as the refresh function does not include postcodes.
111 indexer.LOG.setLevel(logging.ERROR)
112 indexer.Indexer(context.nominatim.get_libpq_dsn(), tokenizer, 1).index_full(analyse=False)
114 check_database_integrity(context)
116 @when("updating places")
117 def update_place_table(context):
118 """ Update the place table with the given data. Also runs all triggers
119 related to updates and reindexes the new data.
121 context.nominatim.run_nominatim('refresh', '--functions')
122 with context.db.cursor() as cur:
123 for row in context.table:
124 PlaceColumn(context).add_row(row, False).db_insert(cur)
126 context.nominatim.reindex_placex(context.db)
127 check_database_integrity(context)
129 @when("updating postcodes")
130 def update_postcodes(context):
131 """ Rerun the calculation of postcodes.
133 context.nominatim.run_nominatim('refresh', '--postcodes')
135 @when("marking for delete (?P<oids>.*)")
136 def delete_places(context, oids):
137 """ Remove entries from the place table. Multiple ids may be given
138 separated by commas. Also runs all triggers
139 related to updates and reindexes the new data.
141 context.nominatim.run_nominatim('refresh', '--functions')
142 with context.db.cursor() as cur:
143 for oid in oids.split(','):
144 NominatimID(oid).query_osm_id(cur, 'DELETE FROM place WHERE {}')
146 context.nominatim.reindex_placex(context.db)
148 ################################ THEN ##################################
150 @then("(?P<table>placex|place) contains(?P<exact> exactly)?")
151 def check_place_contents(context, table, exact):
152 """ Check contents of place/placex tables. Each row represents a table row
153 and all data must match. Data not present in the expected table, may
154 be arbitry. The rows are identified via the 'object' column which must
155 have an identifier of the form '<NRW><osm id>[:<class>]'. When multiple
156 rows match (for example because 'class' was left out and there are
157 multiple entries for the given OSM object) then all must match. All
158 expected rows are expected to be present with at least one database row.
159 When 'exactly' is given, there must not be additional rows in the database.
161 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
162 expected_content = set()
163 for row in context.table:
164 nid = NominatimID(row['object'])
165 query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype'
166 if table == 'placex':
167 query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
168 query += " FROM %s WHERE {}" % (table, )
169 nid.query_osm_id(cur, query)
170 assert cur.rowcount > 0, "No rows found for " + row['object']
174 expected_content.add((res['osm_type'], res['osm_id'], res['class']))
176 DBRow(nid, res, context).assert_row(row, ['object'])
179 cur.execute('SELECT osm_type, osm_id, class from {}'.format(table))
180 assert expected_content == set([(r[0], r[1], r[2]) for r in cur])
183 @then("(?P<table>placex|place) has no entry for (?P<oid>.*)")
184 def check_place_has_entry(context, table, oid):
185 """ Ensure that no database row for the given object exists. The ID
186 must be of the form '<NRW><osm id>[:<class>]'.
188 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
189 NominatimID(oid).query_osm_id(cur, "SELECT * FROM %s where {}" % table)
190 assert cur.rowcount == 0, \
191 "Found {} entries for ID {}".format(cur.rowcount, oid)
194 @then("search_name contains(?P<exclude> not)?")
195 def check_search_name_contents(context, exclude):
196 """ Check contents of place/placex tables. Each row represents a table row
197 and all data must match. Data not present in the expected table, may
198 be arbitry. The rows are identified via the 'object' column which must
199 have an identifier of the form '<NRW><osm id>[:<class>]'. All
200 expected rows are expected to be present with at least one database row.
202 tokenizer = tokenizer_factory.get_tokenizer_for_db(context.nominatim.get_test_config())
204 with tokenizer.name_analyzer() as analyzer:
205 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
206 for row in context.table:
207 nid = NominatimID(row['object'])
208 nid.row_by_place_id(cur, 'search_name',
209 ['ST_X(centroid) as cx', 'ST_Y(centroid) as cy'])
210 assert cur.rowcount > 0, "No rows found for " + row['object']
213 db_row = DBRow(nid, res, context)
214 for name, value in zip(row.headings, row.cells):
215 if name in ('name_vector', 'nameaddress_vector'):
216 items = [x.strip() for x in value.split(',')]
217 tokens = analyzer.get_word_token_info(items)
220 assert len(tokens) >= len(items), \
221 "No word entry found for {}. Entries found: {!s}".format(value, len(tokens))
222 for word, token, wid in tokens:
224 assert wid not in res[name], \
225 "Found term for {}/{}: {}".format(nid, name, wid)
227 assert wid in res[name], \
228 "Missing term for {}/{}: {}".format(nid, name, wid)
229 elif name != 'object':
230 assert db_row.contains(name, value), db_row.assert_msg(name, value)
232 @then("search_name has no entry for (?P<oid>.*)")
233 def check_search_name_has_entry(context, oid):
234 """ Check that there is noentry in the search_name table for the given
235 objects. IDs are in format '<NRW><osm id>[:<class>]'.
237 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
238 NominatimID(oid).row_by_place_id(cur, 'search_name')
240 assert cur.rowcount == 0, \
241 "Found {} entries for ID {}".format(cur.rowcount, oid)
243 @then("location_postcode contains exactly")
244 def check_location_postcode(context):
245 """ Check full contents for location_postcode table. Each row represents a table row
246 and all data must match. Data not present in the expected table, may
247 be arbitry. The rows are identified via 'country' and 'postcode' columns.
248 All rows must be present as excepted and there must not be additional
251 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
252 cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
253 assert cur.rowcount == len(list(context.table)), \
254 "Postcode table has {} rows, expected {}.".format(cur.rowcount, len(list(context.table)))
258 key = (row['country_code'], row['postcode'])
259 assert key not in results, "Postcode table has duplicate entry: {}".format(row)
260 results[key] = DBRow((row['country_code'],row['postcode']), row, context)
262 for row in context.table:
263 db_row = results.get((row['country'],row['postcode']))
264 assert db_row is not None, \
265 "Missing row for country '{r['country']}' postcode '{r['postcode']}'.".format(r=row)
267 db_row.assert_row(row, ('country', 'postcode'))
269 @then("word contains(?P<exclude> not)?")
270 def check_word_table(context, exclude):
271 """ Check the contents of the word table. Each row represents a table row
272 and all data must match. Data not present in the expected table, may
273 be arbitry. The rows are identified via all given columns.
275 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
276 for row in context.table:
277 wheres = ' AND '.join(["{} = %s".format(h) for h in row.headings])
278 cur.execute("SELECT * from word WHERE " + wheres, list(row.cells))
280 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
282 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
285 @then("there are(?P<exclude> no)? word tokens for postcodes (?P<postcodes>.*)")
286 def check_word_table_for_postcodes(context, exclude, postcodes):
287 """ Check that the tokenizer produces postcode tokens for the given
288 postcodes. The postcodes are a comma-separated list of postcodes.
291 nctx = context.nominatim
292 tokenizer = tokenizer_factory.get_tokenizer_for_db(nctx.get_test_config())
293 with tokenizer.name_analyzer() as ana:
294 plist = [ana.normalize_postcode(p) for p in postcodes.split(',')]
298 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
299 if nctx.tokenizer == 'legacy_icu':
300 cur.execute("""SELECT info->>'postcode' FROM word
301 WHERE type = 'P' and info->>'postcode' = any(%s)""",
304 cur.execute("""SELECT word FROM word WHERE word = any(%s)
305 and class = 'place' and type = 'postcode'""",
308 found = [row[0] for row in cur]
309 assert len(found) == len(set(found)), f"Duplicate rows for postcodes: {found}"
312 assert len(found) == 0, f"Unexpected postcodes: {found}"
314 assert set(found) == set(plist), \
315 f"Missing postcodes {set(plist) - set(found)}. Found: {found}"
317 @then("place_addressline contains")
318 def check_place_addressline(context):
319 """ Check the contents of the place_addressline table. Each row represents
320 a table row and all data must match. Data not present in the expected
321 table, may be arbitry. The rows are identified via the 'object' column,
322 representing the addressee and the 'address' column, representing the
325 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
326 for row in context.table:
327 nid = NominatimID(row['object'])
328 pid = nid.get_place_id(cur)
329 apid = NominatimID(row['address']).get_place_id(cur)
330 cur.execute(""" SELECT * FROM place_addressline
331 WHERE place_id = %s AND address_place_id = %s""",
333 assert cur.rowcount > 0, \
334 "No rows found for place %s and address %s" % (row['object'], row['address'])
337 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
339 @then("place_addressline doesn't contain")
340 def check_place_addressline_exclude(context):
341 """ Check that the place_addressline doesn't contain any entries for the
342 given addressee/address item pairs.
344 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
345 for row in context.table:
346 pid = NominatimID(row['object']).get_place_id(cur)
347 apid = NominatimID(row['address']).get_place_id(cur)
348 cur.execute(""" SELECT * FROM place_addressline
349 WHERE place_id = %s AND address_place_id = %s""",
351 assert cur.rowcount == 0, \
352 "Row found for place %s and address %s" % (row['object'], row['address'])
354 @then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
355 def check_location_property_osmline(context, oid, neg):
356 """ Check that the given way is present in the interpolation table.
358 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
359 cur.execute("""SELECT *, ST_AsText(linegeo) as geomtxt
360 FROM location_property_osmline
361 WHERE osm_id = %s AND startnumber IS NOT NULL""",
365 assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
368 todo = list(range(len(list(context.table))))
371 row = context.table[i]
372 if (int(row['start']) == res['startnumber']
373 and int(row['end']) == res['endnumber']):
377 assert False, "Unexpected row " + str(res)
379 DBRow(oid, res, context).assert_row(row, ('start', 'end'))