Skip to content

Commit 8f4e72b

Browse files
committed
fix(duckdb): do not cascade table/view drops in DuckLake catalogs
DuckLake does not implement `DROP TABLE ... CASCADE` or `DROP VIEW ... CASCADE` and raises "Cascade Drop not supported in DuckLake". Since #5133 the janitor always deletes snapshot tables with cascade=True, so on a DuckLake catalog it can never reclaim space and silently reports "Cleanup complete." until a snapshot expires. Cascade support is declared per engine adapter, but on DuckDB it varies per attached catalog. Resolve the target catalog's type from duckdb_databases() and omit CASCADE for TABLE/VIEW drops in DuckLake catalogs. SCHEMA cascade is still supported by DuckLake and is left untouched, as is behaviour for native DuckDB catalogs. Fixes #6032 Signed-off-by: kingjaiteh <omarjaiteh453@gmail.com>
1 parent b1e36b9 commit 8f4e72b

2 files changed

Lines changed: 81 additions & 9 deletions

File tree

sqlmesh/core/engine_adapter/duckdb.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,8 @@ def _create_table(
173173
track_rows_processed: bool = True,
174174
**kwargs: t.Any,
175175
) -> None:
176-
catalog = self.get_current_catalog()
177-
catalog_type_tuple = self.fetchone(
178-
exp.select("type")
179-
.from_("duckdb_databases()")
180-
.where(exp.column("database_name").eq(catalog))
181-
)
182-
catalog_type = catalog_type_tuple[0] if catalog_type_tuple else None
183-
184176
partitioned_by_exps = None
185-
if catalog_type == "ducklake":
177+
if self._get_catalog_type(self.get_current_catalog()) == "ducklake":
186178
partitioned_by_exps = kwargs.pop("partitioned_by", None)
187179

188180
super()._create_table(
@@ -215,6 +207,35 @@ def _create_table(
215207
)
216208
self.execute(f"ALTER TABLE {table_name_str} SET PARTITIONED BY ({partitioned_by_str});")
217209

210+
def _drop_object(
211+
self,
212+
name: TableName | SchemaName,
213+
exists: bool = True,
214+
kind: str = "TABLE",
215+
cascade: bool = False,
216+
**drop_args: t.Any,
217+
) -> None:
218+
# DuckLake catalogs do not implement DROP TABLE / DROP VIEW ... CASCADE and raise
219+
# "Cascade Drop not supported in DuckLake". Views in DuckDB are late-binding, so
220+
# dropping the underlying table without CASCADE is safe there.
221+
if cascade and kind.upper() in ("TABLE", "VIEW"):
222+
catalog = exp.to_table(name).catalog or self.get_current_catalog()
223+
if self._get_catalog_type(catalog) == "ducklake":
224+
cascade = False
225+
226+
super()._drop_object(name=name, exists=exists, kind=kind, cascade=cascade, **drop_args)
227+
228+
def _get_catalog_type(self, catalog: t.Optional[str]) -> t.Optional[str]:
229+
"""Returns the type of the given catalog (e.g. 'duckdb', 'ducklake') as reported by duckdb_databases()."""
230+
if not catalog:
231+
return None
232+
catalog_type_tuple = self.fetchone(
233+
exp.select("type")
234+
.from_("duckdb_databases()")
235+
.where(exp.column("database_name").eq(catalog))
236+
)
237+
return catalog_type_tuple[0] if catalog_type_tuple else None
238+
218239
@property
219240
def _is_motherduck(self) -> bool:
220241
return self._extra_config.get("is_motherduck", False)

tests/core/engine_adapter/test_duckdb.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,54 @@ def test_ducklake_partitioning(adapter: EngineAdapter, duck_conn, tmp_path):
154154
f"SELECT * FROM __ducklake_metadata_{catalog}.main.ducklake_partition_info"
155155
).fetchdf()
156156
assert partition_info.shape[0] == 1
157+
158+
159+
def test_drop_table_ducklake_no_cascade(adapter: EngineAdapter, duck_conn, tmp_path):
160+
# DuckLake does not implement DROP TABLE/VIEW ... CASCADE, so the adapter must
161+
# omit CASCADE for objects in a DuckLake catalog while keeping it for native catalogs.
162+
catalog = "a_ducklake_db"
163+
164+
duck_conn.install_extension("ducklake")
165+
duck_conn.load_extension("ducklake")
166+
duck_conn.execute(
167+
f"ATTACH 'ducklake:{tmp_path}/{catalog}.ducklake' AS {catalog} (DATA_PATH '{tmp_path}');"
168+
)
169+
170+
duck_conn.execute(f"CREATE SCHEMA {catalog}.phys")
171+
duck_conn.execute(f"CREATE SCHEMA {catalog}.virt")
172+
duck_conn.execute(f"CREATE TABLE {catalog}.phys.t (i INTEGER)")
173+
duck_conn.execute(f"CREATE VIEW {catalog}.virt.v AS SELECT * FROM {catalog}.phys.t")
174+
175+
# native catalog, cascade is passed through
176+
duck_conn.execute("CREATE TABLE memory.main.native_t (i INTEGER)")
177+
duck_conn.execute("CREATE VIEW memory.main.native_v AS SELECT * FROM memory.main.native_t")
178+
179+
adapter.drop_table(f"{catalog}.phys.t", cascade=True)
180+
adapter.drop_view(f"{catalog}.virt.v", cascade=True)
181+
adapter.drop_table("memory.main.native_t", cascade=True)
182+
adapter.drop_view("memory.main.native_v", cascade=True)
183+
184+
assert not adapter.table_exists(f"{catalog}.phys.t")
185+
assert not adapter.table_exists(f"{catalog}.virt.v")
186+
assert not adapter.table_exists("memory.main.native_t")
187+
assert not adapter.table_exists("memory.main.native_v")
188+
189+
190+
def test_drop_object_cascade_by_catalog_type(make_mocked_engine_adapter: t.Callable):
191+
adapter = make_mocked_engine_adapter(DuckDBEngineAdapter)
192+
adapter.fetchone = lambda *_args, **_kwargs: ("ducklake",) # type: ignore
193+
194+
adapter.drop_table("lake.phys.t", cascade=True)
195+
adapter.drop_view("lake.virt.v", cascade=True)
196+
# schema cascade is supported by DuckLake and must be preserved
197+
adapter.drop_schema("lake.virt", cascade=True)
198+
199+
adapter.fetchone = lambda *_args, **_kwargs: ("duckdb",) # type: ignore
200+
adapter.drop_table("native.phys.t", cascade=True)
201+
202+
assert to_sql_calls(adapter) == [
203+
'DROP TABLE IF EXISTS "lake"."phys"."t"',
204+
'DROP VIEW IF EXISTS "lake"."virt"."v"',
205+
'DROP SCHEMA IF EXISTS "lake"."virt" CASCADE',
206+
'DROP TABLE IF EXISTS "native"."phys"."t" CASCADE',
207+
]

0 commit comments

Comments
 (0)