Skip to content

Commit 106fa82

Browse files
authored
Merge pull request #52 from napakalas/issue-#50
CQ: Improve schema mismatch diagnostics
2 parents fd798e9 + 37e5071 commit 106fa82

3 files changed

Lines changed: 87 additions & 6 deletions

File tree

mapserver/competency/__init__.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,40 @@
4242
COMPETENCY_USER = os.environ.get('COMPETENCY_USER')
4343
COMPETENCY_HOST = os.environ.get('COMPETENCY_HOST', 'localhost:5432')
4444

45+
COMPETENCY_SCHEMA_VERSION_KEY = 'schema_version'
46+
COMPETENCY_SCHEMA_STATE_KEY = 'competency-schema-version'
47+
COMPETENCY_SCHEMA_VERSION = '1.1'
48+
4549
if not COMPETENCY_USER:
4650
print('Competency queries are unavailable because COMPETENCY_USER is not set')
4751

52+
#===============================================================================
53+
54+
async def table_exists(connection: asyncpg.Connection, table_name: str) -> bool:
55+
#===========================================================================
56+
reg_class = await connection.fetchval('SELECT to_regclass($1)', table_name)
57+
return reg_class is not None
58+
59+
async def schema_version(connection: asyncpg.Connection) -> str|None:
60+
#===================================================================
61+
if not await table_exists(connection, 'metadata'):
62+
return None
63+
row = await connection.fetchrow(
64+
'SELECT value FROM metadata WHERE name=$1',
65+
COMPETENCY_SCHEMA_VERSION_KEY,
66+
)
67+
return row[0] if row is not None else None
68+
69+
def schema_mismatch_error(expected: str, actual: str|None, query_id: str|None=None) -> str:
70+
#=============================================================================
71+
found = actual if actual is not None else 'missing metadata/schema_version'
72+
query = f' (query {query_id})' if query_id is not None else ''
73+
return (
74+
f'Competency schema version mismatch{query}: '
75+
f'expected `{expected}` but found `{found}`. '
76+
'Some queries may fail until the database schema and query definitions are aligned.'
77+
)
78+
4879
#===============================================================================
4980
#===============================================================================
5081

@@ -76,6 +107,8 @@ async def competency_connection_context(app: Litestar) -> AsyncGenerator[None, N
76107
timeout=5
77108
)
78109
app.state['competency-pool'] = competency_pool
110+
async with competency_pool.acquire() as connection:
111+
app.state[COMPETENCY_SCHEMA_STATE_KEY] = await schema_version(connection)
79112
except Exception as err:
80113
# log (where?)
81114
print(f'Unable to connect to competency database: {COMPETENCY_HOST}/{COMPETENCY_DATABASE}')
@@ -91,6 +124,23 @@ def get_competency_pool(app: Litestar) -> Optional[asyncpg.Pool]:
91124
#================================================================
92125
return getattr(app.state, 'competency-pool', None)
93126

127+
def get_competency_schema_version(app: Litestar) -> str|None:
128+
#==============================================================
129+
return getattr(app.state, COMPETENCY_SCHEMA_STATE_KEY, None)
130+
131+
async def get_competency_schema_info(app: Litestar) -> dict[str, str|None]:
132+
#======================================================================
133+
if (get_competency_pool(app)) is None:
134+
return {
135+
'version': None,
136+
'expected': COMPETENCY_SCHEMA_VERSION,
137+
'error': 'Backend cannot connect to Competency database',
138+
}
139+
return {
140+
'version': get_competency_schema_version(app),
141+
'expected': COMPETENCY_SCHEMA_VERSION,
142+
}
143+
94144
#===============================================================================
95145
#===============================================================================
96146

@@ -118,6 +168,7 @@ async def query(data: QueryRequest, request: Request) -> QueryResults|QueryError
118168
return {'error': f'Error building query: {err}'}
119169
if (pool := get_competency_pool(request.app)) is None:
120170
return {'error': 'Backend cannot connect to Competency database'}
171+
db_schema = get_competency_schema_version(request.app)
121172
try:
122173
async with pool.acquire() as connection:
123174
records = await connection.fetch(sql, *params)
@@ -133,6 +184,9 @@ async def query(data: QueryRequest, request: Request) -> QueryResults|QueryError
133184
}
134185
}
135186
except Exception as err:
136-
return {'error': f'Error executing query: {err}'}
187+
error_msg = f'Error executing query: {err}.'
188+
if db_schema != COMPETENCY_SCHEMA_VERSION:
189+
error_msg += f' {schema_mismatch_error(COMPETENCY_SCHEMA_VERSION, db_schema, data["query_id"])}'
190+
return {'error': error_msg}
137191

