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
21 class CheckState(Enum):
22 """ Possible states of a check. FATAL stops check execution entirely.
30 CheckResult = Union[CheckState, Tuple[CheckState, Mapping[str, Any]]]
31 CheckFunc = Callable[[Connection, Configuration], CheckResult]
33 def _check(hint: Optional[str] = None) -> Callable[[CheckFunc], CheckFunc]:
34 """ Decorator for checks. It adds the function to the list of
35 checks to execute and adds the code for printing progress messages.
37 def decorator(func: CheckFunc) -> CheckFunc:
38 title = (func.__doc__ or '').split('\n', 1)[0].strip()
40 def run_check(conn: Connection, config: Configuration) -> CheckState:
41 print(title, end=' ... ')
42 ret = func(conn, config)
43 if isinstance(ret, tuple):
47 if ret == CheckState.OK:
48 print('\033[92mOK\033[0m')
49 elif ret == CheckState.WARN:
50 print('\033[93mWARNING\033[0m')
53 print(dedent(hint.format(**params)))
54 elif ret == CheckState.NOT_APPLICABLE:
55 print('not applicable')
57 print('\x1B[31mFailed\033[0m')
59 print(dedent(hint.format(**params)))
62 CHECKLIST.append(run_check)
69 def __init__(self, msg: str) -> None:
72 def close(self) -> None:
73 """ Dummy function to provide the implementation.
76 def check_database(config: Configuration) -> int:
77 """ Run a number of checks on the database and return the status.
80 conn = connect(config.get_libpq_dsn()).connection
81 except UsageError as err:
82 conn = _BadConnection(str(err)) # type: ignore[assignment]
85 for check in CHECKLIST:
86 ret = check(conn, config)
87 if ret == CheckState.FATAL:
90 if ret in (CheckState.FATAL, CheckState.FAIL):
97 def _get_indexes(conn: Connection) -> List[str]:
98 indexes = ['idx_place_addressline_address_place_id',
99 'idx_placex_rank_search',
100 'idx_placex_rank_address',
101 'idx_placex_parent_place_id',
102 'idx_placex_geometry_reverse_lookuppolygon',
103 'idx_placex_geometry_placenode',
104 'idx_osmline_parent_place_id',
105 'idx_osmline_parent_osm_id',
107 'idx_postcode_postcode'
109 if conn.table_exists('search_name'):
110 indexes.extend(('idx_search_name_nameaddress_vector',
111 'idx_search_name_name_vector',
112 'idx_search_name_centroid'))
113 if conn.server_version_tuple() >= (11, 0, 0):
114 indexes.extend(('idx_placex_housenumber',
115 'idx_osmline_parent_osm_id_with_hnr'))
116 if conn.table_exists('place'):
117 indexes.extend(('idx_location_area_country_place_id',
118 'idx_place_osm_unique',
119 'idx_placex_rank_address_sector',
120 'idx_placex_rank_boundaries_sector'))
127 # Functions are exectured in the order they appear here.
133 * Is the database server started?
134 * Check the NOMINATIM_DATABASE_DSN variable in your local .env
135 * Try connecting to the database with the same settings
137 Project directory: {config.project_dir}
138 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
140 def check_connection(conn: Any, config: Configuration) -> CheckResult:
141 """ Checking database connection
143 if isinstance(conn, _BadConnection):
144 return CheckState.FATAL, dict(error=conn.msg, config=config)
149 placex table not found
152 * Are you connecting to the right database?
153 * Did the import process finish without errors?
155 Project directory: {config.project_dir}
156 Current setting of NOMINATIM_DATABASE_DSN: {config.DATABASE_DSN}
158 def check_placex_table(conn: Connection, config: Configuration) -> CheckResult:
159 """ Checking for placex table
161 if conn.table_exists('placex'):
164 return CheckState.FATAL, dict(config=config)
167 @_check(hint="""placex table has no data. Did the import finish successfully?""")
168 def check_placex_size(conn: Connection, _: Configuration) -> CheckResult:
169 """ Checking for placex content
171 with conn.cursor() as cur:
172 cnt = cur.scalar('SELECT count(*) FROM (SELECT * FROM placex LIMIT 100) x')
174 return CheckState.OK if cnt > 0 else CheckState.FATAL
177 @_check(hint="""{msg}""")
178 def check_tokenizer(_: Connection, config: Configuration) -> CheckResult:
179 """ Checking that tokenizer works
182 tokenizer = tokenizer_factory.get_tokenizer_for_db(config)
184 return CheckState.FAIL, dict(msg="""\
185 Cannot load tokenizer. Did the import finish successfully?""")
187 result = tokenizer.check_database(config)
192 return CheckState.FAIL, dict(msg=result)
196 Wikipedia/Wikidata importance tables missing.
197 Quality of search results may be degraded. Reverse geocoding is unaffected.
198 See https://nominatim.org/release-docs/latest/admin/Import/#wikipediawikidata-rankings
200 def check_existance_wikipedia(conn: Connection, _: Configuration) -> CheckResult:
201 """ Checking for wikipedia/wikidata data
203 if not conn.table_exists('search_name') or not conn.table_exists('place'):
204 return CheckState.NOT_APPLICABLE
206 with conn.cursor() as cur:
207 cnt = cur.scalar('SELECT count(*) FROM wikipedia_article')
209 return CheckState.WARN if cnt == 0 else CheckState.OK
213 The indexing didn't finish. {count} entries are not yet indexed.
215 To index the remaining entries, run: {index_cmd}
217 def check_indexing(conn: Connection, _: Configuration) -> CheckResult:
218 """ Checking indexing status
220 with conn.cursor() as cur:
221 cnt = cur.scalar('SELECT count(*) FROM placex WHERE indexed_status > 0')
226 if conn.index_exists('idx_placex_rank_search'):
227 # Likely just an interrupted update.
228 index_cmd = 'nominatim index'
230 # Looks like the import process got interrupted.
231 index_cmd = 'nominatim import --continue indexing'
233 return CheckState.FAIL, dict(count=cnt, index_cmd=index_cmd)
237 The following indexes are missing:
240 Rerun the index creation with: nominatim import --continue db-postprocess
242 def check_database_indexes(conn: Connection, _: Configuration) -> CheckResult:
243 """ Checking that database indexes are complete
246 for index in _get_indexes(conn):
247 if not conn.index_exists(index):
248 missing.append(index)
251 return CheckState.FAIL, dict(indexes='\n '.join(missing))
257 At least one index is invalid. That can happen, e.g. when index creation was
258 disrupted and later restarted. You should delete the affected indices
264 def check_database_index_valid(conn: Connection, _: Configuration) -> CheckResult:
265 """ Checking that all database indexes are valid
267 with conn.cursor() as cur:
268 cur.execute(""" SELECT relname FROM pg_class, pg_index
269 WHERE pg_index.indisvalid = false
270 AND pg_index.indexrelid = pg_class.oid""")
272 broken = [c[0] for c in cur]
275 return CheckState.FAIL, dict(indexes='\n '.join(broken))
282 Run TIGER import again: nominatim add-data --tiger-data <DIR>
284 def check_tiger_table(conn: Connection, config: Configuration) -> CheckResult:
285 """ Checking TIGER external data table.
287 if not config.get_bool('USE_US_TIGER_DATA'):
288 return CheckState.NOT_APPLICABLE
290 if not conn.table_exists('location_property_tiger'):
291 return CheckState.FAIL, dict(error='TIGER data table not found.')
293 with conn.cursor() as cur:
294 if cur.scalar('SELECT count(*) FROM location_property_tiger') == 0:
295 return CheckState.FAIL, dict(error='TIGER data table is empty.')