Skip to content

feat(janitor): archive stale relations before deletion in direct mode - #809

Merged
kvlonge merged 8 commits into
mainfrom
feat/janitor-archive-before-delete
Sep 24, 2026
Merged

kvlonge merged 8 commits into
mainfrom
feat/janitor-archive-before-delete

Conversation

@kvlonge

@kvlonge kvlonge commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Why

In direct mode the janitor only reported stale relations, so cleanup was manual and a dropped relation was hard to recover. It also found nothing on case-folding warehouses: schema keys from the catalog and the project differed in case, and no adapter reported relation age, so any retention_days above 0 skipped every relation.

Changes

  • Direct mode renames stale relations in place to _sqb_archive__<UTC timestamp>__<name>, then drops archives older than archive_retention_days. Deletion is decided by strict name grammar, embedded timestamp and managed schema only. Look-alike names are reported and never touched.
  • One append-only _sqlbuild_janitor_events row per archive and delete, with deterministic IDs. It is never read for decisions.
  • Defaults: retention_days 14 in direct mode (30 in virtual mode when unset), archive_retention_days 14, and 0/0 allowed.
  • Direct mode acts only on plain lowercase names. Both modes skip case-colliding names, and exclude patterns ignore case.
  • Janitor relation keys compare case-insensitively, so Snowflake-style catalogs are scanned correctly.
  • Relation age for Snowflake, Databricks (DESCRIBE DETAIL for tables) and BigQuery, fetched only for candidates and never at retention 0.
  • New render_rename_view adapter hook; BigQuery rejects view renames.
  • Shared fit_artifact_logical_name. The disabled-janitor guidance now shows TOML.

Verification

  • make test and make check-ci pass locally; fensu check reports 0 faults.
  • New unit, integration and DuckDB e2e tests cover archiving, expiry, 0/0, look-alikes, long names, audit failure and re-runs, case folding, age sources and virtual-mode safety.
  • Manual Snowflake run against a real project: 12 relations archived with matching row counts, re-run idempotent, only expired strict archives deleted, look-alikes left alone, UNDROP and manual restore worked.

Direct-mode janitor now renames eligible stale relations in place to
_SQB_ARCHIVE__<UTC timestamp>__<fitted name> after retention_days, and drops
relations in managed schemas whose names strictly match the archive grammar
once their embedded timestamp is older than archive_retention_days.
Malformed look-alike names are reported and never deleted. Deletion
decisions use only the name grammar, the embedded timestamp, and managed
schema scope.

Each archive and delete appends a deterministic, idempotent row to the new
_sqlbuild_janitor_events audit table, which is never read for decisions.
retention_days now defaults to 14 and the new archive_retention_days
defaults to 14; 0/0 archives and deletes in one run.

Identifier fitting moves to a shared adapter relation helper, reused by
scenario artifacts and archive names. Virtual-environment janitor behaviour
is unchanged.
… retention default

Direct-mode janitor now renames or drops a relation only when its catalog
name is a plain lowercase identifier that does not collide case-insensitively
with another relation in the same schema listing. Other names are reported as
skipped and never touched. Archive names are generated in case-folded form so
the archives the janitor creates always pass this guard.

An unset [janitor] retention_days now resolves to 14 days in direct mode and
keeps the 30-day default in virtual-environment mode; explicit values and the
--retention-days override apply to both modes as before.
…catalog identifiers

Listed relations are matched to target schemas case-insensitively and respelled with the project's schema spelling, so warehouses that fold unquoted identifiers (for example lowercase listings of uppercase schemas) produce janitor candidates. Relation keys, source-schema overlap detection, and scan-schema collection compare case-insensitively while rendering keeps an addressable spelling.
Snowflake relation listings already read created and last_altered for tables and views. The adapter now reports age metadata support and normalizes both timestamps to UTC so retention-based janitor cleanup can evaluate relation age.
Databricks relation listings read created and last_altered from information_schema.tables. BigQuery keeps its list_tables listing and adds an on-demand with_relation_age_metadata hook that reads creation and last-modified times from one __TABLES__ query per dataset; janitor planning calls it, so other commands pay no extra query jobs. Catalog timestamps share one UTC normalizer. SQL Server, Postgres, DuckDB, and MotherDuck document why they do not report relation age.
…ite times on Databricks

Janitor classification now asks the adapter for age metadata only for relations that survive the desired, tracked, exclude, protection, and addressing filters, in both direct and virtual mode. Databricks information_schema last_altered tracks DDL only, so non-view relations no longer take ages from it; the Databricks hook reads createdAt and lastModified from DESCRIBE DETAIL per candidate and leaves the age unknown when that fails. Views keep their information_schema timestamps.
…eanup metadata-free

Folded relation identity made tracked-only membership case-insensitive, so virtual mode could drop an excluded lowercase table through an untracked quoted twin. Virtual mode now applies the case-collision guard, and exclude patterns match case-insensitively in both modes. BigQuery age metadata is keyed by the exact case-sensitive table ID. Janitor no longer reads relation ages when retention_days is 0.
@kvlonge
kvlonge enabled auto-merge (squash) September 24, 2026 19:11
@kvlonge
kvlonge merged commit 693e580 into main Sep 24, 2026
28 checks passed
@kvlonge
kvlonge deleted the feat/janitor-archive-before-delete branch September 24, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant