Skip to content

Commit 7906e8b

Browse files
committed
fix: generate a valid duckdb gateway config for DuckLake pipelines
A `ducklake` entry in config.yaml fell through the generic credentials path and produced an invalid duckdb gateway config, which surfaced as a ConfigError from connection.py with a message identical to an unrelated failure — so the real diagnostic was lost. DuckLake connections are a duckdb gateway with the lake attached as a catalog, so build that block explicitly. Unsupported catalog drivers are cut to a clear ClickException rather than propagating the generic error; postgres/mysql/MotherDuck catalogs are not yet mapped and are tracked on the issue. Tests: 3 new (duckdb catalog, sqlite catalog, unsupported driver) plus the existing dlt pipeline tests. Verified to fail on unpatched source and pass on this commit. Signed-off-by: Syed Annas <annas.mazhar10@gmail.com>
1 parent 849d6ed commit 7906e8b

2 files changed

Lines changed: 126 additions & 10 deletions

File tree

‎sqlmesh/integrations/dlt.py‎

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,19 @@ def generate_dlt_models_and_settings(
6464
connection_config = None
6565
else:
6666
client = pipeline.destination_client()
67-
config = client.config
68-
credentials = config.credentials
69-
configs = {
70-
key: value
71-
for key in dir(credentials)
72-
if not key.startswith("_")
73-
and not callable(value := getattr(credentials, key))
74-
and value is not None
75-
}
76-
connection_config = format_config(configs, db_type)
67+
if db_type == "ducklake":
68+
connection_config = format_ducklake_config(client.config)
69+
else:
70+
config = client.config
71+
credentials = config.credentials
72+
configs = {
73+
key: value
74+
for key in dir(credentials)
75+
if not key.startswith("_")
76+
and not callable(value := getattr(credentials, key))
77+
and value is not None
78+
}
79+
connection_config = format_config(configs, db_type)
7780

7881
dlt_tables = {
7982
name: table
@@ -209,6 +212,33 @@ def generate_incremental_model(
209212
"""
210213

211214

215+
def format_ducklake_config(client_config: t.Any) -> str:
216+
"""Generate a duckdb-gateway connection block with the DuckLake attached as catalog."""
217+
creds = client_config.credentials
218+
catalog = creds.catalog
219+
drivername = getattr(catalog, "drivername", "") or ""
220+
if drivername not in ("duckdb", "sqlite"):
221+
raise click.ClickException(
222+
f"Unsupported DuckLake catalog '{drivername}'. SQLMesh dlt init currently supports "
223+
"file-backed catalogs (duckdb, sqlite); postgres/mysql/MotherDuck catalogs are not "
224+
"yet mapped. Tracked in SQLMesh/sqlmesh#5914."
225+
)
226+
alias = creds.ducklake_name or "ducklake"
227+
lines = [
228+
" type: duckdb",
229+
" catalogs:",
230+
f" {alias}:",
231+
" type: ducklake",
232+
f" path: {catalog.database}",
233+
f" data_path: {creds.storage_url}",
234+
]
235+
metadata_schema = creds.metadata_schema or alias
236+
lines.append(f" metadata_schema: {metadata_schema}")
237+
if getattr(client_config, "override_data_path", False):
238+
lines.append(" override_data_path: true")
239+
return "\n".join(lines)
240+
241+
212242
def format_config(configs: t.Dict[str, str], db_type: str) -> str:
213243
"""Generate a string for the gateway connection config."""
214244
config = {

‎tests/cli/test_cli.py‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,92 @@ def test_dlt_pipeline(runner, tmp_path):
14571457
remove(dataset_path)
14581458

14591459

1460+
def _stub_ducklake_dlt(monkeypatch, drivername="sqlite"):
1461+
"""Install a fake `dlt` module exposing a ducklake pipeline. No dlt install needed."""
1462+
import sys
1463+
import types
1464+
1465+
catalog = types.SimpleNamespace(drivername=drivername, database="/tmp/x/mre_ducklake.sqlite")
1466+
credentials = types.SimpleNamespace(
1467+
ducklake_name="mre_ducklake",
1468+
metadata_schema=None,
1469+
catalog=catalog,
1470+
storage_url="/tmp/x/mre_ducklake.files",
1471+
)
1472+
client_config = types.SimpleNamespace(credentials=credentials, override_data_path=False)
1473+
pipeline = types.SimpleNamespace(
1474+
destination=types.SimpleNamespace(to_name=lambda dest: "ducklake"),
1475+
default_schema=types.SimpleNamespace(
1476+
tables={}, _dlt_tables_prefix="_dlt", loads_table_name="_dlt_loads"
1477+
),
1478+
dataset_name="mre",
1479+
)
1480+
pipeline._get_load_storage = lambda: types.SimpleNamespace(list_loaded_packages=lambda: [])
1481+
pipeline.destination_client = lambda: types.SimpleNamespace(config=client_config)
1482+
1483+
dlt_fake = types.ModuleType("dlt")
1484+
dlt_fake.attach = lambda pipeline_name, pipelines_dir="": pipeline
1485+
1486+
schema_utils = types.ModuleType("dlt.common.schema.utils")
1487+
schema_utils.has_table_seen_data = lambda table: True
1488+
schema_utils.is_complete_column = lambda col: True
1489+
1490+
pipeline_exceptions = types.ModuleType("dlt.pipeline.exceptions")
1491+
pipeline_exceptions.CannotRestorePipelineException = type(
1492+
"CannotRestorePipelineException", (Exception,), {}
1493+
)
1494+
1495+
for name, module in {
1496+
"dlt": dlt_fake,
1497+
"dlt.common": types.ModuleType("dlt.common"),
1498+
"dlt.common.schema": types.ModuleType("dlt.common.schema"),
1499+
"dlt.common.schema.utils": schema_utils,
1500+
"dlt.pipeline": types.ModuleType("dlt.pipeline"),
1501+
"dlt.pipeline.exceptions": pipeline_exceptions,
1502+
}.items():
1503+
monkeypatch.setitem(sys.modules, name, module)
1504+
1505+
1506+
@pytest.mark.parametrize(
1507+
"drivername", ["sqlite", "duckdb"], ids=["sqlite-catalog", "duckdb-catalog"]
1508+
)
1509+
def test_dlt_ducklake_pipeline(monkeypatch, drivername):
1510+
import yaml
1511+
1512+
from sqlmesh.core.config.connection import DuckDBConnectionConfig, parse_connection_config
1513+
from sqlmesh.integrations import dlt as dlt_module
1514+
1515+
_stub_ducklake_dlt(monkeypatch, drivername=drivername)
1516+
1517+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1518+
pipeline_name="mre_ducklake", dialect="duckdb"
1519+
)
1520+
1521+
assert "type: duckdb" in connection_config
1522+
assert "type: ducklake" in connection_config
1523+
assert "path: /tmp/x/mre_ducklake.sqlite" in connection_config
1524+
assert "data_path: /tmp/x/mre_ducklake.files" in connection_config
1525+
1526+
# Round-trip: the exact ConfigError from #5914 no longer fires
1527+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1528+
config = parse_connection_config(parsed)
1529+
assert isinstance(config, DuckDBConnectionConfig)
1530+
attach_sql = next(iter(config.catalogs.values())).to_sql("mre_ducklake")
1531+
assert attach_sql == (
1532+
"ATTACH IF NOT EXISTS 'ducklake:/tmp/x/mre_ducklake.sqlite' AS mre_ducklake "
1533+
"(DATA_PATH '/tmp/x/mre_ducklake.files', METADATA_SCHEMA 'mre_ducklake')"
1534+
)
1535+
1536+
1537+
def test_dlt_ducklake_unsupported_catalog(monkeypatch):
1538+
from sqlmesh.integrations import dlt as dlt_module
1539+
1540+
_stub_ducklake_dlt(monkeypatch, drivername="postgres")
1541+
1542+
with pytest.raises(ClickException, match="Unsupported DuckLake catalog"):
1543+
dlt_module.generate_dlt_models_and_settings(pipeline_name="mre_ducklake", dialect="duckdb")
1544+
1545+
14601546
@time_machine.travel(FREEZE_TIME)
14611547
def test_environments(runner, tmp_path):
14621548
create_example_project(tmp_path)

0 commit comments

Comments
 (0)