Skip to content

Commit 0fa4bb4

Browse files
committed
Fix catalog case mismatch: switch to REQUIRES_SET_CATALOG + no-op set_current_catalog
Root cause: the set_catalog decorator in shared.py:346 does a raw case-sensitive == comparison between catalog_name (extracted from a SQL expression) and _default_catalog (stored at adapter construction time). Because the Db2 sqlglot dialect uses UPPERCASE normalisation, model names built with dialect='db2' always produce uppercase catalog identifiers (e.g. 'TESTDB'). However tests that call ctx.create_context() use default_dialect='duckdb' (LOWERCASE), which lowercases model names to 'testdb'. These two cases cannot both match a single static _default_catalog string — the comparison is structurally broken for Db2. Fix: switch catalog_support from SINGLE_CATALOG_ONLY to REQUIRES_SET_CATALOG. The REQUIRES_SET_CATALOG path in shared.py calls get_current_catalog() at runtime and compares the live value against catalog_name. Since get_current_catalog() returns uppercase ('TESTDB') to match the Db2 dialect: - When catalog_name == 'TESTDB' (db2-dialect context): match, no set_current_catalog call - When catalog_name == 'testdb' (duckdb-dialect context): no match, set_current_catalog is called — which is a no-op since Db2 cannot switch catalogs at runtime In both cases the catalog is always stripped from DDL before it reaches Db2 (the catalog stripping happens earlier in the decorator regardless of catalog_support). So neither path causes any SQL error. Also revert the previous two incorrect lower() fixes: - get_current_catalog() back to .upper() (Db2 CURRENT SERVER is always uppercase) - Db2ConnectionConfig.get_catalog() back to .upper() (matches dialect normalisation) Update test_db2.py: - test_get_current_catalog: assert result == 'TESTDB' (was 'testdb') - test_catalog_support: assert REQUIRES_SET_CATALOG (was SINGLE_CATALOG_ONLY)
1 parent 70b0937 commit 0fa4bb4

3 files changed

Lines changed: 41 additions & 20 deletions

File tree

‎sqlmesh/core/config/connection.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,13 +2650,12 @@ def _engine_adapter(self) -> t.Type[EngineAdapter]:
26502650
)
26512651

26522652
def get_catalog(self) -> t.Optional[str]:
2653-
"""Return the catalog name in lowercase so _default_catalog matches the value
2654-
returned by get_current_catalog() (which also returns lowercase) and the catalog
2655-
names produced by the DuckDB-dialect config parser (LOWERCASE normalisation).
2656-
The set_catalog decorator strips the catalog from all DDL before sending it to
2657-
Db2, so the case of this value never affects SQL sent to the engine."""
2653+
"""Return the catalog name in uppercase to match the Db2 dialect's UPPERCASE
2654+
normalisation strategy. All catalog identifiers in SQL expressions are uppercased
2655+
by the Db2 sqlglot dialect, so _default_catalog must also be uppercase for the
2656+
default_catalog property to return a consistent value."""
26582657
catalog = super().get_catalog()
2659-
return catalog.lower() if catalog else None
2658+
return catalog.upper() if catalog else None
26602659

26612660
@property
26622661
def _connection_factory(self) -> t.Callable:

