]> git.openstreetmap.org Git - nominatim.git/blob - tests/steps/db_setup.py
Remove interpolation lines from placex and save them in an extra table.
[nominatim.git] / tests / steps / db_setup.py
1 """ Steps for setting up a test database with imports and updates.
2
3     There are two ways to state geometries for test data: with coordinates
4     and via scenes.
5
6     Coordinates should be given as a wkt without the enclosing type name.
7
8     Scenes are prepared geometries which can be found in the scenes/data/
9     directory. Each scene is saved in a .wkt file with its name, which
10     contains a list of id/wkt pairs. A scene can be set globally
11     for a scene by using the step `the scene <scene name>`. Then each
12     object should be refered to as `:<object id>`. A geometry can also
13     be referred to without loading the scene by explicitly stating the
14     scene: `<scene name>:<object id>`.
15 """
16
17 from nose.tools import *
18 from lettuce import *
19 import psycopg2
20 import psycopg2.extensions
21 import psycopg2.extras
22 import os
23 import subprocess
24 import random
25 import base64
26 import sys
27
28 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
29
30 @before.each_scenario
31 def setup_test_database(scenario):
32     """ Creates a new test database from the template database
33         that was set up earlier in terrain.py. Will be done only
34         for scenarios whose feature is tagged with 'DB'.
35     """
36     if scenario.feature.tags is not None and 'DB' in scenario.feature.tags:
37         world.db_template_setup()
38         world.write_nominatim_config(world.config.test_db)
39         conn = psycopg2.connect(database=world.config.template_db)
40         conn.set_isolation_level(0)
41         cur = conn.cursor()
42         cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
43         cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
44         conn.close()
45         world.conn = psycopg2.connect(database=world.config.test_db)
46         psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
47
48 @step('a wiped database')
49 def db_setup_wipe_db(step):
50     """Explicit DB scenario setup only needed
51        to work around a bug where scenario outlines don't call
52        before_each_scenario correctly.
53     """
54     if hasattr(world, 'conn'):
55         world.conn.close()
56     conn = psycopg2.connect(database=world.config.template_db)
57     conn.set_isolation_level(0)
58     cur = conn.cursor()
59     cur.execute('DROP DATABASE IF EXISTS %s' % (world.config.test_db, ))
60     cur.execute('CREATE DATABASE %s TEMPLATE = %s' % (world.config.test_db, world.config.template_db))
61     conn.close()
62     world.conn = psycopg2.connect(database=world.config.test_db)
63     psycopg2.extras.register_hstore(world.conn, globally=False, unicode=True)
64
65
66 @after.each_scenario
67 def tear_down_test_database(scenario):
68     """ Drops any previously created test database.
69     """
70     if hasattr(world, 'conn'):
71         world.conn.close()
72     if scenario.feature.tags is not None and 'DB' in scenario.feature.tags and not world.config.keep_scenario_db:
73         conn = psycopg2.connect(database=world.config.template_db)
74         conn.set_isolation_level(0)
75         cur = conn.cursor()
76         cur.execute('DROP DATABASE %s' % (world.config.test_db,))
77         conn.close()
78
79
80 def _format_placex_cols(cols, geomtype, force_name):
81     if 'name' in cols:
82         if cols['name'].startswith("'"):
83             cols['name'] = world.make_hash(cols['name'])
84         else:
85             cols['name'] = { 'name' : cols['name'] }
86     elif force_name:
87         cols['name'] = { 'name' : base64.urlsafe_b64encode(os.urandom(int(random.random()*30))) }
88     if 'extratags' in cols:
89         cols['extratags'] = world.make_hash(cols['extratags'])
90     if 'admin_level' not in cols:
91         cols['admin_level'] = 100
92     if 'geometry' in cols:
93         coords = world.get_scene_geometry(cols['geometry'])
94         if coords is None:
95             coords = "'%s(%s)'::geometry" % (geomtype, cols['geometry'])
96         else:
97             coords = "'%s'::geometry" % coords.wkt
98         cols['geometry'] = coords
99     for k in cols:
100         if not cols[k]:
101             cols[k] = None
102
103
104 def _insert_place_table_nodes(places, force_name):
105     cur = world.conn.cursor()
106     for line in places:
107         cols = dict(line)
108         cols['osm_type'] = 'N'
109         _format_placex_cols(cols, 'POINT', force_name)
110         if 'geometry' in cols:
111             coords = cols.pop('geometry')
112         else:
113             coords = "ST_Point(%f, %f)" % (random.random()*360 - 180, random.random()*180 - 90)
114
115         query = 'INSERT INTO place (%s,geometry) values(%s, ST_SetSRID(%s, 4326))' % (
116               ','.join(cols.iterkeys()),
117               ','.join(['%s' for x in range(len(cols))]),
118               coords
119              )
120         cur.execute(query, cols.values())
121     world.conn.commit()
122
123
124 def _insert_place_table_objects(places, geomtype, force_name):
125     cur = world.conn.cursor()
126     for line in places:
127         cols = dict(line)
128         if 'osm_type' not in cols:
129             cols['osm_type'] = 'W'
130         _format_placex_cols(cols, geomtype, force_name)
131         coords = cols.pop('geometry')
132
133         query = 'INSERT INTO place (%s, geometry) values(%s, ST_SetSRID(%s, 4326))' % (
134               ','.join(cols.iterkeys()),
135               ','.join(['%s' for x in range(len(cols))]),
136               coords
137              )
138         cur.execute(query, cols.values())
139     world.conn.commit()
140
141 @step(u'the scene (.*)')
142 def import_set_scene(step, scene):
143     world.load_scene(scene)
144
145 @step(u'the (named )?place (node|way|area)s')
146 def import_place_table_nodes(step, named, osmtype):
147     """Insert a list of nodes into the place table.
148        Expects a table where columns are named in the same way as place.
149     """
150     cur = world.conn.cursor()
151     cur.execute('ALTER TABLE place DISABLE TRIGGER place_before_insert')
152     if osmtype == 'node':
153         _insert_place_table_nodes(step.hashes, named is not None)
154     elif osmtype == 'way' :
155         _insert_place_table_objects(step.hashes, 'LINESTRING', named is not None)
156     elif osmtype == 'area' :
157         _insert_place_table_objects(step.hashes, 'POLYGON', named is not None)
158     cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert')
159     cur.close()
160     world.conn.commit()
161
162
163 @step(u'the relations')
164 def import_fill_planet_osm_rels(step):
165     """Adds a raw relation to the osm2pgsql table.
166        Three columns need to be suplied: id, tags, members.
167     """
168     cur = world.conn.cursor()
169     for line in step.hashes:
170         members = []
171         parts = { 'n' : [], 'w' : [], 'r' : [] }
172         if line['members'].strip():
173             for mem in line['members'].split(','):
174                 memparts = mem.strip().split(':', 2)
175                 memid = memparts[0].lower()
176                 parts[memid[0]].append(int(memid[1:]))
177                 members.append(memid)
178                 if len(memparts) == 2:
179                     members.append(memparts[1])
180                 else:
181                     members.append('')
182         tags = []
183         for k,v in world.make_hash(line['tags']).iteritems():
184             tags.extend((k,v))
185         if not members:
186             members = None
187
188         cur.execute("""INSERT INTO planet_osm_rels
189                       (id, way_off, rel_off, parts, members, tags)
190                       VALUES (%s, %s, %s, %s, %s, %s)""",
191                    (line['id'], len(parts['n']), len(parts['n']) + len(parts['w']),
192                    parts['n'] + parts['w'] + parts['r'], members, tags))
193     world.conn.commit()
194         
195
196 @step(u'the ways')
197 def import_fill_planet_osm_ways(step):
198     cur = world.conn.cursor()
199     for line in step.hashes:
200         if 'tags' in line:
201             tags = world.make_hash(line['tags'])
202         else:
203             tags = None
204         nodes = [int(x.strip()) for x in line['nodes'].split(',')]
205
206         cur.execute("""INSERT INTO planet_osm_ways (id, nodes, tags)
207                        VALUES (%s, %s, %s)""",
208                     (line['id'], nodes, tags))
209     world.conn.commit()
210
211 ############### import and update steps #######################################
212
213 @step(u'importing')
214 def import_database(step):
215     """ Runs the actual indexing. """
216     world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions')
217     cur = world.conn.cursor()
218     #world.db_dump_table('place')
219     cur.execute("""insert into placex (osm_type, osm_id, class, type, name, admin_level,
220                    housenumber, street, addr_place, isin, postcode, country_code, extratags,
221                    geometry) select * from place where not (class='place' and type='houses' and osm_type='W')""")
222     cur.execute("""select insert_osmline (osm_id, housenumber, street, addr_place, postcode, country_code, geometry) from place where class='place' and type='houses' and osm_type='W'""")
223     world.conn.commit()
224     world.run_nominatim_script('setup', 'index', 'index-noanalyse')
225     #world.db_dump_table('placex')
226     #world.db_dump_table('location_property_osmline')
227
228 @step(u'updating place (node|way|area)s')
229 def update_place_table_nodes(step, osmtype):
230     """ Replace a geometry in place by reinsertion and reindex database."""
231     world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
232     if osmtype == 'node':
233         _insert_place_table_nodes(step.hashes, False)
234     elif osmtype == 'way':
235         _insert_place_table_objects(step.hashes, 'LINESTRING', False)
236     elif osmtype == 'area':
237         _insert_place_table_objects(step.hashes, 'POLYGON', False)
238     world.run_nominatim_script('update', 'index')
239
240 @step(u'marking for delete (.*)')
241 def update_delete_places(step, places):
242     """ Remove an entry from place and reindex database.
243     """
244     world.run_nominatim_script('setup', 'create-functions', 'create-partition-functions', 'enable-diff-updates')
245     cur = world.conn.cursor()
246     for place in places.split(','):
247         osmtype, osmid, cls = world.split_id(place)
248         if cls is None:
249             q = "delete from place where osm_type = %s and osm_id = %s"
250             params = (osmtype, osmid)
251         else:
252             q = "delete from place where osm_type = %s and osm_id = %s and class = %s"
253             params = (osmtype, osmid, cls)
254         cur.execute(q, params)
255     world.conn.commit()
256     #world.db_dump_table('placex')
257     world.run_nominatim_script('update', 'index')
258
259
260
261 @step(u'sending query "(.*)"( with dups)?$')
262 def query_cmd(step, query, with_dups):
263     """ Results in standard query output. The same tests as for API queries
264         can be used.
265     """
266     cmd = [os.path.join(world.config.source_dir, 'utils', 'query.php'),
267            '--search', query]
268     if with_dups is not None:
269         cmd.append('--nodedupe')
270     proc = subprocess.Popen(cmd, cwd=world.config.source_dir,
271                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
272     (outp, err) = proc.communicate()
273     assert (proc.returncode == 0), "query.php failed with message: %s" % err
274     world.page = outp
275     world.response_format = 'json'
276     world.request_type = 'search'
277     world.returncode = 200
278