]> git.openstreetmap.org Git - nominatim.git/blob - test/bdd/steps/steps_db_ops.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / test / bdd / steps / steps_db_ops.py
1 from itertools import chain
2
3 import psycopg2.extras
4
5 from place_inserter import PlaceColumn
6 from table_compare import NominatimID, DBRow
7
8 from nominatim.indexer.indexer import Indexer
9
10 def check_database_integrity(context):
11     """ Check some generic constraints on the tables.
12     """
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
18                    WHERE c > 1""")
19     assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
20
21
22 ################################ GIVEN ##################################
23
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.
28     """
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')
34
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.
39     """
40     with context.db.cursor() as cur:
41         for r in context.table:
42             last_node = 0
43             last_way = 0
44             parts = []
45             if r['members']:
46                 members = []
47                 for m in r['members'].split(','):
48                     mid = NominatimID(m)
49                     if mid.typ == 'N':
50                         parts.insert(last_node, int(mid.oid))
51                         last_node += 1
52                         last_way += 1
53                     elif mid.typ == 'W':
54                         parts.insert(last_way, int(mid.oid))
55                         last_way += 1
56                     else:
57                         parts.append(int(mid.oid))
58
59                     members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
60             else:
61                 members = None
62
63             tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
64
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)))
68
69 @given("the ways")
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.
73     """
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(',') ]
78
79             cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
80                         (r['id'], nodes, list(tags)))
81
82 ################################ WHEN ##################################
83
84 @when("importing")
85 def import_and_index_data_from_place_table(context):
86     """ Import data previously set up in the place table.
87     """
88     context.nominatim.copy_from_place(context.db)
89     context.nominatim.run_setup_script('calculate-postcodes')
90
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)
94
95     check_database_integrity(context)
96
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.
101     """
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)
106
107     context.nominatim.reindex_placex(context.db)
108     check_database_integrity(context)
109
110 @when("updating postcodes")
111 def update_postcodes(context):
112     """ Rerun the calculation of postcodes.
113     """
114     context.nominatim.run_nominatim('refresh', '--postcodes')
115
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.
121     """
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 {}')
126
127     context.nominatim.reindex_placex(context.db)
128
129 ################################ THEN ##################################
130
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.
141     """
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']
152
153             for res in cur:
154                 if exact:
155                     expected_content.add((res['osm_type'], res['osm_id'], res['class']))
156
157                 DBRow(nid, res, context).assert_row(row, ['object'])
158
159         if exact:
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])
162
163
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>]'.
168     """
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)
173
174
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.
182     """
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']
189
190             for res in cur:
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
200                                                      and operator is null
201                                               UNION
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
206                                                      and operator is null
207                                            """,
208                                            (list(filter(lambda x: not x.startswith('#'), items)),
209                                             list(filter(lambda x: x.startswith('#'), items))))
210                             if not exclude:
211                                 assert subcur.rowcount >= len(items), \
212                                     "No word entry found for {}. Entries found: {!s}".format(value, subcur.rowcount)
213                             for wid in subcur:
214                                 present = wid[0] in res[name]
215                                 if exclude:
216                                     assert not present, "Found term for {}/{}: {}".format(row['object'], name, wid[1])
217                                 else:
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)
221
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>]'.
226     """
227     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
228         NominatimID(oid).row_by_place_id(cur, 'search_name')
229
230         assert cur.rowcount == 0, \
231                "Found {} entries for ID {}".format(cur.rowcount, oid)
232
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
239         rows.
240     """
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)))
245
246         results = {}
247         for row in cur:
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)
251
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)
256
257             db_row.assert_row(row, ('country', 'postcode'))
258
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.
264     """
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))
269             if exclude:
270                 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
271             else:
272                 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
273
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
280         address item.
281     """
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""",
289                         (pid, apid))
290             assert cur.rowcount > 0, \
291                         "No rows found for place %s and address %s" % (row['object'], row['address'])
292
293             for res in cur:
294                 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
295
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.
300     """
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""",
307                         (pid, apid))
308             assert cur.rowcount == 0, \
309                 "Row found for place %s and address %s" % (row['object'], row['address'])
310
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.
314     """
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""",
319                     (oid, ))
320
321         if neg:
322             assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
323             return
324
325         todo = list(range(len(list(context.table))))
326         for res in cur:
327             for i in todo:
328                 row = context.table[i]
329                 if (int(row['start']) == res['startnumber']
330                     and int(row['end']) == res['endnumber']):
331                     todo.remove(i)
332                     break
333             else:
334                 assert False, "Unexpected row " + str(res)
335
336             DBRow(oid, res, context).assert_row(row, ('start', 'end'))
337
338         assert not todo
339
340