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 ..config import Configuration
15 from ..db.connection import connect, Connection, server_version_tuple,\
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
25 class CheckState(Enum):
26 """ Possible states of a check. FATAL stops check execution entirely.
34 CheckResult = Union[CheckState, Tuple[CheckState, Mapping[str, Any]]]
35 CheckFunc = Callable[[Connection, Configuration], CheckResult]
37 def _check(hint: Optional[str] = None) -> Callable[[CheckFunc], CheckFunc]:
38 """ Decorator for checks. It adds the function to the list of
39 checks to execute and adds the code for printing progress messages.
41 def decorator(func: CheckFunc) -> CheckFunc:
42 title = (func.__doc__ or '').split('\n', 1)[0].strip()
44 def run_check(conn: Connection, config: Configuration) -> CheckState:
45 print(title, end=' ... ')
46 ret = func(conn, config)
47 if isinstance(ret, tuple):
51 if ret == CheckState.OK:
52 print('\033[92mOK\033[0m')
53 elif ret == CheckState.WARN:
54 print('\033[93mWARNING\033[0m')
57 print(dedent(hint.format(**params)))
58 elif ret == CheckState.NOT_APPLICABLE:
59 print('not applicable')
61 print('\x1B[31mFailed\033[0m')
63 print(dedent(hint.format(**params)))
66 CHECKLIST.append(run_check)
73 def __init__(self, msg: str) -> None:
76 def close(self) -> None:
77 """ Dummy function to provide the implementation.
80 def check_database(config: Configuration) -> int:
81 """ Run a number of checks on the database and return the status.
84 conn = connect(config.get_libpq_dsn())
85 except UsageError as err:
86 conn = _BadConnection(str(err)) # type: ignore[assignment]
89 for check in CHECKLIST:
90 ret = check(conn, config)
91 if ret == CheckState.FATAL:
94 if ret in (CheckState.FATAL, CheckState.FAIL):
101 def _get_indexes(conn: Connection) -> List[str]:
102 indexes = ['idx_place_addressline_address_place_id',
103 'idx_placex_rank_search',
104 'idx_placex_rank_address',
105 'idx_placex_parent_place_id',
106 'idx_placex_geometry_reverse_lookuppolygon',
107 'idx_placex_geometry_placenode',
108 'idx_osmline_parent_place_id',
109 'idx_osmline_parent_osm_id',
111 'idx_postcode_postcode'
113 if table_exists(conn, 'search_name'):
114 indexes.extend(('idx_search_name_nameaddress_vector',
115 'idx_search_name_name_vector',
116 'idx_search_name_centroid'))
117 if server_version_tuple(conn) >= (11, 0, 0):
118 indexes.extend(('idx_placex_housenumber',
119 'idx_osmline_parent_osm_id_with_hnr'))
120 if table_exists(conn, 'place'):
121 indexes.extend(('idx_location_area_country_place_id',
122 'idx_place_osm_unique',
123 'idx_placex_rank_address_sector',
124 'idx_placex_rank_boundaries_sector'))
131 # Functions are executed in the order they appear here.
137 * Is the database server started?
138 * Check the NOMINATIM_DATABASE_DSN variable in your local .env
139 * Try connecting to the database with the same settings
141 Project directory: {config.project_dir}
142 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
144 def check_connection(conn: Any, config: Configuration) -> CheckResult:
145 """ Checking database connection
147 if isinstance(conn, _BadConnection):
148 return CheckState.FATAL, dict(error=conn.msg, config=config)
153 Database version ({db_version}) doesn't match Nominatim version ({nom_version})
156 * Are you connecting to the correct database?
160 Check the Migration chapter of the Administration Guide.
162 Project directory: {config.project_dir}
163 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
165 def check_database_version(conn: Connection, config: Configuration) -> CheckResult:
166 """ Checking database_version matches Nominatim software version
169 if table_exists(conn, 'nominatim_properties'):
170 db_version_str = properties.get_property(conn, 'database_version')
172 db_version_str = None
174 if db_version_str is not None:
175 db_version = parse_version(db_version_str)
177 if db_version == NOMINATIM_VERSION:
181 'Run migrations: nominatim admin --migrate'
182 if db_version < NOMINATIM_VERSION
183 else 'You need to upgrade the Nominatim software.'
188 return CheckState.FATAL, dict(db_version=db_version_str,
189 nom_version=NOMINATIM_VERSION,
190 instruction=instruction,
194 placex table not found
197 * Are you connecting to the correct database?
198 * Did the import process finish without errors?
200 Project directory: {config.project_dir}
201 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
203 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
204 """ Checking for placex table
206 if table_exists(conn, 'placex'):
209 return CheckState.FATAL, dict(config=config)
212 @_check(hint="""placex table has no data. Did the import finish successfully?""")
213 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
214 """ Checking for placex content
216 cnt = execute_scalar(conn, '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 table_exists(conn, 'search_name') or not table_exists(conn, 'place'):
248 return CheckState.NOT_APPLICABLE
250 if table_exists(conn, 'wikimedia_importance'):
251 cnt = execute_scalar(conn, 'SELECT count(*) FROM wikimedia_importance')
253 cnt = execute_scalar(conn, 'SELECT count(*) FROM wikipedia_article')
255 return CheckState.WARN if cnt == 0 else CheckState.OK
259 The indexing didn't finish. {count} entries are not yet indexed.
261 To index the remaining entries, run: {index_cmd}
263 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
264 """ Checking indexing status
266 cnt = execute_scalar(conn, 'SELECT count(*) FROM placex WHERE indexed_status > 0')
271 if freeze.is_frozen(conn):
273 Database is marked frozen, it cannot be updated.
274 Low counts of unindexed places are fine."""
275 return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
277 if index_exists(conn, 'idx_placex_rank_search'):
278 # Likely just an interrupted update.
279 index_cmd = 'nominatim index'
281 # Looks like the import process got interrupted.
282 index_cmd = 'nominatim import --continue indexing'
284 return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
288 The following indexes are missing:
291 Rerun the index creation with: nominatim import --continue db-postprocess
293 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
294 """ Checking that database indexes are complete
297 for index in _get_indexes(conn):
298 if not index_exists(conn, index):
299 missing.append(index)
302 return CheckState.FAIL, dict(indexes='\n '.join(missing))
308 At least one index is invalid. That can happen, e.g. when index creation was
309 disrupted and later restarted. You should delete the affected indices
315 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
316 """ Checking that all database indexes are valid
318 with conn.cursor() as cur:
319 cur.execute(""" SELECT relname FROM pg_class, pg_index
320 WHERE pg_index.indisvalid = false
321 AND pg_index.indexrelid = pg_class.oid""")
323 broken = [c[0] for c in cur]
326 return CheckState.FAIL, dict(indexes='\n '.join(broken))
333 Run TIGER import again: nominatim add-data --tiger-data <DIR>
335 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
336 """ Checking TIGER external data table.
338 if not config.get_bool('USE_US_TIGER_DATA'):
339 return CheckState.NOT_APPLICABLE
341 if not table_exists(conn, 'location_property_tiger'):
342 return CheckState.FAIL, dict(error='TIGER data table not found.')
344 if execute_scalar(conn, 'SELECT count(*) FROM location_property_tiger') == 0:
345 return CheckState.FAIL, dict(error='TIGER data table is empty.')