Repository navigation
fix(verify): resolve declared grain columns to the relation's spelling - #533
Merged
marcociav-exmergo merged 2 commits intoOct 12, 2026
Merged
Conversation
…g; a refused scan is a note Both halves of exmergo#483, in maintain/verify.py. The declared column is resolved against the relation's own columns before it reaches the adapter. `_declared_grain_findings` already fetched them from `table_metadata` and discarded them. Each declared column is now matched lowercase to a live column, the comparison `column_contract_findings` already makes, and the adapter is handed the relation's spelling, so Snowflake sees COUNT(DISTINCT "ID") against a relation holding ID. The finding keeps the declared spelling: it speaks about what the model declares. A declared column the relation does not have is named in `notes` and never sent. A statement the warehouse refuses stays inside the grain class. The WarehouseQueryError left grain_findings with no handler in _grain_contract, so one refused COUNT(DISTINCT) made the whole command answer `error`. It is caught per model now: that model is named in `notes` with the server's own words, and every other model's grain and every other finding class still run. The same handler covers the heuristic escalation, which runs the same statement. Five tests on the fake grain adapter fail against main's verify.py and pass here; the rest of tests/maintain and the safety spine's verify, grain and maintain selections pass unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
marcociav-exmergo
approved these changes
Oct 12, 2026
marcociav-exmergo
left a comment
Member
There was a problem hiding this comment.
LGTM! Thanks @catincloudlabs
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.
What this changes
Both halves of #483, in
maintain/verify.pyand its tests only. The offer on the issue says where each half sits; this takes the one decision it named at the default proposed there.The declared column is resolved against the relation's own columns before it reaches the adapter.
_declared_grain_findingsalready fetched them fromtable_metadataand discarded them as_columns. Each declared column is now matched lowercase to a live column, the comparisoncolumn_contract_findingsalready makes a few hundred lines above, and the adapter is handed the relation's spelling. So on Snowflake,- name: idwith auniquetest now producesCOUNT(DISTINCT "ID")against a relation holdingID, which is what the issue's statement should have been. The finding keeps the declared spelling, because it speaks about what the model declares;data.columnsis unchanged for every project that never hit this.A statement the warehouse refuses stays inside the grain class. The
WarehouseQueryErrorleftgrain_findings,_grain_contracthas no handler, and the command answerederrorfor everything. It is now caught per model ingrain_findings: that model is named innoteswith the server's own words, the other models' grains are still checked, and the other finding classes still run. The handler sits around the heuristic escalation too, since it issues the same statement.The decision taken. A declared column the relation does not have (a typo in
schema.yml, or a column dropped since it was declared) is skipped before quoting and named innotes, not sent to the warehouse to fail there, and not a finding: on a dbt project the column-contract class already reports a missing declared column athigh. Yours to change.Behavior changes
maintain verify's grainnotes:the declared grain on <model> names a column the relation does not have, so it was not checked: <columns>the grain could not be measured for <model>: the warehouse refused the distinct-count statement (<the server's words>)error. Nothing else about the envelope moves._declared_grain_findingsreturns a third element, the unresolved columns. It is private to the module.On a connector whose folding matches dbt's spelling, nothing changes except that the two notes exist.
Shown failing without the fix
Five tests on the existing
_GrainAdapterfake intests/maintain/test_verify.py: a lowercase single-column declaration against an upper-cased relation (with duplicates, so the finding's spelling is asserted too), the composite equivalent, a declared column the relation lacks on an adapter that asserts it was never asked to scan, a refusal on one of two models (the other's finding still arrives, the refused one's note quotes the server), and a refusal during the heuristic escalation.With
verify.pychecked out frommainand the tests from this branch:With this branch's
verify.py, the whole file:and
tests/maintainplus the safety spine'sverify,grainandmaintainselections:ruff checkandruff format --checkare clean on both files.Files
packages/dex-core/src/exmergo_dex_core/maintain/verify.py: the resolution in_declared_grain_findings, the per-model handler and two notes ingrain_findings, one import.packages/dex-core/tests/maintain/test_verify.py: the five tests.CHANGELOG.md: two Fixed entries under Unreleased.🤖 Generated with Claude Code