]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/migration.py
add migration for configurable tokenizer
[nominatim.git] / nominatim / tools / migration.py
1 """
2 Functions for database migration to newer software versions.
3 """
4 import logging
5
6 from nominatim.db import properties
7 from nominatim.db.connection import connect
8 from nominatim.version import NOMINATIM_VERSION
9 from nominatim.tools import refresh
10 from nominatim.tokenizer import factory as tokenizer_factory
11 from nominatim.errors import UsageError
12
13 LOG = logging.getLogger()
14
15 _MIGRATION_FUNCTIONS = []
16
17 def migrate(config, paths):
18     """ Check for the current database version and execute migrations,
19         if necesssary.
20     """
21     with connect(config.get_libpq_dsn()) as conn:
22         if conn.table_exists('nominatim_properties'):
23             db_version_str = properties.get_property(conn, 'database_version')
24         else:
25             db_version_str = None
26
27         if db_version_str is not None:
28             parts = db_version_str.split('.')
29             db_version = tuple([int(x) for x in parts[:2] + parts[2].split('-')])
30
31             if db_version == NOMINATIM_VERSION:
32                 LOG.warning("Database already at latest version (%s)", db_version_str)
33                 return 0
34
35             LOG.info("Detected database version: %s", db_version_str)
36         else:
37             db_version = _guess_version(conn)
38
39
40         has_run_migration = False
41         for version, func in _MIGRATION_FUNCTIONS:
42             if db_version <= version:
43                 LOG.warning("Runnning: %s (%s)", func.__doc__.split('\n', 1)[0],
44                             '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(version))
45                 kwargs = dict(conn=conn, config=config, paths=paths)
46                 func(**kwargs)
47                 has_run_migration = True
48
49         if has_run_migration:
50             LOG.warning('Updating SQL functions.')
51             refresh.create_functions(conn, config)
52
53         properties.set_property(conn, 'database_version',
54                                 '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(NOMINATIM_VERSION))
55
56         conn.commit()
57
58     return 0
59
60
61 def _guess_version(conn):
62     """ Guess a database version when there is no property table yet.
63         Only migrations for 3.6 and later are supported, so bail out
64         when the version seems older.
65     """
66     with conn.cursor() as cur:
67         # In version 3.6, the country_name table was updated. Check for that.
68         cnt = cur.scalar("""SELECT count(*) FROM
69                             (SELECT svals(name) FROM  country_name
70                              WHERE country_code = 'gb')x;
71                          """)
72         if cnt < 100:
73             LOG.fatal('It looks like your database was imported with a version '
74                       'prior to 3.6.0. Automatic migration not possible.')
75             raise UsageError('Migration not possible.')
76
77     return (3, 5, 0, 99)
78
79
80
81 def _migration(major, minor, patch=0, dbpatch=0):
82     """ Decorator for a single migration step. The parameters describe the
83         version after which the migration is applicable, i.e before changing
84         from the given version to the next, the migration is required.
85
86         All migrations are run in the order in which they are defined in this
87         file. Do not run global SQL scripts for migrations as you cannot be sure
88         that these scripts do the same in later versions.
89
90         Functions will always be reimported in full at the end of the migration
91         process, so the migration functions may leave a temporary state behind
92         there.
93     """
94     def decorator(func):
95         _MIGRATION_FUNCTIONS.append(((major, minor, patch, dbpatch), func))
96
97     return decorator
98
99
100 @_migration(3, 5, 0, 99)
101 def import_status_timestamp_change(conn, **_):
102     """ Add timezone to timestamp in status table.
103
104         The import_status table has been changed to include timezone information
105         with the time stamp.
106     """
107     with conn.cursor() as cur:
108         cur.execute("""ALTER TABLE import_status ALTER COLUMN lastimportdate
109                        TYPE timestamp with time zone;""")
110
111
112 @_migration(3, 5, 0, 99)
113 def add_nominatim_property_table(conn, config, **_):
114     """ Add nominatim_property table.
115     """
116     if not conn.table_exists('nominatim_properties'):
117         with conn.cursor() as cur:
118             cur.execute("""CREATE TABLE nominatim_properties (
119                                property TEXT,
120                                value TEXT);
121                            GRANT SELECT ON TABLE nominatim_properties TO "{}";
122                         """.format(config.DATABASE_WEBUSER))
123
124 @_migration(3, 6, 0, 0)
125 def change_housenumber_transliteration(conn, **_):
126     """ Transliterate housenumbers.
127
128         The database schema switched from saving raw housenumbers in
129         placex.housenumber to saving transliterated ones.
130     """
131     with conn.cursor() as cur:
132         cur.execute("""CREATE OR REPLACE FUNCTION create_housenumber_id(housenumber TEXT)
133                        RETURNS TEXT AS $$
134                        DECLARE
135                          normtext TEXT;
136                        BEGIN
137                          SELECT array_to_string(array_agg(trans), ';')
138                            INTO normtext
139                            FROM (SELECT lookup_word as trans, getorcreate_housenumber_id(lookup_word)
140                                  FROM (SELECT make_standard_name(h) as lookup_word
141                                        FROM regexp_split_to_table(housenumber, '[,;]') h) x) y;
142                          return normtext;
143                        END;
144                        $$ LANGUAGE plpgsql STABLE STRICT;""")
145         cur.execute("DELETE FROM word WHERE class = 'place' and type = 'house'")
146         cur.execute("""UPDATE placex
147                        SET housenumber = create_housenumber_id(housenumber)
148                        WHERE housenumber is not null""")
149
150
151 @_migration(3, 7, 0, 0)
152 def switch_placenode_geometry_index(conn, **_):
153     """ Replace idx_placex_geometry_reverse_placeNode index.
154
155         Make the index slightly more permissive, so that it can also be used
156         when matching up boundaries and place nodes. It makes the index
157         idx_placex_adminname index unnecessary.
158     """
159     with conn.cursor() as cur:
160         cur.execute(""" CREATE INDEX IF NOT EXISTS idx_placex_geometry_placenode ON placex
161                         USING GIST (geometry)
162                         WHERE osm_type = 'N' and rank_search < 26
163                               and class = 'place' and type != 'postcode'
164                               and linked_place_id is null""")
165         cur.execute(""" DROP INDEX IF EXISTS idx_placex_adminname """)
166
167
168 @_migration(3, 7, 0, 1)
169 def install_legacy_tokenizer(conn, config, **_):
170     """ Setup legacy tokenizer.
171
172         If no other tokenizer has been configured yet, then create the
173         configuration for the backwards-compatible legacy tokenizer
174     """
175     if properties.get_property(conn, 'tokenizer') is None:
176         tokenizer = tokenizer_factory.create_tokenizer(config, init_db=False,
177                                                        module_name='legacy')
178
179         tokenizer.migrate_database(config)