Skip to content

#196 — subquery-aware decomposition v1: NOT EXISTS / NOT IN anti-join arms - #229

Merged
cmbays merged 5 commits into
mainfrom
domain-196-subquery-anti-join-evidence
Jun 11, 2026
Merged

cmbays merged 5 commits into
mainfrom
domain-196-subquery-anti-join-evidence

Conversation

@cmbays

@cmbays cmbays commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Design summary

The correlated-subquery evidence family (v1) + its first consumers: join.anti-join gains NOT EXISTS and NOT IN detection arms with the same supersedes + inverted-recommendation + covered/uncovered/UNKNOWN semantics as the shipped LEFT JOIN + IS NULL form.

  • Sibling subquery_facts family, never a join-fact lie: SubqueryFact { kind ∈ {NotExists, NotIn} (#[non_exhaustive]), consumer, inner_leaf, equi_keys: Vec<JoinKeyPair> } rides CteGraph behind #[serde(skip)] — the #172 — check union.arm-coverage: unexercised UNION ALL arms #191/renderer hygiene: fold post-merge bot findings on PR #38 into the next render-layer touch #40 additive pattern. The embedded report payload is byte-identical to pre-capability: subquery-aware model decomposition — correlated-subquery evidence family for the check engine #196 (wire-isolation test mirrors left_join_facts_never_reach_the_wire). Normalizing subqueries into LeftJoinFact was the rejected alternative.
  • No second parse: collect_subquery_facts rides the engine's existing single sqlparser pass, invoked from the exact call sites as the LEFT JOIN collection (every CTE body + the terminal body, WITH-less models included). Extraction reads only top-level WHERE AND conjuncts (collect_and_conjuncts reuse).
  • Verdict-machinery reuse: bind_keys / key_match_verdict generalize over an internal KeyedJoinView constructed from both &LeftJoinFact and &SubqueryFact (the subquery's inner relation plays the LEFT JOIN's right-leaf role; pairs are normalized OUTER-side-left). The LEFT JOIN path is byte-identical — every pre-existing join test pins it unchanged. The subquery arms inherit closure binding (resolve_side_external), value-normalized key matching, attribution, and the inverted matching-row sketch for free.
  • join.left-null-propagation never enumerates subquery constructs (it consumes left_join_facts only) — verified by test, not assumed — so per-construct supersedes stays trivially correct.

Tier honesty (HIGH, cues never assertions)

Form Outcome Pinned by
Correlated NOT EXISTS, single plain-table inner, resolvable outer↔inner equi keys Detected — not_exists[{consumer}:{leaf}], inverted sketch, matched-pair satisfaction engine + detector + not_exists_anti_join_fires_through_the_real_engine (the FLIPPED #173 pin) + BDD
Single-column NOT IN over a plain-table inner Detected — not_in[{consumer}:{leaf}], membership pair as the key not_in_anti_join_fires_through_the_real_engine (new specimen) + BDD
Correlated NOT EXISTS with no resolvable equi pair (non-equi / OR-nested keys) UNKNOWN, never UNCOVERED (fact with empty keys → bind fails) correlated_but_non_equi_not_exists_yields_a_fact_with_empty_keys + real-engine verdict test
NOT IN with an unresolvable outer column (multi-relation FROM, unqualified col; unknown qualifier) UNKNOWN, never UNCOVERED not_in_unresolvable_outer_column_yields_empty_keys + real-engine verdict test
Uncorrelated NOT EXISTS Silent (not a keyed anti-join) uncorrelated_not_exists_emits_no_fact + real-engine residual pin
Non-negated EXISTS / IN (semi-join, membership) Silent (future consumers — no dead variants) non_negated_exists_and_in_emit_no_facts + real-engine residual pin
OR-branch / HAVING / JOIN-ON / projection positions Silent or_branch_not_exists_emits_no_fact + real-engine residual pin
Derived-table / multi-table / joined / WITH-bearing inners Silent derived_table_inner_emits_no_fact, multi_table_inner_emits_no_fact
Multi-column / expression NOT IN projections; non-column outer exprs Silent not_in_multi_column_or_expression_projection_emits_no_fact

NOT IN NULL-trap honesty: the spec prose states that a NULL in the inner column makes NOT IN yield no rows; detection still treats the construct as the anti-join idiom (that is how it is authored), and the matched-row fixture the check recommends is exactly what surfaces the trap. Stated, never silently ignored.

Candidate further consumers (enumerated per the issue AC)

  1. IN-subquery membership (col IN (SELECT …)) — semi-join inclusion; satisfaction needs both a member and a non-member given row.
  2. EXISTS semi-join (non-negated, correlated) — same key binding, Match direction with the inverted risk class vs anti-join.
  3. Scalar-subquery aggregates ((SELECT max(…) …) in projection/predicate) — pairs with the framing doc's literal-predicate evaluator zone.
  4. Satisfaction detection over subquery arms — which given shapes exercise the inner branch (the issue's named possible sub-issue).

All arrive as additive SubqueryKind variants + consumers — never an engine rewrite.

Restoration-sites ledger (the greppable-exclusion discipline)

Dogfood (the #164 precedent)

  • Golden (UNCOVERED): tests/fixtures/playground-current.json gains int_patients__never_admitted — a correlated NOT EXISTS over stg_synthea__encounters, no unit test (programmatic single-line JSON splice; ensure-ascii round-trip verified lossless before splicing; MANIFEST.toml sha + provenance prose updated). The golden playground report now renders not_exists[(final select):stg_synthea__encounters] UNCOVERED with the inverted sketch; a real-fixture test pins it. jaffle-shop + diff-showcase goldens are byte-identical (no anti-join spec embeds there); playground + explore regenerated with the exact CI commands and audited at the JSON level — every delta traces to the new model card, the join.anti-join check_specs entry, or manifest_nodes.
  • Live (Covered): dbt-project/models/marts/orders_never_refunded.sql (NOT IN over stg_refunds) + a unit test whose givens carry a matching pair (order 1 IS refunded) with expect proving the exclusion. Verified end-to-end against a real fusion-compiled manifest (dbt-fusion 2.0.0-preview.177): the payload carries join.anti-join | not_in[(final select):stg_refunds] | covered attributed to the new test. The sticky PR preview's dbt-project row renders the satisfied twin live.

Gates (run directly — lefthook skips in fresh worktrees)

fmt ✓ · clippy --all-targets --locked -D warnings exit 0 ✓ · nextest 1222/1222 ✓ · BDD 160 scenarios / 1032 steps ✓ (feature count stays 22) · heuristics_ledger byte-gate ✓ · headless zero-egress (--ignored) ✓ · headless toggle (--ignored) ✓ · cargo doc -D warnings (incl. --document-private-items, the pre-push mirror) ✓ · cargo deny check ✓ · domain purity (clean-arch test) ✓.

Closes #196

🤖 Generated with Claude Code


Open in Stage

github-actions Bot and others added 2 commits June 11, 2026 11:10
…IN anti-join arms (#196 v1)

The check engine was blind to logic living in subqueries: #173 shipped
join.anti-join detecting only the LEFT JOIN + IS NULL idiom, with the
NOT EXISTS / NOT IN equivalents as declared exclusions. This lands the
cute-dbt#196 evidence family (v1) and its first consumers.

Domain (src/domain/cte.rs): SubqueryFact { kind: SubqueryKind
{NotExists, NotIn} (#[non_exhaustive]), consumer, inner_leaf,
equi_keys: Vec<JoinKeyPair> } — the LeftJoinFact sibling, riding
CteGraph behind #[serde(skip)] (wire-isolation test pins it; the
embedded payload shape is byte-identical to pre-#196).

Engine (src/adapters/cte_engine.rs): collect_subquery_facts rides the
SAME single parse at the same call sites as the LEFT JOIN collection.
From top-level WHERE AND conjuncts only: correlated NOT EXISTS over a
single plain-table inner (equi pairs normalized OUTER-side-left; a
correlated-but-keyless form emits empty keys), and single-column NOT IN
(the membership pair; an unresolvable outer side emits empty keys).
OR-branch / non-negated / derived-or-multi-table-inner / uncorrelated
forms stay silent, each pinned by a negative test.

Detector (src/domain/checks.rs): bind_keys / key_match_verdict
generalize over an internal KeyedJoinView constructed from both fact
families — LEFT JOIN behavior byte-identical (existing tests pin it).
detect_join_anti_join gains not_exists[...] / not_in[...] sites with
ordinal dedup, form-specific evidence, the same inverted-recommendation
sketch, and the same Covered/Uncovered/UNKNOWN ladder.
join.left-null-propagation never enumerates subquery constructs
(verified by test, not assumed).

Spec + ledger: conditions gain both forms (incl. the NOT IN NULL-trap
honesty note); the lifted v1 exclusion line is replaced by the residual
exclusions; heuristics/registry.toml + book/src/checks/join.anti-join.md
regenerated (byte-gated).

Tests: the #173 silence pin not_exists_anti_join_is_silent_through_the_
real_engine FLIPS into positive detection; NOT IN positive twin added;
covered/uncovered/UNKNOWN through the real engine for both arms; fresh
residual-exclusion pins; 14 engine inline tests; 7 detector unit tests;
3 BDD scenarios join coverage_checks.feature (feature count stays 22).

Dogfood (the #164 precedent): the synthetic playground-current.json
gains int_patients__never_admitted (correlated NOT EXISTS, no unit
test) — the golden playground report now renders the not_exists arm
UNCOVERED with the inverted sketch (programmatic single-line JSON
splice; MANIFEST.toml sha updated; real-fixture pin added). dbt-project/
gains orders_never_refunded (NOT IN) + a unit test whose givens carry a
matching pair — verified Covered with attribution against a real
fusion-compiled manifest; the live PR preview renders the satisfied
twin. jaffle-shop + diff-showcase goldens are byte-identical (no
anti-join spec embeds there); playground + explore regenerated and
audited delta-by-delta.

Closes #196

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The --document-private-items doc gate reads [NOT] as an intra-doc link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@cmbays, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 59 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16c4d1f1-ba4e-4f4a-81bc-e55091de3f56

📥 Commits

Reviewing files that changed from the base of the PR and between f86b68e and 35396ab.

📒 Files selected for processing (18)
  • book/src/checks/join.anti-join.md
  • dbt-project/models/marts/_marts__models.yml
  • dbt-project/models/marts/orders_never_refunded.sql
  • examples/explore/dag.html
  • examples/explore/tests.html
  • examples/playground-report.html
  • features/coverage_checks.feature
  • heuristics/registry.toml
  • src/adapters/cte_engine.rs
  • src/domain/checks.rs
  • src/domain/cte.rs
  • src/domain/mod.rs
  • tests/check_engine.rs
  • tests/fixtures/MANIFEST.toml
  • tests/fixtures/playground-current.json
  • tests/steps/builders.rs
  • tests/steps/coverage_checks.rs
  • tests/steps/world.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch domain-196-subquery-anti-join-evidence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

📄 Rendered report preview

All golden examples regenerated cleanly.

🟡 Golden examples

Committed to examples/ and byte-identity gated — the canonical reports contributors and consumers browse. Stable across PRs.

Report View Download
diff-showcase-report.html ▶ Open ↗ ⬇ Download
playground-report.html ▶ Open ↗ ⬇ Download
jaffle-shop-report.html ▶ Open ↗ ⬇ Download

🐶 Live dogfood preview

This PR's own dbt-project/ diff, freshly compiled by fusion into an ephemeral manifest (never committed) and rendered with --pr-diff. Regenerated every PR — the live self-dogfood, not a committed example.

Report View Download
dbt-project-report.html ▶ Open ↗ ⬇ Download

▶ Open ↗ opens the report in your browser in one click —
published to this repo's GitHub Pages under /pr-229/.
⬇ Download fetches the same self-contained HTML as a workflow
artifact (auth-gated; works fully offline). Either way the report
makes zero external resource requests.

The Pages preview may take ~1 min to update after this comment
posts. On PRs from forks the Open link is unavailable (read-only
token) — use Download.

Alternative: GitHub CLI
# gh CLI >= 2.63 extracts into ./report-preview-playground/.
gh run download 27362224840 -R breezy-bays-labs/cute-dbt -n report-preview-playground
open report-preview-playground/playground-report.html

Posted by report-preview.yml for 35396abd5d2564108b4e5979e7f5b585d6d69024. Affordance only — never blocks merge.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for detecting anti-joins authored as NOT EXISTS or NOT IN subqueries. It updates the CTE engine to parse and collect these subquery facts, integrates them into the join.anti-join heuristic check, and updates the documentation and test suites accordingly. The review feedback suggests a minor refactoring in src/adapters/cte_engine.rs to use subquery.body.as_ref() instead of &*subquery.body for more idiomatic Rust code.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/adapters/cte_engine.rs Outdated
github-actions Bot and others added 2 commits June 11, 2026 12:06
Gemini review on PR #229: `subquery.body.as_ref()` over `&*subquery.body`
in conforming_inner's let-else. House-style check confirms it: the `&*`
form was the only occurrence in all of src/, while .as_ref() is the
file's established explicit-reference idiom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cmbays
cmbays merged commit b807e12 into main Jun 11, 2026
33 checks passed
@cmbays
cmbays deleted the domain-196-subquery-anti-join-evidence branch June 11, 2026 16:39
github-actions Bot added a commit that referenced this pull request Jun 11, 2026
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.

capability: subquery-aware model decomposition — correlated-subquery evidence family for the check engine

1 participant