1 from itertools import chain
5 from place_inserter import PlaceColumn
6 from table_compare import NominatimID, DBRow
8 from nominatim.indexer.indexer import Indexer
10 def check_database_integrity(context):
11 """ Check some generic constraints on the tables.
13 # place_addressline should not have duplicate (place_id, address_place_id)
14 cur = context.db.cursor()
15 cur.execute("""SELECT count(*) FROM
16 (SELECT place_id, address_place_id, count(*) as c
17 FROM place_addressline GROUP BY place_id, address_place_id) x
19 assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
22 ################################ GIVEN ##################################
24 @given("the (?P<named>named )?places")
25 def add_data_to_place_table(context, named):
26 """ Add entries into the place table. 'named places' makes sure that
27 the entries get a random name when none is explicitly given.
29 with context.db.cursor() as cur:
30 cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
31 for row in context.table:
32 PlaceColumn(context).add_row(row, named is not None).db_insert(cur)
33 cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
35 @given("the relations")
36 def add_data_to_planet_relations(context):
37 """ Add entries into the osm2pgsql relation middle table. This is needed
38 for tests on data that looks up members.
40 with context.db.cursor() as cur:
41 for r in context.table:
47 for m in r['members'].split(','):
50 parts.insert(last_node, int(mid.oid))
54 parts.insert(last_way, int(mid.oid))
57 parts.append(int(mid.oid))
59 members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
63 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
65 cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags)
66 VALUES (%s, %s, %s, %s, %s, %s)""",
67 (r['id'], last_node, last_way, parts, members, list(tags)))
70 def add_data_to_planet_ways(context):
71 """ Add entries into the osm2pgsql way middle table. This is necessary for
72 tests on that that looks up node ids in this table.
74 with context.db.cursor() as cur:
75 for r in context.table:
76 tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
77 nodes = [ int(x.strip()) for x in r['nodes'].split(',') ]
79 cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
80 (r['id'], nodes, list(tags)))
82 ################################ WHEN ##################################
85 def import_and_index_data_from_place_table(context):
86 """ Import data previously set up in the place table.
88 context.nominatim.copy_from_place(context.db)
89 context.nominatim.run_setup_script('calculate-postcodes')
91 # Call directly as the refresh function does not include postcodes.
92 indexer = Indexer(context.nominatim.test_env['NOMINATIM_DATABASE_DSN'][6:], 1)
93 indexer.index_full(analyse=False)
95 check_database_integrity(context)
97 @when("updating places")
98 def update_place_table(context):
99 """ Update the place table with the given data. Also runs all triggers
100 related to updates and reindexes the new data.
102 context.nominatim.run_nominatim('refresh', '--functions')
103 with context.db.cursor() as cur:
104 for row in context.table:
105 PlaceColumn(context).add_row(row, False).db_insert(cur)
107 context.nominatim.reindex_placex(context.db)
108 check_database_integrity(context)
110 @when("updating postcodes")
111 def update_postcodes(context):
112 """ Rerun the calculation of postcodes.
114 context.nominatim.run_nominatim('refresh', '--postcodes')
116 @when("marking for delete (?P<oids>.*)")
117 def delete_places(context, oids):
118 """ Remove entries from the place table. Multiple ids may be given
119 separated by commas. Also runs all triggers
120 related to updates and reindexes the new data.
122 context.nominatim.run_nominatim('refresh', '--functions')
123 with context.db.cursor() as cur:
124 for oid in oids.split(','):
125 NominatimID(oid).query_osm_id(cur, 'DELETE FROM place WHERE {}')
127 context.nominatim.reindex_placex(context.db)
129 ################################ THEN ##################################
131 @then("(?P<table>placex|place) contains(?P<exact> exactly)?")
132 def check_place_contents(context, table, exact):
133 """ Check contents of place/placex tables. Each row represents a table row
134 and all data must match. Data not present in the expected table, may
135 be arbitry. The rows are identified via the 'object' column which must
136 have an identifier of the form '<NRW><osm id>[:<class>]'. When multiple
137 rows match (for example because 'class' was left out and there are
138 multiple entries for the given OSM object) then all must match. All
139 expected rows are expected to be present with at least one database row.
140 When 'exactly' is given, there must not be additional rows in the database.
142 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
143 expected_content = set()
144 for row in context.table:
145 nid = NominatimID(row['object'])
146 query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype'
147 if table == 'placex':
148 query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
149 query += " FROM %s WHERE {}" % (table, )
150 nid.query_osm_id(cur, query)
151 assert cur.rowcount > 0, "No rows found for " + row['object']
155 expected_content.add((res['osm_type'], res['osm_id'], res['class']))
157 DBRow(nid, res, context).assert_row(row, ['object'])
160 cur.execute('SELECT osm_type, osm_id, class from {}'.format(table))
161 assert expected_content == set([(r[0], r[1], r[2]) for r in cur])
164 @then("(?P<table>placex|place) has no entry for (?P<oid>.*)")
165 def check_place_has_entry(context, table, oid):
166 """ Ensure that no database row for the given object exists. The ID
167 must be of the form '<NRW><osm id>[:<class>]'.
169 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
170 NominatimID(oid).query_osm_id(cur, "SELECT * FROM %s where {}" % table)
171 assert cur.rowcount == 0, \
172 "Found {} entries for ID {}".format(cur.rowcount, oid)
175 @then("search_name contains(?P<exclude> not)?")
176 def check_search_name_contents(context, exclude):
177 """ Check contents of place/placex tables. Each row represents a table row
178 and all data must match. Data not present in the expected table, may
179 be arbitry. The rows are identified via the 'object' column which must
180 have an identifier of the form '<NRW><osm id>[:<class>]'. All
181 expected rows are expected to be present with at least one database row.
183 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
184 for row in context.table:
185 nid = NominatimID(row['object'])
186 nid.row_by_place_id(cur, 'search_name',
187 ['ST_X(centroid) as cx', 'ST_Y(centroid) as cy'])
188 assert cur.rowcount > 0, "No rows found for " + row['object']
191 db_row = DBRow(nid, res, context)
192 for name, value in zip(row.headings, row.cells):
193 if name in ('name_vector', 'nameaddress_vector'):
194 items = [x.strip() for x in value.split(',')]
195 with context.db.cursor() as subcur:
196 subcur.execute(""" SELECT word_id, word_token
197 FROM word, (SELECT unnest(%s::TEXT[]) as term) t
198 WHERE word_token = make_standard_name(t.term)
199 and class is null and country_code is null
202 SELECT word_id, word_token
203 FROM word, (SELECT unnest(%s::TEXT[]) as term) t
204 WHERE word_token = ' ' || make_standard_name(t.term)
205 and class is null and country_code is null
208 (list(filter(lambda x: not x.startswith('#'), items)),
209 list(filter(lambda x: x.startswith('#'), items))))
211 assert subcur.rowcount >= len(items), \
212 "No word entry found for {}. Entries found: {!s}".format(value, subcur.rowcount)
214 present = wid[0] in res[name]
216 assert not present, "Found term for {}/{}: {}".format(row['object'], name, wid[1])
218 assert present, "Missing term for {}/{}: {}".fromat(row['object'], name, wid[1])
219 elif name != 'object':
220 assert db_row.contains(name, value), db_row.assert_msg(name, value)
222 @then("search_name has no entry for (?P<oid>.*)")
223 def check_search_name_has_entry(context, oid):
224 """ Check that there is noentry in the search_name table for the given
225 objects. IDs are in format '<NRW><osm id>[:<class>]'.
227 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
228 NominatimID(oid).row_by_place_id(cur, 'search_name')
230 assert cur.rowcount == 0, \
231 "Found {} entries for ID {}".format(cur.rowcount, oid)
233 @then("location_postcode contains exactly")
234 def check_location_postcode(context):
235 """ Check full contents for location_postcode table. Each row represents a table row
236 and all data must match. Data not present in the expected table, may
237 be arbitry. The rows are identified via 'country' and 'postcode' columns.
238 All rows must be present as excepted and there must not be additional
241 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
242 cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
243 assert cur.rowcount == len(list(context.table)), \
244 "Postcode table has {} rows, expected {}.".foramt(cur.rowcount, len(list(context.table)))
248 key = (row['country_code'], row['postcode'])
249 assert key not in results, "Postcode table has duplicate entry: {}".format(row)
250 results[key] = DBRow((row['country_code'],row['postcode']), row, context)
252 for row in context.table:
253 db_row = results.get((row['country'],row['postcode']))
254 assert db_row is not None, \
255 "Missing row for country '{r['country']}' postcode '{r['postcode']}'.".format(r=row)
257 db_row.assert_row(row, ('country', 'postcode'))
259 @then("word contains(?P<exclude> not)?")
260 def check_word_table(context, exclude):
261 """ Check the contents of the word table. Each row represents a table row
262 and all data must match. Data not present in the expected table, may
263 be arbitry. The rows are identified via all given columns.
265 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
266 for row in context.table:
267 wheres = ' AND '.join(["{} = %s".format(h) for h in row.headings])
268 cur.execute("SELECT * from word WHERE " + wheres, list(row.cells))
270 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
272 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
274 @then("place_addressline contains")
275 def check_place_addressline(context):
276 """ Check the contents of the place_addressline table. Each row represents
277 a table row and all data must match. Data not present in the expected
278 table, may be arbitry. The rows are identified via the 'object' column,
279 representing the addressee and the 'address' column, representing the
282 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
283 for row in context.table:
284 nid = NominatimID(row['object'])
285 pid = nid.get_place_id(cur)
286 apid = NominatimID(row['address']).get_place_id(cur)
287 cur.execute(""" SELECT * FROM place_addressline
288 WHERE place_id = %s AND address_place_id = %s""",
290 assert cur.rowcount > 0, \
291 "No rows found for place %s and address %s" % (row['object'], row['address'])
294 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
296 @then("place_addressline doesn't contain")
297 def check_place_addressline_exclude(context):
298 """ Check that the place_addressline doesn't contain any entries for the
299 given addressee/address item pairs.
301 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
302 for row in context.table:
303 pid = NominatimID(row['object']).get_place_id(cur)
304 apid = NominatimID(row['address']).get_place_id(cur)
305 cur.execute(""" SELECT * FROM place_addressline
306 WHERE place_id = %s AND address_place_id = %s""",
308 assert cur.rowcount == 0, \
309 "Row found for place %s and address %s" % (row['object'], row['address'])
311 @then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
312 def check_location_property_osmline(context, oid, neg):
313 """ Check that the given way is present in the interpolation table.
315 with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
316 cur.execute("""SELECT *, ST_AsText(linegeo) as geomtxt
317 FROM location_property_osmline
318 WHERE osm_id = %s AND startnumber IS NOT NULL""",
322 assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
325 todo = list(range(len(list(context.table))))
328 row = context.table[i]
329 if (int(row['start']) == res['startnumber']
330 and int(row['end']) == res['endnumber']):
334 assert False, "Unexpected row " + str(res)
336 DBRow(oid, res, context).assert_row(row, ('start', 'end'))