1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2024 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Functions for database migration to newer software versions.
 
  10 from typing import List, Tuple, Callable, Any
 
  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
 
  23 LOG = logging.getLogger()
 
  25 _MIGRATION_FUNCTIONS: List[Tuple[NominatimVersion, Callable[..., None]]] = []
 
  28 def migrate(config: Configuration, paths: Any) -> int:
 
  29     """ Check for the current database version and execute migrations,
 
  32     with connect(config.get_libpq_dsn()) as conn:
 
  34         if table_exists(conn, 'nominatim_properties'):
 
  35             db_version_str = properties.get_property(conn, 'database_version')
 
  39         if db_version_str is not None:
 
  40             db_version = parse_version(db_version_str)
 
  44         if db_version is None or db_version < (4, 3, 0, 0):
 
  45             LOG.fatal('Your database version is older than 4.3. '
 
  46                       'Direct migration is not possible.\n'
 
  47                       'You should strongly consider a reimport. If that is not possible\n'
 
  48                       'please upgrade to 4.3 first and then to the newest version.')
 
  49             raise UsageError('Migration not possible.')
 
  51         if db_version == NOMINATIM_VERSION:
 
  52             LOG.warning("Database already at latest version (%s)", db_version_str)
 
  55         LOG.info("Detected database version: %s", db_version_str)
 
  57         for version, func in _MIGRATION_FUNCTIONS:
 
  58             if db_version < version:
 
  59                 title = func.__doc__ or ''
 
  60                 LOG.warning("Running: %s (%s)", title.split('\n', 1)[0], version)
 
  61                 kwargs = dict(conn=conn, config=config, paths=paths)
 
  65         LOG.warning('Updating SQL functions.')
 
  66         refresh.create_functions(conn, config)
 
  67         tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
 
  68         tokenizer.update_sql_functions(config)
 
  70         properties.set_property(conn, 'database_version', str(NOMINATIM_VERSION))
 
  77 def _migration(major: int, minor: int, patch: int = 0,
 
  78                dbpatch: int = 0) -> Callable[[Callable[..., None]], Callable[..., None]]:
 
  79     """ Decorator for a single migration step. The parameters describe the
 
  80         version after which the migration is applicable, i.e before changing
 
  81         from the given version to the next, the migration is required.
 
  83         All migrations are run in the order in which they are defined in this
 
  84         file. Do not run global SQL scripts for migrations as you cannot be sure
 
  85         that these scripts do the same in later versions.
 
  87         Functions will always be reimported in full at the end of the migration
 
  88         process, so the migration functions may leave a temporary state behind
 
  91     def decorator(func: Callable[..., None]) -> Callable[..., None]:
 
  92         version = NominatimVersion(major, minor, patch, dbpatch)
 
  93         _MIGRATION_FUNCTIONS.append((version, func))
 
  99 @_migration(4, 4, 99, 0)
 
 100 def create_postcode_area_lookup_index(conn: Connection, **_: Any) -> None:
 
 101     """ Create index needed for looking up postcode areas from postocde points.
 
 103     with conn.cursor() as cur:
 
 104         cur.execute("""CREATE INDEX IF NOT EXISTS idx_placex_postcode_areas
 
 105                        ON placex USING BTREE (country_code, postcode)
 
 106                        WHERE osm_type = 'R' AND class = 'boundary' AND type = 'postal_code'
 
 110 @_migration(4, 4, 99, 1)
 
 111 def create_postcode_parent_index(conn: Connection, **_: Any) -> None:
 
 112     """ Create index needed for updating postcodes when a parent changes.
 
 114     if table_exists(conn, 'planet_osm_ways'):
 
 115         with conn.cursor() as cur:
 
 116             cur.execute("""CREATE INDEX IF NOT EXISTS
 
 117                              idx_location_postcode_parent_place_id
 
 118                              ON location_postcode USING BTREE (parent_place_id)""")
 
 121 @_migration(5, 1, 99, 0)
 
 122 def create_placex_entrance_table(conn: Connection, config: Configuration, **_: Any) -> None:
 
 123     """ Add the placex_entrance table to store linked-up entrance nodes
 
 125     if not table_exists(conn, 'placex_entrance'):
 
 126         sqlp = SQLPreprocessor(conn, config)
 
 127         sqlp.run_string(conn, """
 
 128             -- Table to store location of entrance nodes
 
 129             CREATE TABLE placex_entrance (
 
 130               place_id BIGINT NOT NULL,
 
 131               osm_id BIGINT NOT NULL,
 
 133               location GEOMETRY(Point, 4326) NOT NULL,
 
 136             CREATE UNIQUE INDEX idx_placex_entrance_place_id_osm_id ON placex_entrance
 
 137               USING BTREE (place_id, osm_id) {{db.tablespace.search_index}};
 
 138             GRANT SELECT ON placex_entrance TO "{{config.DATABASE_WEBUSER}}" ;
 
 142 @_migration(5, 1, 99, 1)
 
 143 def create_place_entrance_table(conn: Connection, config: Configuration, **_: Any) -> None:
 
 144     """ Add the place_entrance table to store incomming entrance nodes
 
 146     if not table_exists(conn, 'place_entrance'):
 
 147         with conn.cursor() as cur:
 
 149             -- Table to store location of entrance nodes
 
 150             CREATE TABLE place_entrance (
 
 151               osm_id BIGINT NOT NULL,
 
 154               geometry GEOMETRY(Point, 4326) NOT NULL
 
 156             CREATE UNIQUE INDEX place_entrance_osm_id_idx ON place_entrance
 
 157               USING BTREE (osm_id);