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.errors import UsageError
 
  17 from nominatim.tokenizer import factory as tokenizer_factory
 
  18 from nominatim.tools import freeze
 
  22 class CheckState(Enum):
 
  23     """ Possible states of a check. FATAL stops check execution entirely.
 
  31 CheckResult = Union[CheckState, Tuple[CheckState, Mapping[str, Any]]]
 
  32 CheckFunc = Callable[[Connection, Configuration], CheckResult]
 
  34 def _check(hint: Optional[str] = None) -> Callable[[CheckFunc], CheckFunc]:
 
  35     """ Decorator for checks. It adds the function to the list of
 
  36         checks to execute and adds the code for printing progress messages.
 
  38     def decorator(func: CheckFunc) -> CheckFunc:
 
  39         title = (func.__doc__ or '').split('\n', 1)[0].strip()
 
  41         def run_check(conn: Connection, config: Configuration) -> CheckState:
 
  42             print(title, end=' ... ')
 
  43             ret = func(conn, config)
 
  44             if isinstance(ret, tuple):
 
  48             if ret == CheckState.OK:
 
  49                 print('\033[92mOK\033[0m')
 
  50             elif ret == CheckState.WARN:
 
  51                 print('\033[93mWARNING\033[0m')
 
  54                     print(dedent(hint.format(**params)))
 
  55             elif ret == CheckState.NOT_APPLICABLE:
 
  56                 print('not applicable')
 
  58                 print('\x1B[31mFailed\033[0m')
 
  60                     print(dedent(hint.format(**params)))
 
  63         CHECKLIST.append(run_check)
 
  70     def __init__(self, msg: str) -> None:
 
  73     def close(self) -> None:
 
  74         """ Dummy function to provide the implementation.
 
  77 def check_database(config: Configuration) -> int:
 
  78     """ Run a number of checks on the database and return the status.
 
  81         conn = connect(config.get_libpq_dsn()).connection
 
  82     except UsageError as err:
 
  83         conn = _BadConnection(str(err)) # type: ignore[assignment]
 
  86     for check in CHECKLIST:
 
  87         ret = check(conn, config)
 
  88         if ret == CheckState.FATAL:
 
  91         if ret in (CheckState.FATAL, CheckState.FAIL):
 
  98 def _get_indexes(conn: Connection) -> List[str]:
 
  99     indexes = ['idx_place_addressline_address_place_id',
 
 100                'idx_placex_rank_search',
 
 101                'idx_placex_rank_address',
 
 102                'idx_placex_parent_place_id',
 
 103                'idx_placex_geometry_reverse_lookuppolygon',
 
 104                'idx_placex_geometry_placenode',
 
 105                'idx_osmline_parent_place_id',
 
 106                'idx_osmline_parent_osm_id',
 
 108                'idx_postcode_postcode'
 
 110     if conn.table_exists('search_name'):
 
 111         indexes.extend(('idx_search_name_nameaddress_vector',
 
 112                         'idx_search_name_name_vector',
 
 113                         'idx_search_name_centroid'))
 
 114         if conn.server_version_tuple() >= (11, 0, 0):
 
 115             indexes.extend(('idx_placex_housenumber',
 
 116                             'idx_osmline_parent_osm_id_with_hnr'))
 
 117     if conn.table_exists('place'):
 
 118         indexes.extend(('idx_location_area_country_place_id',
 
 119                         'idx_place_osm_unique',
 
 120                         'idx_placex_rank_address_sector',
 
 121                         'idx_placex_rank_boundaries_sector'))
 
 128 # Functions are exectured in the order they appear here.
 
 134              * Is the database server started?
 
 135              * Check the NOMINATIM_DATABASE_DSN variable in your local .env
 
 136              * Try connecting to the database with the same settings
 
 138              Project directory: {config.project_dir}
 
 139              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 141 def check_connection(conn: Any, config: Configuration) -> CheckResult:
 
 142     """ Checking database connection
 
 144     if isinstance(conn, _BadConnection):
 
 145         return CheckState.FATAL, dict(error=conn.msg, config=config)
 
 150              placex table not found
 
 153              * Are you connecting to the right database?
 
 154              * Did the import process finish without errors?
 
 156              Project directory: {config.project_dir}
 
 157              Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
 
 159 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
 
 160     """ Checking for placex table
 
 162     if conn.table_exists('placex'):
 
 165     return CheckState.FATAL, dict(config=config)
 
 168 @_check(hint="""placex table has no data. Did the import finish successfully?""")
 
 169 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
 
 170     """ Checking for placex content
 
 172     with conn.cursor() as cur:
 
 173         cnt = cur.scalar('SELECT count(*) FROM (SELECT * FROM placex LIMIT 100) x')
 
 175     return CheckState.OK if cnt > 0 else CheckState.FATAL
 
 178 @_check(hint="""{msg}""")
 
 179 def check_tokenizer(_: Connection, config: Configuration) -> CheckResult:
 
 180     """ Checking that tokenizer works
 
 183         tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
 
 185         return CheckState.FAIL, dict(msg="""\
 
 186             Cannot load tokenizer. Did the import finish successfully?""")
 
 188     result = tokenizer.check_database(config)
 
 193     return CheckState.FAIL, dict(msg=result)
 
 197              Wikipedia/Wikidata importance tables missing.
 
 198              Quality of search results may be degraded. Reverse geocoding is unaffected.
 
 199              See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
 
 201 def check_existance_wikipedia(conn: Connection, _: Configuration) -> CheckResult:
 
 202     """ Checking for wikipedia/wikidata data
 
 204     if not conn.table_exists('search_name') or not conn.table_exists('place'):
 
 205         return CheckState.NOT_APPLICABLE
 
 207     with conn.cursor() as cur:
 
 208         cnt = cur.scalar('SELECT count(*) FROM wikipedia_article')
 
 210         return CheckState.WARN if cnt == 0 else CheckState.OK
 
 214              The indexing didn't finish. {count} entries are not yet indexed.
 
 216              To index the remaining entries, run:   {index_cmd}
 
 218 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
 
 219     """ Checking indexing status
 
 221     with conn.cursor() as cur:
 
 222         cnt = cur.scalar('SELECT count(*) FROM placex WHERE indexed_status > 0')
 
 227     if freeze.is_frozen(conn):
 
 229             Database is marked frozen, it cannot be updated.
 
 230             Low counts of unindexed places are fine."""
 
 231         return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
 
 233     if conn.index_exists('idx_placex_rank_search'):
 
 234         # Likely just an interrupted update.
 
 235         index_cmd = 'nominatim index'
 
 237         # Looks like the import process got interrupted.
 
 238         index_cmd = 'nominatim import --continue indexing'
 
 240     return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
 
 244              The following indexes are missing:
 
 247              Rerun the index creation with:   nominatim import --continue db-postprocess
 
 249 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
 
 250     """ Checking that database indexes are complete
 
 253     for index in _get_indexes(conn):
 
 254         if not conn.index_exists(index):
 
 255             missing.append(index)
 
 258         return CheckState.FAIL, dict(indexes='\n  '.join(missing))
 
 264              At least one index is invalid. That can happen, e.g. when index creation was
 
 265              disrupted and later restarted. You should delete the affected indices
 
 271 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
 
 272     """ Checking that all database indexes are valid
 
 274     with conn.cursor() as cur:
 
 275         cur.execute(""" SELECT relname FROM pg_class, pg_index
 
 276                         WHERE pg_index.indisvalid = false
 
 277                         AND pg_index.indexrelid = pg_class.oid""")
 
 279         broken = [c[0] for c in cur]
 
 282         return CheckState.FAIL, dict(indexes='\n  '.join(broken))
 
 289              Run TIGER import again:   nominatim add-data --tiger-data <DIR>
 
 291 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
 
 292     """ Checking TIGER external data table.
 
 294     if not config.get_bool('USE_US_TIGER_DATA'):
 
 295         return CheckState.NOT_APPLICABLE
 
 297     if not conn.table_exists('location_property_tiger'):
 
 298         return CheckState.FAIL, dict(error='TIGER data table not found.')
 
 300     with conn.cursor() as cur:
 
 301         if cur.scalar('SELECT count(*) FROM location_property_tiger') == 0:
 
 302             return CheckState.FAIL, dict(error='TIGER data table is empty.')