‎sqlmesh/core/engine_adapter/db2.py‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,26 @@ class Db2EngineAdapter(
8888
def get_current_catalog(self) -> t.Optional[str]:
8989
"""
9090
Db2 requires FROM SYSIBM.SYSDUMMY1 to read the CURRENT SERVER special register.
91-
Returns lowercase so _default_catalog matches the catalog name produced by the
92-
DuckDB-dialect config parser (which lowercases all identifiers via LOWERCASE
93-
normalisation strategy). The set_catalog decorator always strips the catalog
94-
from DDL before execution, so the case of this value never affects SQL sent to
95-
Db2. Every other SINGLE_CATALOG_ONLY engine (Postgres, Redshift, MySQL) also
96-
returns lowercase — this follows the same pattern.
91+
Returns uppercase because the Db2 sqlglot dialect uses UPPERCASE normalisation,
92+
so all catalog identifiers in expressions are uppercased by the dialect. The
93+
set_catalog decorator (REQUIRES_SET_CATALOG path) compares catalog_name from the
94+
expression against the live get_current_catalog() return value — both uppercase
95+
means they match for same-catalog operations.
9796
"""
9897
result = self.fetchone("SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1")
9998
if result:
100-
return result[0].lower() if result[0] else None
99+
return result[0].upper() if result[0] else None
101100
return None
102101

102+
def set_current_catalog(self, catalog: str) -> None:
103+
"""
104+
Db2 is a single-database engine; switching catalogs at runtime is not supported.
105+
This is a no-op so that the REQUIRES_SET_CATALOG path in the set_catalog decorator
106+
can call it without error when catalog_name != current_catalog (which can happen
107+
when a DuckDB-dialect context lowercases identifiers). The catalog is always stripped
108+
from DDL before it reaches Db2, so this never causes a SQL error.
109+
"""
110+
103111
def _build_schema_exp(
104112
self,
105113
table: exp.Table,
@@ -446,7 +454,18 @@ def _revoke_grants_config_expr(
446454

447455
@property
448456
def catalog_support(self) -> CatalogSupport:
449-
return CatalogSupport.SINGLE_CATALOG_ONLY
457+
# REQUIRES_SET_CATALOG is used instead of SINGLE_CATALOG_ONLY because the
458+
# SINGLE_CATALOG_ONLY path in the set_catalog decorator does a raw case-sensitive
459+
# == comparison between catalog_name (from the expression, uppercased by the Db2
460+
# dialect) and _default_catalog (set at construction time). When ctx.create_context()
461+
# is called with default_dialect="duckdb", model names are lowercased, causing a
462+
# mismatch even though both refer to the same catalog.
463+
#
464+
# REQUIRES_SET_CATALOG bypasses _default_catalog entirely: it calls
465+
# get_current_catalog() at runtime and compares against the live value. If they
466+
# differ, set_current_catalog() is called — which is a no-op since Db2 cannot
467+
# switch catalogs. In all cases the catalog is stripped from DDL before execution.
468+
return CatalogSupport.REQUIRES_SET_CATALOG
450469

451470
def table_exists(self, table_name: TableName) -> bool:
452471
"""

‎tests/core/engine_adapter/test_db2.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -374,15 +374,16 @@ def test_merge_alias_replacement(adapter: Db2EngineAdapter):
374374

375375

376376
def test_get_current_catalog(adapter: Db2EngineAdapter):
377-
"""get_current_catalog reads CURRENT SERVER from SYSIBM.SYSDUMMY1 and returns lowercase.
378-
Lowercase is required so _default_catalog matches the catalog name produced by the
379-
DuckDB-dialect config parser, which lowercases all identifiers.
377+
"""get_current_catalog reads CURRENT SERVER from SYSIBM.SYSDUMMY1 and returns uppercase.
378+
Uppercase matches the Db2 dialect's UPPERCASE normalisation strategy so the
379+
REQUIRES_SET_CATALOG path compares equal catalog names (both UPPERCASE) for
380+
same-catalog operations.
380381
"""
381382
adapter.cursor.fetchone.return_value = ("TESTDB",)
382383

383384
result = adapter.get_current_catalog()
384385

385-
assert result == "testdb"
386+
assert result == "TESTDB"
386387
# Raw string because fetchone is called with a plain string, not an exp.Expr
387388
assert to_sql_calls(adapter) == ["SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1"]
388389

@@ -425,8 +426,10 @@ def test_server_version(adapter: Db2EngineAdapter, mocker: MockerFixture):
425426

426427

427428
def test_catalog_support(adapter: Db2EngineAdapter):
428-
"""Db2 exposes only one catalog (the database itself)."""
429-
assert adapter.catalog_support == CatalogSupport.SINGLE_CATALOG_ONLY
429+
"""Db2 uses REQUIRES_SET_CATALOG so the set_catalog decorator calls get_current_catalog()
430+
at runtime instead of comparing against _default_catalog. This bypasses the case-sensitive
431+
equality check that would fail when DuckDB-dialect contexts lowercase catalog names."""
432+
assert adapter.catalog_support == CatalogSupport.REQUIRES_SET_CATALOG
430433

431434

432435
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)