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.')