Skip to content

Commit 7518b13

Browse files
committed
fix: quote ducklake YAML scalars, harden tests and typing
Signed-off-by: Syed Annas <annas.mazhar10@gmail.com>
1 parent 7906e8b commit 7518b13

2 files changed

Lines changed: 205 additions & 18 deletions

File tree

‎sqlmesh/integrations/dlt.py‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
import json
14
import typing as t
25
import click
36
from datetime import datetime, timedelta, timezone
@@ -8,6 +11,10 @@
811
from sqlmesh.utils.date import yesterday_ds
912

1013

14+
if t.TYPE_CHECKING:
15+
from dlt.destinations.impl.ducklake.configuration import DuckLakeClientConfiguration
16+
17+
1118
def generate_dlt_models_and_settings(
1219
pipeline_name: str,
1320
dialect: str,
@@ -65,7 +72,11 @@ def generate_dlt_models_and_settings(
6572
else:
6673
client = pipeline.destination_client()
6774
if db_type == "ducklake":
68-
connection_config = format_ducklake_config(client.config)
75+
# Cast: reachable only for ducklake pipelines, so client.config is the
76+
# DuckLake client configuration at runtime (statically the base type).
77+
connection_config = format_ducklake_config(
78+
t.cast("DuckLakeClientConfiguration", client.config)
79+
)
6980
else:
7081
config = client.config
7182
credentials = config.credentials
@@ -212,7 +223,25 @@ def generate_incremental_model(
212223
"""
213224

214225

215-
def format_ducklake_config(client_config: t.Any) -> str:
226+
def _yaml_inline(value: str) -> str:
227+
"""Emit one YAML scalar inline, quoting only when plain would not round-trip.
228+
229+
Ordinary locators (alphanumerics plus / _ . -) are returned unchanged so the
230+
generated config stays byte-identical; anything else (leading quote, newline,
231+
': ', ' #', spaces, etc.) is double-quoted via JSON (single line, valid YAML)
232+
so yaml.safe_load round-trips instead of raising ScannerError. No PyYAML
233+
dependency: json double-quotes are valid YAML double-quotes.
234+
"""
235+
if (
236+
value
237+
and (value[0].isalnum() or value[0] in "/_")
238+
and all(ch.isalnum() or ch in "_./-" for ch in value)
239+
):
240+
return value
241+
return json.dumps(value)
242+
243+
244+
def format_ducklake_config(client_config: DuckLakeClientConfiguration) -> str:
216245
"""Generate a duckdb-gateway connection block with the DuckLake attached as catalog."""
217246
creds = client_config.credentials
218247
catalog = creds.catalog
@@ -224,23 +253,32 @@ def format_ducklake_config(client_config: t.Any) -> str:
224253
"yet mapped. Tracked in SQLMesh/sqlmesh#5914."
225254
)
226255
alias = creds.ducklake_name or "ducklake"
256+
catalog_database = str(catalog.database or "")
257+
storage_url = str(creds.storage_url or "")
227258
lines = [
228259
" type: duckdb",
229260
" catalogs:",
230-
f" {alias}:",
261+
f" {_yaml_inline(alias)}:",
231262
" type: ducklake",
232-
f" path: {catalog.database}",
233-
f" data_path: {creds.storage_url}",
263+
f" path: {_yaml_inline(catalog_database)}",
264+
f" data_path: {_yaml_inline(storage_url)}",
234265
]
235266
metadata_schema = creds.metadata_schema or alias
236-
lines.append(f" metadata_schema: {metadata_schema}")
267+
lines.append(f" metadata_schema: {_yaml_inline(str(metadata_schema))}")
237268
if getattr(client_config, "override_data_path", False):
238269
lines.append(" override_data_path: true")
239270
return "\n".join(lines)
240271

241272

242273
def format_config(configs: t.Dict[str, str], db_type: str) -> str:
243274
"""Generate a string for the gateway connection config."""
275+
# NOTE (SQLMesh#5914 scope cut): only the `ducklake` destination is mapped
276+
# (see format_ducklake_config). Any other unrecognised dlt `db_type`
277+
# (e.g. weaviate, pandas, qdrant, typos) still falls through to
278+
# parse_connection_config below and surfaces as
279+
# ConfigError("Unknown connection type '<type>'."). That is a known
280+
# limitation, not a regression introduced here; #5914 reports only the
281+
# ducklake destination ("When using dlt with a `ducklake` destination ...").
244282
config = {
245283
"type": db_type,
246284
}

‎tests/cli/test_cli.py‎

Lines changed: 161 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,19 +1457,29 @@ def test_dlt_pipeline(runner, tmp_path):
14571457
remove(dataset_path)
14581458

14591459

1460-
def _stub_ducklake_dlt(monkeypatch, drivername="sqlite"):
1460+
def _stub_ducklake_dlt(
1461+
monkeypatch,
1462+
drivername="sqlite",
1463+
ducklake_name="mre_ducklake",
1464+
metadata_schema=None,
1465+
override_data_path=False,
1466+
database="/tmp/x/mre_ducklake.sqlite",
1467+
storage_url="/tmp/x/mre_ducklake.files",
1468+
):
14611469
"""Install a fake `dlt` module exposing a ducklake pipeline. No dlt install needed."""
14621470
import sys
14631471
import types
14641472

1465-
catalog = types.SimpleNamespace(drivername=drivername, database="/tmp/x/mre_ducklake.sqlite")
1473+
catalog = types.SimpleNamespace(drivername=drivername, database=database)
14661474
credentials = types.SimpleNamespace(
1467-
ducklake_name="mre_ducklake",
1468-
metadata_schema=None,
1475+
ducklake_name=ducklake_name,
1476+
metadata_schema=metadata_schema,
14691477
catalog=catalog,
1470-
storage_url="/tmp/x/mre_ducklake.files",
1478+
storage_url=storage_url,
1479+
)
1480+
client_config = types.SimpleNamespace(
1481+
credentials=credentials, override_data_path=override_data_path
14711482
)
1472-
client_config = types.SimpleNamespace(credentials=credentials, override_data_path=False)
14731483
pipeline = types.SimpleNamespace(
14741484
destination=types.SimpleNamespace(to_name=lambda dest: "ducklake"),
14751485
default_schema=types.SimpleNamespace(
@@ -1518,13 +1528,30 @@ def test_dlt_ducklake_pipeline(monkeypatch, drivername):
15181528
pipeline_name="mre_ducklake", dialect="duckdb"
15191529
)
15201530

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
1531+
# Byte-identical for ordinary paths (no quoting, key order and indent unchanged)
1532+
assert connection_config == (
1533+
" type: duckdb\n"
1534+
" catalogs:\n"
1535+
" mre_ducklake:\n"
1536+
" type: ducklake\n"
1537+
" path: /tmp/x/mre_ducklake.sqlite\n"
1538+
" data_path: /tmp/x/mre_ducklake.files\n"
1539+
" metadata_schema: mre_ducklake"
1540+
)
15251541

1526-
# Round-trip: the exact ConfigError from #5914 no longer fires
1542+
# Structural parse instead of substring checks: malformed YAML cannot pass
15271543
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1544+
assert parsed["type"] == "duckdb"
1545+
assert set(parsed["catalogs"]) == {"mre_ducklake"}
1546+
lake = parsed["catalogs"]["mre_ducklake"]
1547+
assert lake == {
1548+
"type": "ducklake",
1549+
"path": "/tmp/x/mre_ducklake.sqlite",
1550+
"data_path": "/tmp/x/mre_ducklake.files",
1551+
"metadata_schema": "mre_ducklake",
1552+
}
1553+
1554+
# Round-trip: the exact ConfigError from #5914 no longer fires
15281555
config = parse_connection_config(parsed)
15291556
assert isinstance(config, DuckDBConnectionConfig)
15301557
attach_sql = next(iter(config.catalogs.values())).to_sql("mre_ducklake")
@@ -1534,14 +1561,136 @@ def test_dlt_ducklake_pipeline(monkeypatch, drivername):
15341561
)
15351562

15361563

1564+
def test_dlt_ducklake_explicit_metadata_schema(monkeypatch):
1565+
import yaml
1566+
1567+
from sqlmesh.integrations import dlt as dlt_module
1568+
1569+
_stub_ducklake_dlt(monkeypatch, metadata_schema="custom_meta")
1570+
1571+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1572+
pipeline_name="mre_ducklake", dialect="duckdb"
1573+
)
1574+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1575+
lake = parsed["catalogs"]["mre_ducklake"]
1576+
assert lake["metadata_schema"] == "custom_meta"
1577+
assert lake["path"] == "/tmp/x/mre_ducklake.sqlite"
1578+
1579+
1580+
def test_dlt_ducklake_override_data_path(monkeypatch):
1581+
import yaml
1582+
1583+
from sqlmesh.integrations import dlt as dlt_module
1584+
1585+
_stub_ducklake_dlt(monkeypatch, override_data_path=True)
1586+
1587+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1588+
pipeline_name="mre_ducklake", dialect="duckdb"
1589+
)
1590+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1591+
lake = parsed["catalogs"]["mre_ducklake"]
1592+
assert lake["override_data_path"] is True
1593+
1594+
1595+
def test_dlt_ducklake_custom_name(monkeypatch):
1596+
import yaml
1597+
1598+
from sqlmesh.integrations import dlt as dlt_module
1599+
1600+
_stub_ducklake_dlt(monkeypatch, ducklake_name="my_lake")
1601+
1602+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1603+
pipeline_name="mre_ducklake", dialect="duckdb"
1604+
)
1605+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1606+
assert set(parsed["catalogs"]) == {"my_lake"}
1607+
lake = parsed["catalogs"]["my_lake"]
1608+
assert lake["metadata_schema"] == "my_lake"
1609+
1610+
1611+
def test_dlt_ducklake_yaml_inline_helper():
1612+
from sqlmesh.integrations.dlt import _yaml_inline
1613+
1614+
# Ordinary values stay byte-identical (no quotes)
1615+
assert _yaml_inline("/tmp/x/mre_ducklake.sqlite") == "/tmp/x/mre_ducklake.sqlite"
1616+
assert _yaml_inline("mre_ducklake") == "mre_ducklake"
1617+
# Pathological values are quoted single-line and round-trip
1618+
import yaml
1619+
1620+
for pathological in (
1621+
"'/tmp/quote/mre_ducklake.sqlite",
1622+
"/tmp/new\nline/mre.sqlite",
1623+
"a: b # c",
1624+
" leading-space",
1625+
):
1626+
emitted = _yaml_inline(pathological)
1627+
assert "\n" not in emitted
1628+
doc = f"connection:\n path: {emitted}\n"
1629+
assert yaml.safe_load(doc)["connection"]["path"] == pathological
1630+
1631+
1632+
@pytest.mark.parametrize(
1633+
"pathological",
1634+
["'/tmp/quote/mre_ducklake.sqlite", "/tmp/new\nline/mre.sqlite"],
1635+
ids=["leading-quote", "newline"],
1636+
)
1637+
def test_dlt_ducklake_pathological_paths_round_trip(monkeypatch, pathological):
1638+
import yaml
1639+
1640+
from sqlmesh.core.config.connection import DuckDBConnectionConfig, parse_connection_config
1641+
from sqlmesh.integrations import dlt as dlt_module
1642+
1643+
_stub_ducklake_dlt(monkeypatch, database=pathological)
1644+
1645+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1646+
pipeline_name="mre_ducklake", dialect="duckdb"
1647+
)
1648+
# Must not raise ScannerError; values must round-trip exactly
1649+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1650+
assert parsed["catalogs"]["mre_ducklake"]["path"] == pathological
1651+
config = parse_connection_config(parsed)
1652+
assert isinstance(config, DuckDBConnectionConfig)
1653+
1654+
1655+
def test_dlt_ducklake_block_coexists_with_second_catalog(monkeypatch):
1656+
import yaml
1657+
1658+
from sqlmesh.core.config.connection import DuckDBConnectionConfig, parse_connection_config
1659+
from sqlmesh.integrations import dlt as dlt_module
1660+
1661+
_stub_ducklake_dlt(monkeypatch)
1662+
1663+
_, connection_config, _ = dlt_module.generate_dlt_models_and_settings(
1664+
pipeline_name="mre_ducklake", dialect="duckdb"
1665+
)
1666+
parsed = yaml.safe_load("connection:\n" + connection_config)["connection"]
1667+
parsed["catalogs"]["other"] = {"type": "ducklake", "path": "/tmp/x/other.sqlite"}
1668+
config = parse_connection_config(parsed)
1669+
assert isinstance(config, DuckDBConnectionConfig)
1670+
assert set(config.catalogs) == {"mre_ducklake", "other"}
1671+
1672+
15371673
def test_dlt_ducklake_unsupported_catalog(monkeypatch):
15381674
from sqlmesh.integrations import dlt as dlt_module
15391675

15401676
_stub_ducklake_dlt(monkeypatch, drivername="postgres")
15411677

1542-
with pytest.raises(ClickException, match="Unsupported DuckLake catalog"):
1678+
called = {}
1679+
1680+
orig = dlt_module.format_ducklake_config
1681+
1682+
def _spy(client_config):
1683+
called["branch"] = True
1684+
return orig(client_config)
1685+
1686+
monkeypatch.setattr(dlt_module, "format_ducklake_config", _spy)
1687+
1688+
with pytest.raises(ClickException, match="Unsupported DuckLake catalog 'postgres'") as exc_info:
15431689
dlt_module.generate_dlt_models_and_settings(pipeline_name="mre_ducklake", dialect="duckdb")
15441690

1691+
assert called.get("branch") is True
1692+
assert "postgres" in str(exc_info.value)
1693+
15451694

15461695
@time_machine.travel(FREEZE_TIME)
15471696
def test_environments(runner, tmp_path):

0 commit comments

Comments
 (0)