1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2025 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 ..config import Configuration
 
  15 from ..db.connection import connect, Connection, \
 
  16                             index_exists, table_exists, execute_scalar
 
  17 from ..db import properties
 
  18 from ..errors import UsageError
 
  19 from ..tokenizer import factory as tokenizer_factory
 
  21 from ..version import NOMINATIM_VERSION, parse_version
 
  26 class CheckState(Enum):
 
  27     """ Possible states of a check. FATAL stops check execution entirely.
 
  36 CheckResult = Union[CheckState, Tuple[CheckState, Mapping[str, Any]]]
 
  37 CheckFunc = Callable[[Connection, Configuration], CheckResult]
 
  40 def _check(hint: Optional[str] = None) -> Callable[[CheckFunc], CheckFunc]:
 
  41     """ Decorator for checks. It adds the function to the list of
 
  42         checks to execute and adds the code for printing progress messages.
 
  44     def decorator(func: CheckFunc) -> CheckFunc:
 
  45         title = (func.__doc__ or '').split('\n', 1)[0].strip()
 
  47         def run_check(conn: Connection, config: Configuration) -> CheckState:
 
  48             print(title, end=' ... ')
 
  49             ret = func(conn, config)
 
  50             if isinstance(ret, tuple):
 
  54             if ret == CheckState.OK:
 
  55                 print('\033[92mOK\033[0m')
 
  56             elif ret == CheckState.WARN:
 
  57                 print('\033[93mWARNING\033[0m')
 
  60                     print(dedent(hint.format(**params)))
 
  61             elif ret == CheckState.NOT_APPLICABLE:
 
  62                 print('not applicable')
 
  64                 print('\x1B[31mFailed\033[0m')
 
  66                     print(dedent(hint.format(**params)))
 
  69         CHECKLIST.append(run_check)
 
  77     def __init__(self, msg: str) -> None:
 
  80     def close(self) -> None:
 
  81         """ Dummy function to provide the implementation.
 
  85 def check_database(config: Configuration) -> int:
 
  86     """ Run a number of checks on the database and return the status.
 
  89         conn = connect(config.get_libpq_dsn())
 
  90     except UsageError as err:
 
  91         conn = _BadConnection(str(err))  # type: ignore[assignment]
 
  94     for check in CHECKLIST:
 
  95         ret = check(conn, config)
 
  96         if ret == CheckState.FATAL:
 
  99         if ret in (CheckState.FATAL, CheckState.FAIL):
 
 103     return overall_result
 
 106 def _get_indexes(conn: Connection) -> List[str]:
 
 107     indexes = ['idx_place_addressline_address_place_id',
 
 108                'idx_placex_rank_search',
 
 109                'idx_placex_rank_address',
 
 110                'idx_placex_parent_place_id',
 
 111                'idx_placex_geometry_reverse_lookupplacenode',
 
 112                'idx_placex_geometry_reverse_lookuppolygon',
 
 113                'idx_placex_geometry_placenode',
 
 114                'idx_osmline_parent_place_id',
 
 115                'idx_osmline_parent_osm_id',
 
 117                'idx_postcode_postcode'
 
 120     # These won't exist if --reverse-only import was used
 
 121     if table_exists(conn, 'search_name'):
 
 122         indexes.extend(('idx_search_name_nameaddress_vector',
 
 123                         'idx_search_name_name_vector',
 
 124                         'idx_search_name_centroid',
 
 125                         'idx_placex_housenumber',
 
 126                         'idx_osmline_parent_osm_id_with_hnr'))
 
 128     # These won't exist if --no-updates import was used
 
 129     if table_exists(conn, 'place'):
 
 130         indexes.extend(('idx_location_area_country_place_id',
 
 131                         'idx_place_osm_unique',
 
 132                         'idx_placex_rank_address_sector',
 
 133                         'idx_placex_rank_boundaries_sector'))
 
 140 # Functions are executed in the order they appear here.
 
 146              * Is the database server started?
 
 147              * Check the NOMINATIM_DATABASE_DSN variable in your local .env
 
 148              * Try connecting to the database with the same settings
 
 150              Project directory: {config.project_dir}
 
 151              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 153 def check_connection(conn: Any, config: Configuration) -> CheckResult:
 
 154     """ Checking database connection
 
 156     if isinstance(conn, _BadConnection):
 
 157         return CheckState.FATAL, dict(error=conn.msg, config=config)
 
 163              Database version ({db_version}) doesn't match Nominatim version ({nom_version})
 
 168              Project directory: {config.project_dir}
 
 169              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 171 def check_database_version(conn: Connection, config: Configuration) -> CheckResult:
 
 172     """ Checking database_version matches Nominatim software version
 
 175     db_version_str = None
 
 176     if not table_exists(conn, 'nominatim_properties'):
 
 177         instruction = 'Are you connecting to the correct database?'
 
 179         db_version_str = properties.get_property(conn, 'database_version')
 
 181         if db_version_str is None:
 
 182             instruction = 'Database version not found. Did the import finish?'
 
 184             db_version = parse_version(db_version_str)
 
 186             if db_version == NOMINATIM_VERSION:
 
 190                 "Run migrations: 'nominatim admin --migrate'"
 
 191                 if db_version < NOMINATIM_VERSION
 
 192                 else 'You need to upgrade the Nominatim software.'
 
 193             ) + ' Check the Migration chapter of the Administration Guide.'
 
 195     return CheckState.FATAL, dict(db_version=db_version_str,
 
 196                                   nom_version=NOMINATIM_VERSION,
 
 197                                   instruction=instruction,
 
 202              placex table not found
 
 205              * Are you connecting to the correct database?
 
 206              * Did the import process finish without errors?
 
 208              Project directory: {config.project_dir}
 
 209              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 211 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
 
 212     """ Checking for placex table
 
 214     if table_exists(conn, 'placex'):
 
 217     return CheckState.FATAL, dict(config=config)
 
 220 @_check(hint="""placex table has no data. Did the import finish successfully?""")
 
 221 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
 
 222     """ Checking for placex content
 
 224     cnt = execute_scalar(conn, 'SELECT count(*) FROM (SELECT * FROM placex LIMIT 100) x')
 
 226     return CheckState.OK if cnt > 0 else CheckState.FATAL
 
 229 @_check(hint="""{msg}""")
 
 230 def check_tokenizer(_: Connection, config: Configuration) -> CheckResult:
 
 231     """ Checking that tokenizer works
 
 234         tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
 
 236         return CheckState.FAIL, dict(msg="""\
 
 237             Cannot load tokenizer. Did the import finish successfully?""")
 
 239     result = tokenizer.check_database(config)
 
 244     return CheckState.FAIL, dict(msg=result)
 
 248              Wikipedia/Wikidata importance tables missing.
 
 249              Quality of search results may be degraded. Reverse geocoding is unaffected.
 
 250              See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
 
 252 def check_existance_wikipedia(conn: Connection, _: Configuration) -> CheckResult:
 
 253     """ Checking for wikipedia/wikidata data
 
 255     if not table_exists(conn, 'search_name') or not table_exists(conn, 'place'):
 
 256         return CheckState.NOT_APPLICABLE
 
 258     if table_exists(conn, 'wikimedia_importance'):
 
 259         cnt = execute_scalar(conn, 'SELECT count(*) FROM wikimedia_importance')
 
 261         cnt = execute_scalar(conn, 'SELECT count(*) FROM wikipedia_article')
 
 263     return CheckState.WARN if cnt == 0 else CheckState.OK
 
 267              The indexing didn't finish. {count} entries are not yet indexed.
 
 269              To index the remaining entries, run:   {index_cmd}
 
 271 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
 
 272     """ Checking indexing status
 
 274     cnt = execute_scalar(conn, 'SELECT count(*) FROM placex WHERE indexed_status > 0')
 
 279     if freeze.is_frozen(conn):
 
 281             Database is marked frozen, it cannot be updated.
 
 282             Low counts of unindexed places are fine."""
 
 283         return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
 
 285     if index_exists(conn, 'idx_placex_rank_search'):
 
 286         # Likely just an interrupted update.
 
 287         index_cmd = 'nominatim index'
 
 289         # Looks like the import process got interrupted.
 
 290         index_cmd = 'nominatim import --continue indexing'
 
 292     return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
 
 296              The following indexes are missing:
 
 299              Rerun the index creation with:   nominatim import --continue db-postprocess
 
 301 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
 
 302     """ Checking that database indexes are complete
 
 305     for index in _get_indexes(conn):
 
 306         if not index_exists(conn, index):
 
 307             missing.append(index)
 
 310         return CheckState.FAIL, dict(indexes='\n  '.join(missing))
 
 316              At least one index is invalid. That can happen, e.g. when index creation was
 
 317              disrupted and later restarted. You should delete the affected indices
 
 323 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
 
 324     """ Checking that all database indexes are valid
 
 326     with conn.cursor() as cur:
 
 327         cur.execute(""" SELECT relname FROM pg_class, pg_index
 
 328                         WHERE pg_index.indisvalid = false
 
 329                         AND pg_index.indexrelid = pg_class.oid""")
 
 331         broken = [c[0] for c in cur]
 
 334         return CheckState.FAIL, dict(indexes='\n  '.join(broken))
 
 341              Run TIGER import again:   nominatim add-data --tiger-data <DIR>
 
 343 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
 
 344     """ Checking TIGER external data table.
 
 346     if not config.get_bool('USE_US_TIGER_DATA'):
 
 347         return CheckState.NOT_APPLICABLE
 
 349     if not table_exists(conn, 'location_property_tiger'):
 
 350         return CheckState.FAIL, dict(error='TIGER data table not found.')
 
 352     if execute_scalar(conn, 'SELECT count(*) FROM location_property_tiger') == 0:
 
 353         return CheckState.FAIL, dict(error='TIGER data table is empty.')