feat: strict mode — test each Trouve before it is published - #12
Draft
OmerBaddour wants to merge 2 commits into
Draft
feat: strict mode — test each Trouve before it is published#12OmerBaddour wants to merge 2 commits into
OmerBaddour wants to merge 2 commits into
Conversation
A table can only be tested once it has been materialized, so a normal run writes production first and finds out afterwards. `clair run --strict` closes that window. Per Trouve: - Build into `<table>__clair_strict_<run_id>`, a sibling in the same schema (incremental Trouves first zero-copy CLONE the target so the INSERT/MERGE has a base to apply to). - Run the Trouve's tests against that staging object. - On pass, promote: `ALTER TABLE ... SWAP WITH ...` for tables, which is metadata-only and does not scale with table size; `CREATE OR REPLACE VIEW` for views. A target that does not exist yet is RENAMEd into place instead. - On fail, drop the staging object. The target is untouched, the node is reported as FAILED, and its dependents are skipped. Promotion happens per node, immediately after its tests, so dependents always read their upstreams under the real names their SQL references. `clair compile --strict` writes the same plan to _clairtifacts/ for review. `--strict` is rejected alongside `--no-test`, since it gates on tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against a live Snowflake account that ALTER TABLE ... SWAP WITH attaches grants to the object rather than the name: after a swap, the production name carries only the staging object's grants and any privilege granted directly on the target is silently revoked. Promotion is now: CREATE OR REPLACE TABLE <target> CLONE <staging> COPY GRANTS which is equally metadata-only, copies every privilege except OWNERSHIP from the object being replaced, and -- because COPY GRANTS falls back to the clone source when there is no object to replace -- removes the SWAP-vs-RENAME branch entirely. Views get CREATE OR REPLACE VIEW ... COPY GRANTS, which fixes the same defect on the view path. Also: - Staging objects are no longer dropped on failure. A rejected candidate is the only copy of what the run produced and reproducing it means re-running everything upstream, so it is retained and its name reported in the error. Staging is dropped only after a successful promotion. - Shorten the suffix from __clair_strict_ to __clair_ (47 -> 40 chars of identifier budget). - Record at the length check that Snowflake's 255-char limit applies per object name, not per fully-qualified path -- a 767-char database.schema.table is legal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A table can only be tested once it has been materialized. In a normal
clair runthat means the target is written first and the tests tell you afterwards — the table is already wrong, and anything reading it has already read the wrong numbers.What
clair run --strictbuilds each Trouve into a run-scoped staging object, tests it there, and only then publishes it.Per node:
<table>__clair_strict_<run_id>, a sibling object in the same database and schema.ALTER TABLE ... SWAP WITH ...for tables (metadata-only; cost does not scale with table size), then drop the superseded copy. Views are promoted withCREATE OR REPLACE VIEW. A target that does not exist yet isRENAMEd into place instead of swapped.FAILED, and its dependents are skipped.Promotion happens per node, immediately after that node's tests and before the next node starts, so dependents always read their upstreams under the real names their SQL references. Nothing in user-written SQL ever mentions a staging name.
Incremental Trouves
An incremental build needs its prior state before
INSERT/MERGEcan apply. clair seeds the staging table withCREATE OR REPLACE TABLE ... CLONE ...— Snowflake clones are metadata-only, so this is constant-time regardless of target size. If the target does not exist, the existing full-refresh fallback kicks in and the clone is skipped.Also
clair compile --strictwrites the full plan (clone, staging build, test checkpoint, promotion) to_clairtifacts/for review without a connection.--strictis rejected alongside--no-test— the whole point is to gate publishing on tests.Failure modes
Implementation notes
clair.core.strictholds the naming and SQL construction; the runner orchestrates.Trouve.build_sql()gained an optionaltarget_nameoverride. References to upstream Trouves are untouched — only the object being written changes. The UPSERT merge-staging table still derives from the real name, so the two suffixes never stack.run_tests()gained an optionalphysical_namesmap so tests can query the staging object while results still report the routed name.--samplefollows the override.after_node_successnow takes(node_name, physical_name). Non-strict behaviour is unchanged: the target is written directly, tests run afterwards, and a failing test cuts off dependents without failing the node itself.Testing
555 tests pass (520 existing, 35 new in
tests/unit/test_strict.py) — covering staging naming and its limits, promote/rename/view/cleanup SQL, the target-name override, and runner behaviour across full refresh, incremental (clone ordering and fallback), views, pandas Trouves, failed builds, failed tests, failed promotion, downstream skipping, and the non-strict path.Verified end to end against
example_projects/example_2(full refresh) andexample_3(append + upsert) viaclair compile --strict. Not yet exercised against a live Snowflake account — the SWAP/CLONE/RENAME statements are unit-tested through a mock adapter, so a real run is worth doing before merge.🤖 Generated with Claude Code