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 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_core.config import Configuration
15 from nominatim_core.db.connection import connect, Connection
16 from nominatim_core.db import properties
17 from nominatim_core.errors import UsageError
18 from ..tokenizer import factory as tokenizer_factory
20 from ..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 executed 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 if conn.table_exists('wikimedia_importance'):
252 cnt = cur.scalar('SELECT count(*) FROM wikimedia_importance')
254 cnt = cur.scalar('SELECT count(*) FROM wikipedia_article')
256 return CheckState.WARN if cnt == 0 else CheckState.OK
260 The indexing didn't finish. {count} entries are not yet indexed.
262 To index the remaining entries, run: {index_cmd}
264 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
265 """ Checking indexing status
267 with conn.cursor() as cur:
268 cnt = cur.scalar('SELECT count(*) FROM placex WHERE indexed_status > 0')
273 if freeze.is_frozen(conn):
275 Database is marked frozen, it cannot be updated.
276 Low counts of unindexed places are fine."""
277 return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
279 if conn.index_exists('idx_placex_rank_search'):
280 # Likely just an interrupted update.
281 index_cmd = 'nominatim index'
283 # Looks like the import process got interrupted.
284 index_cmd = 'nominatim import --continue indexing'
286 return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
290 The following indexes are missing:
293 Rerun the index creation with: nominatim import --continue db-postprocess
295 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
296 """ Checking that database indexes are complete
299 for index in _get_indexes(conn):
300 if not conn.index_exists(index):
301 missing.append(index)
304 return CheckState.FAIL, dict(indexes='\n '.join(missing))
310 At least one index is invalid. That can happen, e.g. when index creation was
311 disrupted and later restarted. You should delete the affected indices
317 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
318 """ Checking that all database indexes are valid
320 with conn.cursor() as cur:
321 cur.execute(""" SELECT relname FROM pg_class, pg_index
322 WHERE pg_index.indisvalid = false
323 AND pg_index.indexrelid = pg_class.oid""")
325 broken = [c[0] for c in cur]
328 return CheckState.FAIL, dict(indexes='\n '.join(broken))
335 Run TIGER import again: nominatim add-data --tiger-data <DIR>
337 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
338 """ Checking TIGER external data table.
340 if not config.get_bool('USE_US_TIGER_DATA'):
341 return CheckState.NOT_APPLICABLE
343 if not conn.table_exists('location_property_tiger'):
344 return CheckState.FAIL, dict(error='TIGER data table not found.')
346 with conn.cursor() as cur:
347 if cur.scalar('SELECT count(*) FROM location_property_tiger') == 0:
348 return CheckState.FAIL, dict(error='TIGER data table is empty.')