Skip to content

Commit d4d492b

Browse files
committed
fix: use REQUIRES_SET_CATALOG + no-op set_current_catalog to handle catalog case mismatch
SINGLE_CATALOG_ONLY uses a raw == comparison (shared.py:346) between catalog_name from the model expression and _default_catalog. The model expression case depends on which dialect built it: db2 dialect produces UPPERCASE, duckdb dialect produces lowercase. Either case can appear at runtime and neither alone satisfies a raw == against a fixed-case string. Switch to REQUIRES_SET_CATALOG: the decorator's alternate path (shared.py:352) calls get_current_catalog() for the RHS of the comparison. Both get_catalog() (connection.py) and get_current_catalog() (db2.py) now return uppercase, so _default_catalog and the live value are always 'TESTDB'. When a lowercase 'testdb' arrives from a duckdb-dialect context it won't match, but that only calls set_current_catalog() which is now a no-op — Db2 has a single catalog and CONNECT TO cannot switch to a different database mid-session anyway. Reverts the .lower() changes from commit ba0fe1d which broke test_janitor by causing 'testdb vs TESTDB' errors on the SINGLE_CATALOG_ONLY path.
1 parent ba0fe1d commit d4d492b

2 files changed

Lines changed: 42 additions & 15 deletions

File tree

‎sqlmesh/core/config/connection.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,14 +2650,17 @@ def _engine_adapter(self) -> t.Type[EngineAdapter]:
26502650
)
26512651

26522652
def get_catalog(self) -> t.Optional[str]:
2653-
"""Normalise the catalog name to lowercase so _default_catalog is consistent
2654-
with get_current_catalog() and _get_current_schema(), both of which return
2655-
lowercase. SQLMesh model names built through a duckdb-dialect context arrive
2656-
as lowercase (DuckDB's LOWERCASE normalisation strategy), and the set_catalog()
2657-
decorator compares catalog_name == _default_catalog with plain ==, so both sides
2658-
must use the same case. SYSCAT queries apply UPPER() at point of use."""
2653+
"""
2654+
Return the catalog (database) name uppercased. Db2 stores all unquoted
2655+
identifiers in uppercase and CURRENT SERVER returns an uppercase string.
2656+
get_current_catalog() also returns uppercase, so _default_catalog and the
2657+
live catalog value are always in the same case. The set_catalog() decorator's
2658+
REQUIRES_SET_CATALOG path compares catalog_name != get_current_catalog(); a
2659+
case mismatch (e.g. duckdb-dialect lowercase "testdb" vs "TESTDB") triggers
2660+
set_current_catalog() which is a no-op — so both cases are handled safely.
2661+
"""
26592662
catalog = super().get_catalog()
2660-
return catalog.lower() if catalog else None
2663+
return catalog.upper() if catalog else None
26612664

26622665
@property
26632666
def _connection_factory(self) -> t.Callable:

‎sqlmesh/core/engine_adapter/db2.py‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,16 @@ class Db2EngineAdapter(
8585
def get_current_catalog(self) -> t.Optional[str]:
8686
"""
8787
Db2 requires FROM SYSIBM.SYSDUMMY1 to read the CURRENT SERVER special register.
88-
Returns lowercase — the same convention as _get_current_schema() — so that all
89-
catalog/schema tokens returned to callers are consistently lowercase.
90-
SYSCAT queries apply UPPER() at point of use where uppercase is required.
88+
Returns the value uppercased to match the convention used by _default_catalog
89+
(which comes from get_catalog() → database name as supplied in config, uppercased).
90+
The set_catalog() decorator's REQUIRES_SET_CATALOG path compares
91+
catalog_name != get_current_catalog() — normalising both to uppercase ensures
92+
the comparison is consistent regardless of which dialect (db2 UPPERCASE vs
93+
duckdb LOWERCASE) produced the catalog token in the model expression.
9194
"""
9295
result = self.fetchone("SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1")
9396
if result:
94-
return result[0].lower() if result[0] else None
97+
return result[0].upper() if result[0] else None
9598
return None
9699

97100
def _build_schema_exp(
@@ -328,7 +331,19 @@ def _db2_type_to_sqlglot(self, db2_type: str, length: int, scale: int) -> exp.Da
328331

329332
@property
330333
def catalog_support(self) -> CatalogSupport:
331-
return CatalogSupport.SINGLE_CATALOG_ONLY
334+
# REQUIRES_SET_CATALOG is used instead of SINGLE_CATALOG_ONLY because the
335+
# SINGLE_CATALOG_ONLY path in set_catalog() (shared.py:346) does a raw ==
336+
# comparison between catalog_name and _default_catalog. catalog_name comes
337+
# from the model expression, whose case depends on the dialect that built it:
338+
# db2 dialect → UPPERCASE, duckdb dialect → lowercase. Either case can appear
339+
# at runtime and we cannot control which, so a raw == would fail for one case.
340+
#
341+
# The REQUIRES_SET_CATALOG path instead calls get_current_catalog() for the
342+
# right-hand side. By returning uppercase from both get_catalog() (connection.py)
343+
# and get_current_catalog(), and by making set_current_catalog() a no-op (Db2 has
344+
# only one catalog — CONNECT TO cannot change it meaningfully), the mismatch is
345+
# tolerated: lowercase "testdb" != "TESTDB" → set_current_catalog (no-op) → proceed.
346+
return CatalogSupport.REQUIRES_SET_CATALOG
332347

333348
def table_exists(self, table_name: TableName) -> bool:
334349
"""
@@ -836,9 +851,18 @@ def _df_to_source_queries(
836851
)
837852

838853
def set_current_catalog(self, catalog: str) -> None:
839-
"""Switches the active catalog using Db2's CONNECT TO statement."""
840-
self.execute(f"CONNECT TO {catalog}")
841-
logger.debug("Switched to catalog: %s", catalog)
854+
"""
855+
No-op for Db2 — there is only one catalog (the database name) and CONNECT TO
856+
cannot switch to a different one in the middle of a session. The set_catalog()
857+
decorator calls this when catalog_name != get_current_catalog(), which happens
858+
because model expressions built through a duckdb-dialect context carry lowercase
859+
catalog names while get_current_catalog() returns uppercase. The mismatch is
860+
case-only and harmless, so we log it and return without executing any SQL.
861+
"""
862+
logger.debug(
863+
"set_current_catalog called with %r — no-op for Db2 (single-catalog engine)",
864+
catalog,
865+
)
842866

843867
@cached_property
844868
def server_version(self) -> t.Tuple[int, int]:

0 commit comments

Comments
 (0)