1 from itertools import chain
 
   5 from place_inserter import PlaceColumn
 
   6 from table_compare import NominatimID, DBRow
 
   9 def check_database_integrity(context):
 
  10     """ Check some generic constraints on the tables.
 
  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
 
  18     assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline"
 
  21 ################################ GIVEN ##################################
 
  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.
 
  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')
 
  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.
 
  39     with context.db.cursor() as cur:
 
  40         for r in context.table:
 
  46                 for m in r['members'].split(','):
 
  49                         parts.insert(last_node, int(mid.oid))
 
  53                         parts.insert(last_way, int(mid.oid))
 
  56                         parts.append(int(mid.oid))
 
  58                     members.extend((mid.typ.lower() + mid.oid, mid.cls or ''))
 
  62             tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])
 
  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)))
 
  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.
 
  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(',') ]
 
  78             cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)",
 
  79                         (r['id'], nodes, list(tags)))
 
  81 ################################ WHEN ##################################
 
  84 def import_and_index_data_from_place_table(context):
 
  85     """ Import data previously set up in the place table.
 
  87     context.nominatim.copy_from_place(context.db)
 
  88     context.nominatim.run_setup_script('calculate-postcodes', 'index', 'index-noanalyse')
 
  89     check_database_integrity(context)
 
  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.
 
  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)
 
 102     context.nominatim.reindex_placex(context.db)
 
 103     check_database_integrity(context)
 
 105 @when("updating postcodes")
 
 106 def update_postcodes(context):
 
 107     """ Rerun the calculation of postcodes.
 
 109     context.nominatim.run_update_script('calculate-postcodes')
 
 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.
 
 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 {}')
 
 123     context.nominatim.reindex_placex(context.db)
 
 125 ################################ THEN ##################################
 
 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.
 
 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']
 
 151                     expected_content.add((res['osm_type'], res['osm_id'], res['class']))
 
 153                 DBRow(nid, res, context).assert_row(row, ['object'])
 
 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])
 
 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>]'.
 
 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)
 
 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.
 
 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']
 
 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
 
 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
 
 204                                            (list(filter(lambda x: not x.startswith('#'), items)),
 
 205                                             list(filter(lambda x: x.startswith('#'), items))))
 
 207                                 assert subcur.rowcount >= len(items), \
 
 208                                     "No word entry found for {}. Entries found: {!s}".format(value, subcur.rowcount)
 
 210                                 present = wid[0] in res[name]
 
 212                                     assert not present, "Found term for {}/{}: {}".format(row['object'], name, wid[1])
 
 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)
 
 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>]'.
 
 223     with context.db.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
 
 224         NominatimID(oid).row_by_place_id(cur, 'search_name')
 
 226         assert cur.rowcount == 0, \
 
 227                "Found {} entries for ID {}".format(cur.rowcount, oid)
 
 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
 
 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)))
 
 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)
 
 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)
 
 253             db_row.assert_row(row, ('country', 'postcode'))
 
 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.
 
 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))
 
 266                 assert cur.rowcount == 0, "Row still in word table: %s" % '/'.join(values)
 
 268                 assert cur.rowcount > 0, "Row not in word table: %s" % '/'.join(values)
 
 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
 
 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""",
 
 286             assert cur.rowcount > 0, \
 
 287                         "No rows found for place %s and address %s" % (row['object'], row['address'])
 
 290                 DBRow(nid, res, context).assert_row(row, ('address', 'object'))
 
 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.
 
 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""",
 
 304             assert cur.rowcount == 0, \
 
 305                 "Row found for place %s and address %s" % (row['object'], row['address'])
 
 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.
 
 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""",
 
 318             assert cur.rowcount == 0, "Interpolation found for way {}.".format(oid)
 
 321         todo = list(range(len(list(context.table))))
 
 324                 row = context.table[i]
 
 325                 if (int(row['start']) == res['startnumber']
 
 326                     and int(row['end']) == res['endnumber']):
 
 330                 assert False, "Unexpected row " + str(res)
 
 332             DBRow(oid, res, context).assert_row(row, ('start', 'end'))