Skip to content

fix(verify): resolve declared grain columns to the relation's spelling - #533

Merged
marcociav-exmergo merged 2 commits into
exmergo:mainfrom
catincloudlabs:verify-grain-column-case
Oct 12, 2026
Merged

marcociav-exmergo merged 2 commits into
exmergo:mainfrom
catincloudlabs:verify-grain-column-case

Conversation

@catincloudlabs

Copy link
Copy Markdown
Contributor

What this changes

Both halves of #483, in maintain/verify.py and 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.

  1. 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 as _columns. Each declared column is now matched lowercase to a live column, the comparison column_contract_findings already makes a few hundred lines above, and the adapter is handed the relation's spelling. So on Snowflake, - name: id with a unique test now produces COUNT(DISTINCT "ID") against a relation holding ID, 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.columns is unchanged for every project that never hit this.

  2. A statement the warehouse refuses stays inside the grain class. The WarehouseQueryError left grain_findings, _grain_contract has no handler, and the command answered error for everything. It is now caught per model in grain_findings: that model is named in notes with 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 in notes, 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 at high. Yours to change.

Behavior changes

  • Two new sentences can appear in maintain verify's grain notes:
    • 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>)
  • A refused grain statement no longer makes the command answer error. Nothing else about the envelope moves.
  • An adapter that reports no column metadata at all gets the declared spelling, as before; there is nothing to resolve against.
  • _declared_grain_findings returns 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 _GrainAdapter fake in tests/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.py checked out from main and the tests from this branch:

5 failed, 123 deselected

With this branch's verify.py, the whole file:

128 passed

and tests/maintain plus the safety spine's verify, grain and maintain selections:

360 passed, 5 skipped, 228 deselected

ruff check and ruff format --check are 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 in grain_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

…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>
@catincloudlabs catincloudlabs changed the title fix(verify): resolve declared grain columns to the relation's spelling; a refused scan is a note fix(verify): resolve declared grain columns to the relation's spelling Oct 11, 2026

@marcociav-exmergo marcociav-exmergo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks @catincloudlabs

@marcociav-exmergo
marcociav-exmergo merged commit 9b845bc into exmergo:main Oct 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants