Skip to content

Commit 6870d2e

Browse files
committed
fix: prevent default catalog leak into catalog-unsupported gateways
Fixes #5748 When the default gateway has a default catalog set (e.g., Trino with catalog: example_catalog), that catalog was silently prepended to model names targeting secondary gateways that do not support catalogs (e.g., ClickHouse), causing UnsupportedCatalogOperationError at evaluation time. The per-gateway catalog dict omits catalog-unsupported gateways entirely, so the model loader could not distinguish "no catalog" from "not checked" and fell through to the global default. This change explicitly sets default_catalog to None when a gateway is known but absent from the dict. Signed-off-by: Michael Day <michael.day@cloudkitchens.com>
1 parent 8f092ac commit 6870d2e

File tree

2 files changed

+131
-6
lines changed

2 files changed

+131
-6
lines changed

sqlmesh/core/model/definition.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,12 +2083,15 @@ def create_models_from_blueprints(
20832083
else:
20842084
gateway_name = None
20852085

2086-
if (
2087-
default_catalog_per_gateway
2088-
and gateway_name
2089-
and (catalog := default_catalog_per_gateway.get(gateway_name)) is not None
2090-
):
2091-
loader_kwargs["default_catalog"] = catalog
2086+
if default_catalog_per_gateway and gateway_name:
2087+
catalog = default_catalog_per_gateway.get(gateway_name)
2088+
if catalog is not None:
2089+
loader_kwargs["default_catalog"] = catalog
2090+
else:
2091+
# Gateway exists but has no entry in the dict (e.g., catalog-unsupported
2092+
# engines like ClickHouse). Clear the default catalog so the global
2093+
# default from the primary gateway doesn't leak into this model's name.
2094+
loader_kwargs["default_catalog"] = None
20922095

20932096
model_blueprints.append(
20942097
loader(

tests/core/test_catalog_leak.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
Test: Default gateway catalog should not leak into catalog-unsupported secondary gateways.
3+
4+
Regression test for https://github.com/TobikoData/sqlmesh/issues/5748
5+
6+
When the default gateway has a default catalog set (e.g., Trino with catalog: example_catalog),
7+
that catalog should not be prepended to model names targeting secondary gateways that do not
8+
support catalogs (e.g., ClickHouse).
9+
"""
10+
11+
from sqlglot import parse
12+
13+
from sqlmesh.core.model import load_sql_based_model
14+
from sqlmesh.core.model.definition import load_sql_based_models
15+
16+
17+
def test_default_catalog_not_leaked_to_unsupported_gateway():
18+
"""
19+
When a model targets a gateway that is NOT in default_catalog_per_gateway,
20+
the global default_catalog should be cleared (set to None) instead of
21+
leaking through from the default gateway.
22+
"""
23+
expressions = parse(
24+
"""
25+
MODEL (
26+
name my_schema.my_model,
27+
kind FULL,
28+
gateway clickhouse_gw,
29+
dialect clickhouse,
30+
);
31+
32+
SELECT 1 AS id
33+
""",
34+
read="clickhouse",
35+
)
36+
37+
# default_gw has a catalog, clickhouse_gw is absent because it doesn't support catalogs
38+
default_catalog_per_gateway = {
39+
"default_gw": "example_catalog",
40+
}
41+
42+
models = load_sql_based_models(
43+
expressions,
44+
get_variables=lambda gw: {},
45+
dialect="clickhouse",
46+
default_catalog_per_gateway=default_catalog_per_gateway,
47+
default_catalog="example_catalog",
48+
)
49+
50+
assert len(models) == 1
51+
model = models[0]
52+
53+
assert not model.catalog, (
54+
f"Default gateway catalog leaked into catalog-unsupported gateway model. "
55+
f"Expected no catalog, got: {model.catalog}"
56+
)
57+
assert "example_catalog" not in model.fqn, (
58+
f"Default gateway catalog found in model FQN: {model.fqn}"
59+
)
60+
61+
62+
def test_default_catalog_still_applied_to_supported_gateway():
63+
"""
64+
Control test: when a model targets a gateway that IS in default_catalog_per_gateway,
65+
the catalog should still be correctly applied.
66+
"""
67+
expressions = parse(
68+
"""
69+
MODEL (
70+
name my_schema.my_model,
71+
kind FULL,
72+
gateway other_duckdb,
73+
);
74+
75+
SELECT 1 AS id
76+
""",
77+
read="duckdb",
78+
)
79+
80+
default_catalog_per_gateway = {
81+
"default_gw": "example_catalog",
82+
"other_duckdb": "other_db",
83+
}
84+
85+
models = load_sql_based_models(
86+
expressions,
87+
get_variables=lambda gw: {},
88+
dialect="duckdb",
89+
default_catalog_per_gateway=default_catalog_per_gateway,
90+
default_catalog="example_catalog",
91+
)
92+
93+
assert len(models) == 1
94+
model = models[0]
95+
96+
assert model.catalog == "other_db", f"Expected catalog 'other_db', got: {model.catalog}"
97+
98+
99+
def test_no_gateway_uses_global_default_catalog():
100+
"""
101+
Control test: when a model does NOT specify a gateway, the global
102+
default_catalog should still be applied as before.
103+
"""
104+
expressions = parse(
105+
"""
106+
MODEL (
107+
name my_schema.my_model,
108+
kind FULL,
109+
);
110+
111+
SELECT 1 AS id
112+
""",
113+
read="duckdb",
114+
)
115+
116+
model = load_sql_based_model(
117+
expressions,
118+
default_catalog="example_catalog",
119+
dialect="duckdb",
120+
)
121+
122+
assert model.catalog == "example_catalog"

0 commit comments

Comments
 (0)