]> 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
9 def check_database_integrity(context):
10     """ Check some generic constraints on the tables.
11     """
12     # place_addressline should not have duplicate (place_id, address_place_id)
13     cur = context.db.cursor()
14     cur.execute("""SELECT count(*) FROM
15                     (SELECT place_id, address_place_id, count(*) as c
16                      FROM place_addressline GROUP BY place_id, address_place_id) x
17                    WHERE c > 1""")
18     assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
19
20
21 ################################ GIVEN ##################################
22
23 @given("the (?P<named>named )?places")
24 def add_data_to_place_table(context, named):
25     """ Add entries into the place table. 'named places' makes sure that
26         the entries get a random name when none is explicitly given.
27     """
28     with context.db.cursor() as cur:
29         cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
30         for row in context.table:
31             PlaceColumn(context).add_row(row, named is not None).db_insert(cur)
32         cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
33
34 @given("the relations")
35 def add_data_to_planet_relations(context):
36     """ Add entries into the osm2pgsql relation middle table. This is needed
37         for tests on data that looks up members.
38     """
39     with context.db.cursor() as cur:
40         for r in context.table:
41             last_node = 0
42             last_way = 0
43             parts = []
44             if r['members']:
45                 members = []
46                 for m in r['members'].split(','):
47                     mid = NominatimID(m)
48                     if mid.typ == 'N':
49                         parts.insert(last_node, int(mid.oid))
50                         last_node += 1
51                         last_way += 1
52                     elif mid.typ == 'W':
53                         parts.insert(last_way, int(mid.oid))
54                         last_way += 1
55                     else:
56                         parts.append(int(mid.oid))
57
58                     members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
59             else:
60                 members = None
61
62             tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
63
64             cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags)
65                            VALUES (%s, %s, %s, %s, %s, %s)""",
66                         (r['id'], last_node, last_way, parts, members, list(tags)))
67
68 @given("the ways")
69 def add_data_to_planet_ways(context):
70     """ Add entries into the osm2pgsql way middle table. This is necessary for
71         tests on that that looks up node ids in this table.
72     """
73     with context.db.cursor() as cur:
74         for r in context.table:
75             tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
76             nodes = [ int(x.strip()) for x in r['nodes'].split(',') ]
77
78             cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
79                         (r['id'], nodes, list(tags)))
80
81 ################################ WHEN ##################################
82
83 @when("importing")
84 def import_and_index_data_from_place_table(context):
85     """ Import data previously set up in the place table.
86     """
87     context.nominatim.copy_from_place(context.db)
88     context.nominatim.run_setup_script('calculate-postcodes', 'index', 'index-noanalyse')
89     check_database_integrity(context)
90
91 @when("updating places")
92 def update_place_table(context):
93     """ Update the place table with the given data. Also runs all triggers
94         related to updates and reindexes the new data.
95     """
96     context.nominatim.run_setup_script(
97         'create-functions', 'create-partition-functions', 'enable-diff-updates')
98     with context.db.cursor() as cur:
99         for row in context.table:
100             PlaceColumn(context).add_row(row, False).db_insert(cur)
101
102     context.nominatim.reindex_placex(context.db)
103     check_database_integrity(context)
104
105 @when("updating postcodes")
106 def update_postcodes(context):
107     """ Rerun the calculation of postcodes.
108     """
109     context.nominatim.run_update_script('calculate-postcodes')
110
111 @when("marking for delete (?P<oids>.*)")
112 def delete_places(context, oids):
113     """ Remove entries from the place table. Multiple ids may be given
114         separated by commas. Also runs all triggers
115         related to updates and reindexes the new data.
116     """
117     context.nominatim.run_setup_script(
118         'create-functions', 'create-partition-functions', 'enable-diff-updates')
119     with context.db.cursor() as cur:
120         for oid in oids.split(','):
121             NominatimID(oid).query_osm_id(cur, 'DELETE FROM place WHERE {}')
122
123     context.nominatim.reindex_placex(context.db)
124
125 ################################ THEN ##################################
126
127 @then("(?P<table>placex|place) contains(?P<exact> exactly)?")
128 def check_place_contents(context, table, exact):
129     """ Check contents of place/placex tables. Each row represents a table row
130         and all data must match. Data not present in the expected table, may
131         be arbitry. The rows are identified via the 'object' column which must
132         have an identifier of the form '<NRW><osm id>[:<class>]'. When multiple
133         rows match (for example because 'class' was left out and there are
134         multiple entries for the given OSM object) then all must match. All
135         expected rows are expected to be present with at least one database row.
136         When 'exactly' is given, there must not be additional rows in the database.
137     """
138     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
139         expected_content = set()
140         for row in context.table:
141             nid = NominatimID(row['object'])
142             query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype'
143             if table == 'placex':
144                 query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy'
145             query += " FROM %s WHERE {}" % (table, )
146             nid.query_osm_id(cur, query)
147             assert cur.rowcount > 0, "No rows found for " + row['object']
148
149             for res in cur:
150                 if exact:
151                     expected_content.add((res['osm_type'], res['osm_id'], res['class']))
152
153                 DBRow(nid, res, context).assert_row(row, ['object'])
154
155         if exact:
156             cur.execute('SELECT osm_type, osm_id, class from {}'.format(table))
157             assert expected_content == set([(r[0], r[1], r[2]) for r in cur])
158
159
160 @then("(?P<table>placex|place) has no entry for (?P<oid>.*)")
161 def check_place_has_entry(context, table, oid):
162     """ Ensure that no database row for the given object exists. The ID
163         must be of the form '<NRW><osm id>[:<class>]'.
164     """
165     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
166         NominatimID(oid).query_osm_id(cur, "SELECT * FROM %s where {}" % table)
167         assert cur.rowcount == 0, \
168                "Found {} entries for ID {}".format(cur.rowcount, oid)
169
170
171 @then("search_name contains(?P<exclude> not)?")
172 def check_search_name_contents(context, exclude):
173     """ Check contents of place/placex tables. Each row represents a table row
174         and all data must match. Data not present in the expected table, may
175         be arbitry. The rows are identified via the 'object' column which must
176         have an identifier of the form '<NRW><osm id>[:<class>]'. All
177         expected rows are expected to be present with at least one database row.
178     """
179     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
180         for row in context.table:
181             nid = NominatimID(row['object'])
182             nid.row_by_place_id(cur, 'search_name',
183                                 ['ST_X(centroid) as cx', 'ST_Y(centroid) as cy'])
184             assert cur.rowcount > 0, "No rows found for " + row['object']
185
186             for res in cur:
187                 db_row = DBRow(nid, res, context)
188                 for name, value in zip(row.headings, row.cells):
189                     if name in ('name_vector', 'nameaddress_vector'):
190                         items = [x.strip() for x in value.split(',')]
191                         with context.db.cursor() as subcur:
192                             subcur.execute(""" SELECT word_id, word_token
193                                                FROM word, (SELECT unnest(%s::TEXT[]) as term) t
194                                                WHERE word_token = make_standard_name(t.term)
195                                                      and class is null and country_code is null
196                                                      and operator is null
197                                               UNION
198                                                SELECT word_id, word_token
199                                                FROM word, (SELECT unnest(%s::TEXT[]) as term) t
200                                                WHERE word_token = ' ' || make_standard_name(t.term)
201                                                      and class is null and country_code is null
202                                                      and operator is null
203                                            """,
204                                            (list(filter(lambda x: not x.startswith('#'), items)),
205                                             list(filter(lambda x: x.startswith('#'), items))))
206                             if not exclude:
207                                 assert subcur.rowcount >= len(items), \
208                                     "No word entry found for {}. Entries found: {!s}".format(value, subcur.rowcount)
209                             for wid in subcur:
210                                 present = wid[0] in res[name]
211                                 if exclude:
212                                     assert not present, "Found term for {}/{}: {}".format(row['object'], name, wid[1])
213                                 else:
214                                     assert present, "Missing term for {}/{}: {}".fromat(row['object'], name, wid[1])
215                     elif name != 'object':
216                         assert db_row.contains(name, value), db_row.assert_msg(name, value)
217
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>]'.
222     """
223     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
224         NominatimID(oid).row_by_place_id(cur, 'search_name')
225
226         assert cur.rowcount == 0, \
227                "Found {} entries for ID {}".format(cur.rowcount, oid)
228
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
235         rows.
236     """
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 {}.".foramt(cur.rowcount, len(list(context.table)))
241
242         results = {}
243         for row in cur:
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)
247
248         for row in context.table:
249             db_row = results.get((row['country'],row['postcode']))
250             assert db_row is not None, \
251                 "Missing row for country '{r['country']}' postcode '{r['postcode']}'.".format(r=row)
252
253             db_row.assert_row(row, ('country', 'postcode'))
254
255 @then("word contains(?P<exclude> not)?")
256 def check_word_table(context, exclude):
257     """ Check the contents of the word table. Each row represents a table row
258         and all data must match. Data not present in the expected table, may
259         be arbitry. The rows are identified via all given columns.
260     """
261     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
262         for row in context.table:
263             wheres = ' AND '.join(["{} = %s".format(h) for h in row.headings])
264             cur.execute("SELECT * from word WHERE " + wheres, list(row.cells))
265             if exclude:
266                 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
267             else:
268                 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
269
270 @then("place_addressline contains")
271 def check_place_addressline(context):
272     """ Check the contents of the place_addressline table. Each row represents
273         a table row and all data must match. Data not present in the expected
274         table, may be arbitry. The rows are identified via the 'object' column,
275         representing the addressee and the 'address' column, representing the
276         address item.
277     """
278     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
279         for row in context.table:
280             nid = NominatimID(row['object'])
281             pid = nid.get_place_id(cur)
282             apid = NominatimID(row['address']).get_place_id(cur)
283             cur.execute(""" SELECT * FROM place_addressline
284                             WHERE place_id = %s AND address_place_id = %s""",
285                         (pid, apid))
286             assert cur.rowcount > 0, \
287                         "No rows found for place %s and address %s" % (row['object'], row['address'])
288
289             for res in cur:
290                 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
291
292 @then("place_addressline doesn't contain")
293 def check_place_addressline_exclude(context):
294     """ Check that the place_addressline doesn't contain any entries for the
295         given addressee/address item pairs.
296     """
297     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
298         for row in context.table:
299             pid = NominatimID(row['object']).get_place_id(cur)
300             apid = NominatimID(row['address']).get_place_id(cur)
301             cur.execute(""" SELECT * FROM place_addressline
302                             WHERE place_id = %s AND address_place_id = %s""",
303                         (pid, apid))
304             assert cur.rowcount == 0, \
305                 "Row found for place %s and address %s" % (row['object'], row['address'])
306
307 @then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
308 def check_location_property_osmline(context, oid, neg):
309     """ Check that the given way is present in the interpolation table.
310     """
311     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
312         cur.execute("""SELECT *, ST_AsText(linegeo) as geomtxt
313                        FROM location_property_osmline
314                        WHERE osm_id = %s AND startnumber IS NOT NULL""",
315                     (oid, ))
316
317         if neg:
318             assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
319             return
320
321         todo = list(range(len(list(context.table))))
322         for res in cur:
323             for i in todo:
324                 row = context.table[i]
325                 if (int(row['start']) == res['startnumber']
326                     and int(row['end']) == res['endnumber']):
327                     todo.remove(i)
328                     break
329             else:
330                 assert False, "Unexpected row " + str(res)
331
332             DBRow(oid, res, context).assert_row(row, ('start', 'end'))
333
334         assert not todo
335
336