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'))