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     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 203         for row in context.table:
 
 204             nid = NominatimID(row['object'])
 
 205             nid.row_by_place_id(cur, 'search_name',
 
 206                                 ['ST_X(centroid) as cx', 'ST_Y(centroid) as cy'])
 
 207             assert cur.rowcount > 0, "No rows found for " + row['object']
 
 210                 db_row = DBRow(nid, res, context)
 
 211                 for name, value in zip(row.headings, row.cells):
 
 212                     if name in ('name_vector', 'nameaddress_vector'):
 
 213                         items = [x.strip() for x in value.split(',')]
 
 214                         with context.db.cursor() as subcur:
 
 215                             subcur.execute(""" SELECT word_id, word_token
 
 216                                                FROM word, (SELECT unnest(%s::TEXT[]) as term) t
 
 217                                                WHERE word_token = make_standard_name(t.term)
 
 218                                                      and class is null and country_code is null
 
 221                                                SELECT word_id, word_token
 
 222                                                FROM word, (SELECT unnest(%s::TEXT[]) as term) t
 
 223                                                WHERE word_token = ' ' || make_standard_name(t.term)
 
 224                                                      and class is null and country_code is null
 
 227                                            (list(filter(lambda x: not x.startswith('#'), items)),
 
 228                                             list(filter(lambda x: x.startswith('#'), items))))
 
 230                                 assert subcur.rowcount >= len(items), \
 
 231                                     "No word entry found for {}. Entries found: {!s}".format(value, subcur.rowcount)
 
 233                                 present = wid[0] in res[name]
 
 235                                     assert not present, "Found term for {}/{}: {}".format(row['object'], name, wid[1])
 
 237                                     assert present, "Missing term for {}/{}: {}".format(row['object'], name, wid[1])
 
 238                     elif name != 'object':
 
 239                         assert db_row.contains(name, value), db_row.assert_msg(name, value)
 
 241 @then("search_name has no entry for (?P<oid>.*)")
 
 242 def check_search_name_has_entry(context, oid):
 
 243     """ Check that there is noentry in the search_name table for the given
 
 244         objects. IDs are in format '<NRW><osm id>[:<class>]'.
 
 246     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 247         NominatimID(oid).row_by_place_id(cur, 'search_name')
 
 249         assert cur.rowcount == 0, \
 
 250                "Found {} entries for ID {}".format(cur.rowcount, oid)
 
 252 @then("location_postcode contains exactly")
 
 253 def check_location_postcode(context):
 
 254     """ Check full contents for location_postcode table. Each row represents a table row
 
 255         and all data must match. Data not present in the expected table, may
 
 256         be arbitry. The rows are identified via 'country' and 'postcode' columns.
 
 257         All rows must be present as excepted and there must not be additional
 
 260     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 261         cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode")
 
 262         assert cur.rowcount == len(list(context.table)), \
 
 263             "Postcode table has {} rows, expected {}.".foramt(cur.rowcount, len(list(context.table)))
 
 267             key = (row['country_code'], row['postcode'])
 
 268             assert key not in results, "Postcode table has duplicate entry: {}".format(row)
 
 269             results[key] = DBRow((row['country_code'],row['postcode']), row, context)
 
 271         for row in context.table:
 
 272             db_row = results.get((row['country'],row['postcode']))
 
 273             assert db_row is not None, \
 
 274                 "Missing row for country '{r['country']}' postcode '{r['postcode']}'.".format(r=row)
 
 276             db_row.assert_row(row, ('country', 'postcode'))
 
 278 @then("word contains(?P<exclude> not)?")
 
 279 def check_word_table(context, exclude):
 
 280     """ Check the contents of the word table. Each row represents a table row
 
 281         and all data must match. Data not present in the expected table, may
 
 282         be arbitry. The rows are identified via all given columns.
 
 284     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 285         for row in context.table:
 
 286             wheres = ' AND '.join(["{} = %s".format(h) for h in row.headings])
 
 287             cur.execute("SELECT * from word WHERE " + wheres, list(row.cells))
 
 289                 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
 
 291                 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
 
 293 @then("place_addressline contains")
 
 294 def check_place_addressline(context):
 
 295     """ Check the contents of the place_addressline table. Each row represents
 
 296         a table row and all data must match. Data not present in the expected
 
 297         table, may be arbitry. The rows are identified via the 'object' column,
 
 298         representing the addressee and the 'address' column, representing the
 
 301     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 302         for row in context.table:
 
 303             nid = NominatimID(row['object'])
 
 304             pid = nid.get_place_id(cur)
 
 305             apid = NominatimID(row['address']).get_place_id(cur)
 
 306             cur.execute(""" SELECT * FROM place_addressline
 
 307                             WHERE place_id = %s AND address_place_id = %s""",
 
 309             assert cur.rowcount > 0, \
 
 310                         "No rows found for place %s and address %s" % (row['object'], row['address'])
 
 313                 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
 
 315 @then("place_addressline doesn't contain")
 
 316 def check_place_addressline_exclude(context):
 
 317     """ Check that the place_addressline doesn't contain any entries for the
 
 318         given addressee/address item pairs.
 
 320     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 321         for row in context.table:
 
 322             pid = NominatimID(row['object']).get_place_id(cur)
 
 323             apid = NominatimID(row['address']).get_place_id(cur)
 
 324             cur.execute(""" SELECT * FROM place_addressline
 
 325                             WHERE place_id = %s AND address_place_id = %s""",
 
 327             assert cur.rowcount == 0, \
 
 328                 "Row found for place %s and address %s" % (row['object'], row['address'])
 
 330 @then("W(?P<oid>\d+) expands to(?P<neg> no)? interpolation")
 
 331 def check_location_property_osmline(context, oid, neg):
 
 332     """ Check that the given way is present in the interpolation table.
 
 334     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 335         cur.execute("""SELECT *, ST_AsText(linegeo) as geomtxt
 
 336                        FROM location_property_osmline
 
 337                        WHERE osm_id = %s AND startnumber IS NOT NULL""",
 
 341             assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
 
 344         todo = list(range(len(list(context.table))))
 
 347                 row = context.table[i]
 
 348                 if (int(row['start']) == res['startnumber']
 
 349                     and int(row['end']) == res['endnumber']):
 
 353                 assert False, "Unexpected row " + str(res)
 
 355             DBRow(oid, res, context).assert_row(row, ('start', 'end'))