1 # SPDX-License-Identifier: GPL-2.0-only
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2022 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Collection of functions that check if the database is complete and functional.
 
  10 from typing import Callable, Optional, Any, Union, Tuple, Mapping, List
 
  12 from textwrap import dedent
 
  14 from nominatim.config import Configuration
 
  15 from nominatim.db.connection import connect, Connection
 
  16 from nominatim.db import properties
 
  17 from nominatim.errors import UsageError
 
  18 from nominatim.tokenizer import factory as tokenizer_factory
 
  19 from nominatim.tools import freeze
 
  20 from nominatim.version import NOMINATIM_VERSION, parse_version
 
  24 class CheckState(Enum):
 
  25     """ Possible states of a check. FATAL stops check execution entirely.
 
  33 CheckResult = Union[CheckState, Tuple[CheckState, Mapping[str, Any]]]
 
  34 CheckFunc = Callable[[Connection, Configuration], CheckResult]
 
  36 def _check(hint: Optional[str] = None) -> Callable[[CheckFunc], CheckFunc]:
 
  37     """ Decorator for checks. It adds the function to the list of
 
  38         checks to execute and adds the code for printing progress messages.
 
  40     def decorator(func: CheckFunc) -> CheckFunc:
 
  41         title = (func.__doc__ or '').split('\n', 1)[0].strip()
 
  43         def run_check(conn: Connection, config: Configuration) -> CheckState:
 
  44             print(title, end=' ... ')
 
  45             ret = func(conn, config)
 
  46             if isinstance(ret, tuple):
 
  50             if ret == CheckState.OK:
 
  51                 print('\033[92mOK\033[0m')
 
  52             elif ret == CheckState.WARN:
 
  53                 print('\033[93mWARNING\033[0m')
 
  56                     print(dedent(hint.format(**params)))
 
  57             elif ret == CheckState.NOT_APPLICABLE:
 
  58                 print('not applicable')
 
  60                 print('\x1B[31mFailed\033[0m')
 
  62                     print(dedent(hint.format(**params)))
 
  65         CHECKLIST.append(run_check)
 
  72     def __init__(self, msg: str) -> None:
 
  75     def close(self) -> None:
 
  76         """ Dummy function to provide the implementation.
 
  79 def check_database(config: Configuration) -> int:
 
  80     """ Run a number of checks on the database and return the status.
 
  83         conn = connect(config.get_libpq_dsn()).connection
 
  84     except UsageError as err:
 
  85         conn = _BadConnection(str(err)) # type: ignore[assignment]
 
  88     for check in CHECKLIST:
 
  89         ret = check(conn, config)
 
  90         if ret == CheckState.FATAL:
 
  93         if ret in (CheckState.FATAL, CheckState.FAIL):
 
 100 def _get_indexes(conn: Connection) -> List[str]:
 
 101     indexes = ['idx_place_addressline_address_place_id',
 
 102                'idx_placex_rank_search',
 
 103                'idx_placex_rank_address',
 
 104                'idx_placex_parent_place_id',
 
 105                'idx_placex_geometry_reverse_lookuppolygon',
 
 106                'idx_placex_geometry_placenode',
 
 107                'idx_osmline_parent_place_id',
 
 108                'idx_osmline_parent_osm_id',
 
 110                'idx_postcode_postcode'
 
 112     if conn.table_exists('search_name'):
 
 113         indexes.extend(('idx_search_name_nameaddress_vector',
 
 114                         'idx_search_name_name_vector',
 
 115                         'idx_search_name_centroid'))
 
 116         if conn.server_version_tuple() >= (11, 0, 0):
 
 117             indexes.extend(('idx_placex_housenumber',
 
 118                             'idx_osmline_parent_osm_id_with_hnr'))
 
 119     if conn.table_exists('place'):
 
 120         indexes.extend(('idx_location_area_country_place_id',
 
 121                         'idx_place_osm_unique',
 
 122                         'idx_placex_rank_address_sector',
 
 123                         'idx_placex_rank_boundaries_sector'))
 
 130 # Functions are exectured in the order they appear here.
 
 136              * Is the database server started?
 
 137              * Check the NOMINATIM_DATABASE_DSN variable in your local .env
 
 138              * Try connecting to the database with the same settings
 
 140              Project directory: {config.project_dir}
 
 141              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 143 def check_connection(conn: Any, config: Configuration) -> CheckResult:
 
 144     """ Checking database connection
 
 146     if isinstance(conn, _BadConnection):
 
 147         return CheckState.FATAL, dict(error=conn.msg, config=config)
 
 152              Database version ({db_version}) doesn't match Nominatim version ({nom_version})
 
 155              * Are you connecting to the correct database?
 
 159              Check the Migration chapter of the Administration Guide.
 
 161              Project directory: {config.project_dir}
 
 162              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 164 def check_database_version(conn: Connection, config: Configuration) -> CheckResult:
 
 165     """ Checking database_version matches Nominatim software version
 
 168     if conn.table_exists('nominatim_properties'):
 
 169         db_version_str = properties.get_property(conn, 'database_version')
 
 171         db_version_str = None
 
 173     if db_version_str is not None:
 
 174         db_version = parse_version(db_version_str)
 
 176         if db_version == NOMINATIM_VERSION:
 
 180             'Run migrations: nominatim admin --migrate'
 
 181             if db_version < NOMINATIM_VERSION
 
 182             else 'You need to upgrade the Nominatim software.'
 
 187     return CheckState.FATAL, dict(db_version=db_version_str,
 
 188                                   nom_version=NOMINATIM_VERSION,
 
 189                                   instruction=instruction,
 
 193              placex table not found
 
 196              * Are you connecting to the correct database?
 
 197              * Did the import process finish without errors?
 
 199              Project directory: {config.project_dir}
 
 200              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 202 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
 
 203     """ Checking for placex table
 
 205     if conn.table_exists('placex'):
 
 208     return CheckState.FATAL, dict(config=config)
 
 211 @_check(hint="""placex table has no data. Did the import finish successfully?""")
 
 212 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
 
 213     """ Checking for placex content
 
 215     with conn.cursor() as cur:
 
 216         cnt = cur.scalar('SELECT count(*) FROM (SELECT * FROM placex LIMIT 100) x')
 
 218     return CheckState.OK if cnt > 0 else CheckState.FATAL
 
 221 @_check(hint="""{msg}""")
 
 222 def check_tokenizer(_: Connection, config: Configuration) -> CheckResult:
 
 223     """ Checking that tokenizer works
 
 226         tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
 
 228         return CheckState.FAIL, dict(msg="""\
 
 229             Cannot load tokenizer. Did the import finish successfully?""")
 
 231     result = tokenizer.check_database(config)
 
 236     return CheckState.FAIL, dict(msg=result)
 
 240              Wikipedia/Wikidata importance tables missing.
 
 241              Quality of search results may be degraded. Reverse geocoding is unaffected.
 
 242              See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
 
 244 def check_existance_wikipedia(conn: Connection, _: Configuration) -> CheckResult:
 
 245     """ Checking for wikipedia/wikidata data
 
 247     if not conn.table_exists('search_name') or not conn.table_exists('place'):
 
 248         return CheckState.NOT_APPLICABLE
 
 250     with conn.cursor() as cur:
 
 251         cnt = cur.scalar('SELECT count(*) FROM wikipedia_article')
 
 253         return CheckState.WARN if cnt == 0 else CheckState.OK
 
 257              The indexing didn't finish. {count} entries are not yet indexed.
 
 259              To index the remaining entries, run:   {index_cmd}
 
 261 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
 
 262     """ Checking indexing status
 
 264     with conn.cursor() as cur:
 
 265         cnt = cur.scalar('SELECT count(*) FROM placex WHERE indexed_status > 0')
 
 270     if freeze.is_frozen(conn):
 
 272             Database is marked frozen, it cannot be updated.
 
 273             Low counts of unindexed places are fine."""
 
 274         return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
 
 276     if conn.index_exists('idx_placex_rank_search'):
 
 277         # Likely just an interrupted update.
 
 278         index_cmd = 'nominatim index'
 
 280         # Looks like the import process got interrupted.
 
 281         index_cmd = 'nominatim import --continue indexing'
 
 283     return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
 
 287              The following indexes are missing:
 
 290              Rerun the index creation with:   nominatim import --continue db-postprocess
 
 292 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
 
 293     """ Checking that database indexes are complete
 
 296     for index in _get_indexes(conn):
 
 297         if not conn.index_exists(index):
 
 298             missing.append(index)
 
 301         return CheckState.FAIL, dict(indexes='\n  '.join(missing))
 
 307              At least one index is invalid. That can happen, e.g. when index creation was
 
 308              disrupted and later restarted. You should delete the affected indices
 
 314 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
 
 315     """ Checking that all database indexes are valid
 
 317     with conn.cursor() as cur:
 
 318         cur.execute(""" SELECT relname FROM pg_class, pg_index
 
 319                         WHERE pg_index.indisvalid = false
 
 320                         AND pg_index.indexrelid = pg_class.oid""")
 
 322         broken = [c[0] for c in cur]
 
 325         return CheckState.FAIL, dict(indexes='\n  '.join(broken))
 
 332              Run TIGER import again:   nominatim add-data --tiger-data <DIR>
 
 334 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
 
 335     """ Checking TIGER external data table.
 
 337     if not config.get_bool('USE_US_TIGER_DATA'):
 
 338         return CheckState.NOT_APPLICABLE
 
 340     if not conn.table_exists('location_property_tiger'):
 
 341         return CheckState.FAIL, dict(error='TIGER data table not found.')
 
 343     with conn.cursor() as cur:
 
 344         if cur.scalar('SELECT count(*) FROM location_property_tiger') == 0:
 
 345             return CheckState.FAIL, dict(error='TIGER data table is empty.')