Skip to content

fix(validation): resolve PK identity-raise vs check-report tension (item 8j) - #297

Merged
discreteds merged 3 commits into
developfrom
bugfix/pk-identity-vs-check-tension
Aug 18, 2026
Merged

fix(validation): resolve PK identity-raise vs check-report tension (item 8j)#297
discreteds merged 3 commits into
developfrom
bugfix/pk-identity-vs-check-tension

Conversation

@discreteds

@discreteds discreteds commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Closes backlog item 8j: resolves the spec-level tension between §7's keyed-identity raise-by-default precondition and acceptance-16's primary_key_unique check (meant to report the same duplicate/null-key condition as a validation outcome), plus a BaseDataContract.to_checks() vs compile_datacontract() asymmetry.

  • Task 1 (datacontracts/contract.py): BaseDataContract.to_checks() now emits primary_key_unique (reuses the existing primary_key_check(spec) helper) — fixes the asymmetry where a TypeSpec and an equivalent BaseDataContract subclass validated a resource differently.
  • Task 2 (datacontracts/contract.py): allow_imperfect_key threaded through validate_datacontract/validate_datacontract_quick, restoring the already-shipped Validator.validate escape hatch at this entry point. Docstring corrected — it claimed "never raises", which was false for keyed-identity failures.
  • Task 3 (relations/dag/dag.py, relations/dag/validation.py, validation/runner.py): allow_imperfect_key threaded through RelationDAG.validate/validate_quick. dag.validate/validate_quick now never raise IdentityInvalidError out of the call — a resource's invalid keyed identity is isolated into that resource's own ValidationResult (check_id="__identity__", status="error"), every other resource in the batch still validates and reports normally. Isolation is categorical across every IdentityInvalidError cause (including the unconditional missing-key-fields raise).
  • Task 4: Amended mountainash-central's 2026-07-03 validation spec status line with a pointer to this item's design amendment.

Also updates item 8l's test_duplicate_primary_key_raises_identity_errortest_duplicate_primary_key_isolates_identity_failure (tests/relations/dag/cross_backend/test_datapackage_validation_loop.py): that test exercised exactly the pre-8j DAG-tier raise behavior this item deliberately supersedes per spec §3.2.

Unblocks item 8k (syntheticdata round-trip), which is gated on primary_key_unique reporting for real.

Verification

  • hatch run ruff:check — clean.
  • hatch run mypy:check — zero new errors (98 pre-existing, none on touched files).
  • hatch run test:test-quick23005 passed / 0 failed (develop baseline 22991 + 14 new cases, exactly matching the plan's predicted count), 81 skipped, 3465 xfailed (pre-existing baselines unchanged).

Spec: mountainash-central/04.planning/mountainash/superpowers/specs/2026-08-17-pk-identity-vs-check-tension-design.md (rev 3)
Plan: mountainash-central/04.planning/mountainash/superpowers/plans/2026-08-17-pk-identity-vs-check-tension.md (rev 2)

…sk 1)

BaseDataContract.to_checks() never emitted primary_key_unique - only
compile_datacontract(TypeSpec) did, so a TypeSpec and an equivalent
BaseDataContract subclass validated a resource differently inside
dag.validate (spec §4.1, Fact C). Reuses the existing, already-tested
primary_key_check(spec) helper; cls.to_typespec() already collapses
natural_key into primary_key, so this fires for both declaration
styles with no new logic.
…ontract* (item 8j, task 2)

BaseDataContract.validate_datacontract/validate_datacontract_quick had
no way to reach the already-shipped, already-tested
Validator.validate/allow_imperfect_key escape hatch (spec §4.2, Fact
A) - a declared primary_key/natural_key always raised
IdentityInvalidError on a null/duplicate key before checks ran, with
no way to opt into primary_key_unique reporting instead. Also
corrects validate_datacontract's docstring, which claimed "never
raises" - false for keyed-identity failures.
…per-resource identity isolation (item 8j, task 3)

dag.validate/validate_quick had no allow_imperfect_key parameter, so
every declared keyed identity always raised IdentityInvalidError deep
inside validate_relation - and that raise escaped the per-resource
loop uncaught, aborting validation of every resource after the
failing one in the batch (spec §4.3, Fact B).

RelationDAG.validate/validate_quick -> relations/dag/validation.py's
validate/validate_quick/_run -> ValidationRunner.validate_dag now
thread allow_imperfect_key end to end. validate_dag's per-resource
loop wraps validate_relation in try/except IdentityInvalidError and
isolates the failure into that resource's own ValidationResult
(check_id="__identity__", status="error") - never a raised exception,
never an aborted batch (spec §3.2). Isolation is categorical across
every IdentityInvalidError cause, including the missing-key-fields
raise that allow_imperfect_key never suppresses (spec §7 round-2 fix).

Updates item 8l's test_duplicate_primary_key_raises_identity_error
(tests/relations/dag/cross_backend/test_datapackage_validation_loop.py)
to test_duplicate_primary_key_isolates_identity_failure, asserting the
new isolated-result contract instead of a raise - the old assertion
exercised exactly the DAG-tier behavior this task supersedes.
@discreteds
discreteds merged commit 257cd4a into develop Aug 18, 2026
3 of 4 checks passed
discreteds added a commit that referenced this pull request Aug 18, 2026
…-11) (#299)

Root cause (PR #297 CI investigation): DuckDB gives no row-order
guarantee for ASOF JOIN output absent an explicit ORDER BY. Ibis's
join_asof backend (relsys_ib_ext_ma_util.py) never applied one, so
test_asof_backward_strategy[ibis-duckdb] flipped strict-xfail/XPASS
~50/50 across independent runs (confirmed via 8 isolated process
reruns pre/post fix). Values were always correct; only row order
backing the test's positional assertions flapped. Same incident class
as backlog item 101 (PR #293, 2026-08-16, flagged but not fixed then).

Fix: order the join_asof result by the join key(s) before returning,
matching polars/narwhals' left-order preservation for asof joins.
Deterministic across 8/8 verification runs post-fix.

IB-REL-11 split: ibis-duckdb portion closed resolved_in_mountainash;
distinct, still-genuine ibis-sqlite raise (no ASOF JOIN translation)
carved out to new IB-REL-13. Updated divergences.py, upstream-issues.yaml,
known-divergences.md, and the test's xfail marker in lockstep; regenerated
expression-coverage.{md,json} and _spine_expectation_census.md.

AGENTS.md: removed the stale 'local Ibis fork' dependency note — CI/dev
both resolve ibis-framework from PyPI, no path dependency exists.

Backlog: filed mountainash-central item 108 for a distinct, unrelated
latent bug found in the same function (join_asof silently drops the
strategy= parameter on Ibis — forward/nearest behave as backward) and
closed item 101's flagged sub-issue.
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