]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/tools/migration.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / src / nominatim_db / tools / migration.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Functions for database migration to newer software versions.
9 """
10 from typing import List, Tuple, Callable, Any
11 import logging
12
13 from ..errors import UsageError
14 from ..config import Configuration
15 from ..db import properties
16 from ..db.connection import connect, Connection, \
17                             table_exists, register_hstore
18 from ..db.sql_preprocessor import SQLPreprocessor
19 from ..version import NominatimVersion, NOMINATIM_VERSION, parse_version
20 from ..tokenizer import factory as tokenizer_factory
21 from ..data.country_info import create_country_names, setup_country_config
22 from . import refresh
23
24 LOG = logging.getLogger()
25
26 _MIGRATION_FUNCTIONS: List[Tuple[NominatimVersion, Callable[..., None]]] = []
27
28
29 def migrate(config: Configuration, paths: Any) -> int:
30     """ Check for the current database version and execute migrations,
31         if necesssary.
32     """
33     with connect(config.get_libpq_dsn()) as conn:
34         register_hstore(conn)
35         if table_exists(conn, 'nominatim_properties'):
36             db_version_str = properties.get_property(conn, 'database_version')
37         else:
38             db_version_str = None
39
40         if db_version_str is not None:
41             db_version = parse_version(db_version_str)
42         else:
43             db_version = None
44
45         if db_version is None or db_version < (4, 3, 0, 0):
46             LOG.fatal('Your database version is older than 4.3. '
47                       'Direct migration is not possible.\n'
48                       'You should strongly consider a reimport. If that is not possible\n'
49                       'please upgrade to 4.3 first and then to the newest version.')
50             raise UsageError('Migration not possible.')
51
52         if db_version == NOMINATIM_VERSION:
53             LOG.warning("Database already at latest version (%s)", db_version_str)
54             return 0
55
56         LOG.info("Detected database version: %s", db_version_str)
57
58         for version, func in _MIGRATION_FUNCTIONS:
59             if db_version < version:
60                 title = func.__doc__ or ''
61                 LOG.warning("Running: %s (%s)", title.split('\n', 1)[0], version)
62                 kwargs = dict(conn=conn, config=config, paths=paths)
63                 func(**kwargs)
64                 conn.commit()
65
66         LOG.warning('Updating SQL functions.')
67         refresh.create_functions(conn, config)
68         tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
69         tokenizer.update_sql_functions(config)
70
71         properties.set_property(conn, 'database_version', str(NOMINATIM_VERSION))
72
73         conn.commit()
74
75     return 0
76
77
78 def _migration(major: int, minor: int, patch: int = 0,
79                dbpatch: int = 0) -> Callable[[Callable[..., None]], Callable[..., None]]:
80     """ Decorator for a single migration step. The parameters describe the
81         version after which the migration is applicable, i.e before changing
82         from the given version to the next, the migration is required.
83
84         All migrations are run in the order in which they are defined in this
85         file. Do not run global SQL scripts for migrations as you cannot be sure
86         that these scripts do the same in later versions.
87
88         Functions will always be reimported in full at the end of the migration
89         process, so the migration functions may leave a temporary state behind
90         there.
91     """
92     def decorator(func: Callable[..., None]) -> Callable[..., None]:
93         version = NominatimVersion(major, minor, patch, dbpatch)
94         _MIGRATION_FUNCTIONS.append((version, func))
95         return func
96
97     return decorator
98
99
100 @_migration(4, 4, 99, 0)
101 def create_postcode_area_lookup_index(conn: Connection, **_: Any) -> None:
102     """ Create index needed for looking up postcode areas from postocde points.
103     """
104     with conn.cursor() as cur:
105         cur.execute("""CREATE INDEX IF NOT EXISTS idx_placex_postcode_areas
106                        ON placex USING BTREE (country_code, postcode)
107                        WHERE osm_type = 'R' AND class = 'boundary' AND type = 'postal_code'
108                     """)
109
110
111 @_migration(4, 4, 99, 1)
112 def create_postcode_parent_index(conn: Connection, **_: Any) -> None:
113     """ Create index needed for updating postcodes when a parent changes.
114     """
115     if table_exists(conn, 'planet_osm_ways'):
116         with conn.cursor() as cur:
117             cur.execute("""CREATE INDEX IF NOT EXISTS
118                              idx_location_postcode_parent_place_id
119                              ON location_postcode USING BTREE (parent_place_id)""")
120
121
122 @_migration(5, 1, 99, 0)
123 def create_placex_entrance_table(conn: Connection, config: Configuration, **_: Any) -> None:
124     """ Add the placex_entrance table to store linked-up entrance nodes
125     """
126     if not table_exists(conn, 'placex_entrance'):
127         sqlp = SQLPreprocessor(conn, config)
128         sqlp.run_string(conn, """
129             -- Table to store location of entrance nodes
130             CREATE TABLE placex_entrance (
131               place_id BIGINT NOT NULL,
132               osm_id BIGINT NOT NULL,
133               type TEXT NOT NULL,
134               location GEOMETRY(Point, 4326) NOT NULL,
135               extratags HSTORE
136               );
137             CREATE UNIQUE INDEX idx_placex_entrance_place_id_osm_id ON placex_entrance
138               USING BTREE (place_id, osm_id) {{db.tablespace.search_index}};
139             GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}" ;
140               """)
141
142
143 @_migration(5, 1, 99, 1)
144 def create_place_entrance_table(conn: Connection, config: Configuration, **_: Any) -> None:
145     """ Add the place_entrance table to store incomming entrance nodes
146     """
147     if not table_exists(conn, 'place_entrance'):
148         with conn.cursor() as cur:
149             cur.execute("""
150             -- Table to store location of entrance nodes
151             CREATE TABLE place_entrance (
152               osm_id BIGINT NOT NULL,
153               type TEXT NOT NULL,
154               extratags HSTORE,
155               geometry GEOMETRY(Point, 4326) NOT NULL
156               );
157             CREATE UNIQUE INDEX place_entrance_osm_id_idx ON place_entrance
158               USING BTREE (osm_id);
159               """)
160
161
162 @_migration(5, 2, 99, 1)
163 def convert_country_tokens(conn: Connection, config: Configuration, **_: Any) -> None:
164     """ Convert country word tokens
165
166         Country tokens now save the country in the info field instead of the
167         word. This migration removes all country tokens from the word table
168         and reimports the default country name. This means that custom names
169         are lost. If you need them back, invalidate the OSM objects containing
170         the names by setting indexed_status to 2 and then reindex the database.
171     """
172     tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
173     # There is only one tokenizer at the time of migration, so we make
174     # some assumptions here about the structure of the database. This will
175     # fail if somebody has written a custom tokenizer.
176     with conn.cursor() as cur:
177         cur.execute("DELETE FROM word WHERE type = 'C'")
178     conn.commit()
179
180     setup_country_config(config)
181     create_country_names(conn, tokenizer, config.get_str_list('LANGUAGES'))