Summary
sqlmesh janitor can never reclaim space on a DuckLake catalog. SnapshotEvaluator forces
cascade=True when deleting a snapshot's tables, and DuckLake does not implement cascade drops:
Not implemented Error: Cascade Drop not supported in DuckLake
The cause is a granularity mismatch: cascade support is declared per engine adapter, but on
DuckDB it varies per catalog. A plain DuckDB catalog supports DROP TABLE ... CASCADE; a
DuckLake catalog attached in the same connection does not.
This fails quietly. sqlmesh janitor returns Cleanup complete. having deleted nothing while
no snapshot has yet reached its TTL, so a DuckLake project looks healthy and then silently stops
reclaiming space once snapshots start expiring. Only --ignore-ttl surfaces it immediately.
Reproduction
import duckdb
con = duckdb.connect(); con.execute("INSTALL ducklake; LOAD ducklake")
con.execute("ATTACH 'ducklake:t.ducklake' AS lk (DATA_PATH 'storage/')")
con.execute("CREATE SCHEMA lk.phys; CREATE SCHEMA lk.virt")
con.execute("CREATE TABLE lk.phys.t (i INTEGER); INSERT INTO lk.phys.t VALUES (1)")
con.execute("CREATE VIEW lk.virt.v AS SELECT * FROM lk.phys.t") # as the virtual layer builds
con.execute("DROP TABLE lk.phys.t CASCADE") # FAIL — Cascade Drop not supported in DuckLake
con.execute("DROP TABLE lk.phys.t") # OK — the dependent view does NOT block it
con.execute("DROP SCHEMA lk.virt CASCADE") # OK — schema cascade is supported
In a real project, sqlmesh janitor --ignore-ttl gives:
Cleanup failed!
Error: Execution failed for node SnapshotId<"unlisted"."du"."prefix_map": 259425956>
caused by: _duckdb.NotImplementedException:
Not implemented Error: Cascade Drop not supported in DuckLake
Root cause
sqlmesh/core/snapshot/evaluator.py:1366 (0.236.1):
evaluation_strategy.delete(
table_name,
...
# we need to set cascade=true or we will get a 'cant drop because other objects depend on it'-style
# error on engines that enforce referential integrity, such as Postgres
cascade=True,
)
sqlmesh/core/engine_adapter/duckdb.py:39:
SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA", "TABLE", "VIEW"]
This looks like collateral from #4767 ("Janitor fails to delete object with dependencies in
Postgres"), which is a genuine Postgres problem — but DuckDB does not enforce referential
integrity, so TABLE/VIEW cascade was never needed on this engine, and declaring it breaks
DuckLake catalogs. The reproduction above shows the dependent view does not block a plain
DROP TABLE.
Suggested fix
Make the capability catalog-aware rather than adapter-wide. The adapter already has this exact
pattern at duckdb.py:176 in _create_table:
catalog_type_tuple = self.fetchone(
exp.select("type").from_("duckdb_databases()")
.where(exp.column("database_name").eq(catalog))
)
catalog_type = catalog_type_tuple[0] if catalog_type_tuple else None
if catalog_type == "ducklake":
...
So an _drop_object override on DuckDBEngineAdapter that drops cascade for TABLE/VIEW
when the target's catalog type is ducklake would fix it, keeping SCHEMA (which works, and
which environment cleanup relies on).
One caveat: the existing pattern resolves get_current_catalog(), which is not right for
_drop_object — it receives a fully-qualified name whose catalog may differ from the session's,
so the catalog should be resolved from the table name.
Workaround
Setting DuckDBEngineAdapter.SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"] before building a
Context makes the built-in janitor work correctly. On a real project this dropped 78 physical
tables and reclaimed 3.64 GB, after which plan --skip-backfill reported no changes and all
blocking audits still passed. It is blunter than the fix above — it disables cascade for plain
DuckDB catalogs too — but harmless for the reason given.
Environment
- sqlmesh 0.233.0 and 0.236.1 (verified the declaration and
cascade=True are unchanged in both)
- duckdb 1.5.5, ducklake extension
d8a1881e (DuckLake format 1.0)
- macOS 15, Python 3.11 / 3.12
Summary
sqlmesh janitorcan never reclaim space on a DuckLake catalog.SnapshotEvaluatorforcescascade=Truewhen deleting a snapshot's tables, and DuckLake does not implement cascade drops:The cause is a granularity mismatch: cascade support is declared per engine adapter, but on
DuckDB it varies per catalog. A plain DuckDB catalog supports
DROP TABLE ... CASCADE; aDuckLake catalog attached in the same connection does not.
This fails quietly.
sqlmesh janitorreturnsCleanup complete.having deleted nothing whileno snapshot has yet reached its TTL, so a DuckLake project looks healthy and then silently stops
reclaiming space once snapshots start expiring. Only
--ignore-ttlsurfaces it immediately.Reproduction
In a real project,
sqlmesh janitor --ignore-ttlgives:Root cause
sqlmesh/core/snapshot/evaluator.py:1366(0.236.1):sqlmesh/core/engine_adapter/duckdb.py:39:This looks like collateral from #4767 ("Janitor fails to delete object with dependencies in
Postgres"), which is a genuine Postgres problem — but DuckDB does not enforce referential
integrity, so
TABLE/VIEWcascade was never needed on this engine, and declaring it breaksDuckLake catalogs. The reproduction above shows the dependent view does not block a plain
DROP TABLE.Suggested fix
Make the capability catalog-aware rather than adapter-wide. The adapter already has this exact
pattern at
duckdb.py:176in_create_table:So an
_drop_objectoverride onDuckDBEngineAdapterthat dropscascadeforTABLE/VIEWwhen the target's catalog type is
ducklakewould fix it, keepingSCHEMA(which works, andwhich environment cleanup relies on).
One caveat: the existing pattern resolves
get_current_catalog(), which is not right for_drop_object— it receives a fully-qualified name whose catalog may differ from the session's,so the catalog should be resolved from the table name.
Workaround
Setting
DuckDBEngineAdapter.SUPPORTED_DROP_CASCADE_OBJECT_KINDS = ["SCHEMA"]before building aContextmakes the built-in janitor work correctly. On a real project this dropped 78 physicaltables and reclaimed 3.64 GB, after which
plan --skip-backfillreported no changes and allblocking audits still passed. It is blunter than the fix above — it disables cascade for plain
DuckDB catalogs too — but harmless for the reason given.
Environment
cascade=Trueare unchanged in both)d8a1881e(DuckLake format 1.0)