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_lookupplacenode',
107 'idx_placex_geometry_reverse_lookuppolygon',
108 'idx_placex_geometry_placenode',
109 'idx_osmline_parent_place_id',
110 'idx_osmline_parent_osm_id',
112 'idx_postcode_postcode'
115 # These won't exist if --reverse-only import was used
116 if table_exists(conn, 'search_name'):
117 indexes.extend(('idx_search_name_nameaddress_vector',
118 'idx_search_name_name_vector',
119 'idx_search_name_centroid'))
120 if server_version_tuple(conn) >= (11, 0, 0):
121 indexes.extend(('idx_placex_housenumber',
122 'idx_osmline_parent_osm_id_with_hnr'))
124 # These won't exist if --no-updates import was used
125 if table_exists(conn, 'place'):
126 indexes.extend(('idx_location_area_country_place_id',
127 'idx_place_osm_unique',
128 'idx_placex_rank_address_sector',
129 'idx_placex_rank_boundaries_sector'))
136 # Functions are executed in the order they appear here.
142 * Is the database server started?
143 * Check the NOMINATIM_DATABASE_DSN variable in your local .env
144 * Try connecting to the database with the same settings
146 Project directory: {config.project_dir}
147 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
149 def check_connection(conn: Any, config: Configuration) -> CheckResult:
150 """ Checking database connection
152 if isinstance(conn, _BadConnection):
153 return CheckState.FATAL, dict(error=conn.msg, config=config)
158 Database version ({db_version}) doesn't match Nominatim version ({nom_version})
161 * Are you connecting to the correct database?
165 Check the Migration chapter of the Administration Guide.
167 Project directory: {config.project_dir}
168 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
170 def check_database_version(conn: Connection, config: Configuration) -> CheckResult:
171 """ Checking database_version matches Nominatim software version
174 if table_exists(conn, 'nominatim_properties'):
175 db_version_str = properties.get_property(conn, 'database_version')
177 db_version_str = None
179 if db_version_str is not None:
180 db_version = parse_version(db_version_str)
182 if db_version == NOMINATIM_VERSION:
186 'Run migrations: nominatim admin --migrate'
187 if db_version < NOMINATIM_VERSION
188 else 'You need to upgrade the Nominatim software.'
193 return CheckState.FATAL, dict(db_version=db_version_str,
194 nom_version=NOMINATIM_VERSION,
195 instruction=instruction,
199 placex table not found
202 * Are you connecting to the correct database?
203 * Did the import process finish without errors?
205 Project directory: {config.project_dir}
206 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
208 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
209 """ Checking for placex table
211 if table_exists(conn, 'placex'):
214 return CheckState.FATAL, dict(config=config)
217 @_check(hint="""placex table has no data. Did the import finish successfully?""")
218 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
219 """ Checking for placex content
221 cnt = execute_scalar(conn, 'SELECT count(*) FROM (SELECT * FROM placex LIMIT 100) x')
223 return CheckState.OK if cnt > 0 else CheckState.FATAL
226 @_check(hint="""{msg}""")
227 def check_tokenizer(_: Connection, config: Configuration) -> CheckResult:
228 """ Checking that tokenizer works
231 tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
233 return CheckState.FAIL, dict(msg="""\
234 Cannot load tokenizer. Did the import finish successfully?""")
236 result = tokenizer.check_database(config)
241 return CheckState.FAIL, dict(msg=result)
245 Wikipedia/Wikidata importance tables missing.
246 Quality of search results may be degraded. Reverse geocoding is unaffected.
247 See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
249 def check_existance_wikipedia(conn: Connection, _: Configuration) -> CheckResult:
250 """ Checking for wikipedia/wikidata data
252 if not table_exists(conn, 'search_name') or not table_exists(conn, 'place'):
253 return CheckState.NOT_APPLICABLE
255 if table_exists(conn, 'wikimedia_importance'):
256 cnt = execute_scalar(conn, 'SELECT count(*) FROM wikimedia_importance')
258 cnt = execute_scalar(conn, 'SELECT count(*) FROM wikipedia_article')
260 return CheckState.WARN if cnt == 0 else CheckState.OK
264 The indexing didn't finish. {count} entries are not yet indexed.
266 To index the remaining entries, run: {index_cmd}
268 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
269 """ Checking indexing status
271 cnt = execute_scalar(conn, 'SELECT count(*) FROM placex WHERE indexed_status > 0')
276 if freeze.is_frozen(conn):
278 Database is marked frozen, it cannot be updated.
279 Low counts of unindexed places are fine."""
280 return CheckState.WARN, dict(count=cnt, index_cmd=index_cmd)
282 if index_exists(conn, 'idx_placex_rank_search'):
283 # Likely just an interrupted update.
284 index_cmd = 'nominatim index'
286 # Looks like the import process got interrupted.
287 index_cmd = 'nominatim import --continue indexing'
289 return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
293 The following indexes are missing:
296 Rerun the index creation with: nominatim import --continue db-postprocess
298 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
299 """ Checking that database indexes are complete
302 for index in _get_indexes(conn):
303 if not index_exists(conn, index):
304 missing.append(index)
307 return CheckState.FAIL, dict(indexes='\n '.join(missing))
313 At least one index is invalid. That can happen, e.g. when index creation was
314 disrupted and later restarted. You should delete the affected indices
320 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
321 """ Checking that all database indexes are valid
323 with conn.cursor() as cur:
324 cur.execute(""" SELECT relname FROM pg_class, pg_index
325 WHERE pg_index.indisvalid = false
326 AND pg_index.indexrelid = pg_class.oid""")
328 broken = [c[0] for c in cur]
331 return CheckState.FAIL, dict(indexes='\n '.join(broken))
338 Run TIGER import again: nominatim add-data --tiger-data <DIR>
340 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
341 """ Checking TIGER external data table.
343 if not config.get_bool('USE_US_TIGER_DATA'):
344 return CheckState.NOT_APPLICABLE
346 if not table_exists(conn, 'location_property_tiger'):
347 return CheckState.FAIL, dict(error='TIGER data table not found.')
349 if execute_scalar(conn, 'SELECT count(*) FROM location_property_tiger') == 0:
350 return CheckState.FAIL, dict(error='TIGER data table is empty.')