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