138192
#===============================================================================

mapserver/server/competency.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
#===============================================================================
3030

31-
from ..competency import query, query_definition, query_definitions
31+
from ..competency import query, query_definition, query_definitions, get_competency_schema_info
32+
3233
from ..competency.definition import QueryDefinitionDict, QueryDefinitionSummary
3334
from ..competency.definition import QueryRequest, QueryError, QueryResults
3435

@@ -52,13 +53,19 @@ async def competency_query(data: QueryRequest, request: Request) -> QueryResults
5253
request.logger.warning(result["error"])
5354
return result
5455

56+
@get('schema-version')
57+
async def competency_schema_version(request: Request) -> dict[str, str|None]:
58+
#==========================================================================
59+
return await get_competency_schema_info(request.app)
60+
5561
#===============================================================================
5662
#===============================================================================
5763

5864
competency_router = Router(
5965
path="/competency",
6066
route_handlers=[
6167
competency_query,
68+
competency_schema_version,
6269
competency_query_definition,
6370
competency_query_definitions,
6471
]

tools/competency-query/competency_query.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def print_table(header: list[str], rows: Iterable[Iterable[str]]):
5959
QUERY_ENDPOINT = '/competency/query'
6060

6161
QUERY_DEFINITIONS_ENDPOINT = '/competency/queries'
62+
QUERY_SCHEMA_VERSION_ENDPOINT = '/competency/schema-version'
6263

6364
#===============================================================================
6465

@@ -81,7 +82,7 @@ class CompetencyQueryService:
8182
def __init__(self, map_server: str):
8283
self.__map_server = map_server
8384

84-
def request_json(self, method: str, endpoint: str, **kwds) -> dict|list:
85+
def request_json(self, method: str, endpoint: str, quiet: bool=False, **kwds) -> dict|list:
8586
#=======================================================================
8687
endpoint = self.__map_server + endpoint
8788
try:
@@ -102,15 +103,16 @@ def request_json(self, method: str, endpoint: str, **kwds) -> dict|list:
102103
error = f'HTTP error for request: {response.status_code} {response.reason}'
103104
except requests.exceptions.RequestException as exception:
104105
error = f'Exception: {exception}'
105-
print_formatted_text(FormattedText([('class:error', error),]),
106+
if not quiet:
107+
print_formatted_text(FormattedText([('class:error', error),]),
106108
style=Style.from_dict({'error': '#ff0000 bold'}))
107109
return []
108110

109-
def get_json(self, endpoint: str, param: Optional[str]=None) -> dict|list:
111+
def get_json(self, endpoint: str, param: Optional[str]=None, quiet: bool=False) -> dict|list:
110112
#=========================================================================
111113
if param is not None:
112114
endpoint += f'/{param}'
113-
return self.request_json('GET', endpoint)
115+
return self.request_json('GET', endpoint, quiet=quiet)
114116

115117
def post_query(self, request: QueryRequest) -> dict|list:
116118
#========================================================
@@ -123,13 +125,31 @@ class CompetencyQueryShell:
123125

124126
def __init__(self, map_server: str):
125127
self.__query_service = CompetencyQueryService(map_server)
128+
self.__warn_if_schema_mismatch()
126129
self.__queries: dict[str, str] = { str(query['id']): str(query['label'])
127130
for query in self.__query_service.get_json(QUERY_DEFINITIONS_ENDPOINT)
128131
if 'id' in query }
129132
self.__cmd_session = PromptSession(message=HTML('<p fg="ansiwhite"><b>cq> </b></p>'),
130133
style=Style.from_dict({'': COMMAND_INPUT_STYLE}))
131134
self.__input_session = PromptSession()
132135

136+
def __warn_if_schema_mismatch(self):
137+
#===================================
138+
schema_info = self.__query_service.get_json(QUERY_SCHEMA_VERSION_ENDPOINT, quiet=True)
139+
if isinstance(schema_info, dict):
140+
server_schema = schema_info.get('version')
141+
expected_schema = schema_info.get('expected')
142+
if expected_schema is not None and server_schema != expected_schema:
143+
warning = (
144+
'WARNING: Competency schema version mismatch. '
145+
f'Expected {expected_schema}, server has {server_schema}. '
146+
'A schema upgrade may be required.'
147+
)
148+
print_formatted_text(
149+
FormattedText([('class:warning', warning)]),
150+
style=Style.from_dict({'warning': '#ffaf00 bold'})
151+
)
152+
133153
def __list_queries(self):
134154
#========================
135155
print_table(['ID', 'Name'], list(self.__queries.items()))

0 commit comments

Comments
 (0)