quack: SQL three-valued WHERE over nullable columns (value lane + validity plane) - #1334
Conversation
…idity plane) Filter::sql_where(nullable) rewrites a filter into an ordinary two-valued filter that keeps exactly the rows SQL 3VL makes TRUE. Each subformula lowers to T (TRUE rows) or F (FALSE rows); UNKNOWN is neither and survives NOT/AND/OR up to the WHERE. Leaf on nullable x: T = valid & leaf, F = valid & !leaf; NOT swaps T/F; AND/OR take T and their De Morgan dual for F. Plus Filter::is_null / is_not_null over the validity plane. The validity plane stays the only NULL carrier: no NULL value, no tagged representation, no second bitmap, no expression interpreter. The output uses existing Cmp/Plane/And/Or/Not nodes, so lower and lower_fused run it unchanged and mask-risc is untouched. A filter reading no nullable column is returned unchanged (identical program). tests/sql_null_3vl.rs: independent Kleene oracle over the original filter, NULL payloads that would match; required cases on all validity combos plus 300 random trees, both lowerings, rows and COUNT(*); NULL != 0 / '' / false; aggregate conventions; the naive NOT(valid & x = 5) shown wrong. Disable runs red: F(leaf) without validity, NOT as gate-then-negate, F(AND) without De Morgan, T(leaf) without validity. Board: entry with the decision, gates and re-evaluated SQL READ coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. 📝 WalkthroughWalkthroughThe change adds ChangesNullable SQL filtering
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to This change makes WHERE filters over nullable columns keep only rows that evaluate to TRUE, so NULL rows are excluded. Filters that do not touch nullable columns behave as before. No concrete defect was identified, and the change appears ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 54.35% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 46 functions across 5 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
I’m a rabbit who hops through the NULLs in the lane, Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 0218ca64-5e55-473d-ab82-a31f9856e58b) |
- Filter::sql_where: Semijoin is EXISTS, so a NULL fk makes it FALSE (known), not UNKNOWN. A listed fk still gates its Gather: T = valid AND leaf, F = NOT(valid AND leaf). EqU32Via stays UNKNOWN. - New nullable-fk test: NULL-fk rows point at kept, matching foreign rows; nine composites against an independent oracle, both lowerings. - BoundField gains validity: Option<Mask>; Draft::bind passes the bound filter through sql_where. Non-nullable binds are unchanged. - Disable runs red: Semijoin-as-UNKNOWN; Draft::bind without sql_where. - Board entry: ordered u32/u64 downgraded to bounded P1 after the A/B/C classification (no current class-A unsigned field). It also records SAP's sentinel-NULL optional fields as P1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f46cd60645
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two Codex P2 findings on #1334: - Cmp::Range answers from the row ordinal, but it stands for a comparison on its provenance lane, so a nullable lane makes it UNKNOWN at NULL rows. It is no longer exempt. Test: range, NOT range and range OR y on the fixture against the oracle; the raw range keeps NULL rows (can-fire). The disable run (exempt Range) goes red. - sql_where is now a single pass: rewrite() returns None for a two-valued subtree instead of rescanning it with reads_any at every level, which was quadratic down a NOT chain. The output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2
A 96-deep alternating chain with nullable leaves at every level agrees with the 3VL oracle through lower and lower_fused, and the raw chain does not (anti-vacuity). The same shape over non-nullable leaves comes back unchanged from sql_where. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2
Quack continues to use ordinary value lanes plus resident validity planes. This PR makes Boolean filtering over nullable values SQL-3VL correct. It does not introduce a general-purpose SQL NULL value representation.
value 0, valid 1is a real zero.value 0, valid 0is NULL. NULL never becomes a value: this PR adds no boxed or tagged NULL, no second bitmap and no expression interpreter.The bug this fixes (P0 from the SQL coverage reconnaissance)
Filter::Notlowers to a plain complement. Over a nullable column that silently keeps NULL rows:NOT (x = 5),x <> 5andx NOT IN (…)all keep rows wherexis NULL.NOT (valid(x) AND x = 5), is wrong: SQL'sNOT (x = 5)is UNKNOWN on a NULL row, andWHERErejects UNKNOWN.The change
Filter::sql_where(&self, nullable: &[(Col, Mask)]) -> Filter, plusFilter::is_null/Filter::is_not_nullover the validity plane.Each subformula lowers to
T(φ), the rows where it is TRUE, orF(φ), the rows where it is FALSE. A row in neither is UNKNOWN.WHEREkeepsT(root).φT(φ)F(φ)x(CmpincludingRange,EqU32Viaby its fk)valid(x) ∧ leafvalid(x) ∧ ¬leafSemijoinon nullablefkvalid(fk) ∧ leaf¬(valid(fk) ∧ leaf)leaf¬leafNOT ψF(ψ)T(ψ)AND⋀ T⋁ FOR⋁ T⋀ FCmp/Plane/And/Or/Notnodes, solowerandlower_fusedrun it as is.valid(x)lowers like any resident plane, so the existing survivor skip uses it.Cmp: nullable by its column.Cmp::Rangeexecutes over the row ordinal but retains its provenanceCol. If that semantic lane is nullable, theRangepredicate is gated by that lane's validity plane, so NULL yields UNKNOWN andWHERErejects it. Physical implementation detail ≠ semantic type.EqU32Via: nullable by its fk. With a NULL fk,f.v = cis UNKNOWN.Semijoin: not three-valued. It isEXISTS(SELECT 1 FROM foreign f WHERE f.rid = this.fk AND …), and with a NULL fk that is FALSE, a known answer. The fk is still gated, becauseGatherwould otherwise read the stale payload at the NULL row. SoNOT semijoinkeeps NULL-fk rows, andNOT eq_viadoes not.Plane: never nullable. A plane is a known Boolean, which is what makesIS [NOT] NULLexact.BoundFieldgainsvalidity: Option<Mask>. The binder owns the schema, so it owns nullability.Draft::bindpasses the bound filter throughsql_where, so a boundWHEREover a nullable field is three-valued without the caller doing anything.Out of scope:
COALESCE,NULLIF, nullable arithmetic and projection;x = NULL,NOT IN (…, NULL));NULLS FIRST/LAST.Aggregates are unchanged:
COUNT(x)/SUM/MIN/MAX/AVGstill takevalid(x)in their filter.Gates
tests/sql_null_3vl.rschecks against an independent row-at-a-time Kleene oracle that evaluates the original filter. Rows where a value is NULL carry payloads (5,0,7) that would match the predicates, so any read of a NULL payload shows up.=,<>,NOT (x = 5),AND,OR,NOT (… AND …),NOT (… OR …),[NOT] BETWEEN,[NOT] IN,IS [NOT] NULL.lowerandlower_fused; both kept rows andCOUNT(*).Range:a_range_on_a_nullable_lane_is_gated. NULL rows lie physically inside the ordinal range[10, 100), and the raw range keeps them (can-fire). Range, NOT range and range OR y match the oracle.a_deep_mixed_chain_is_rewritten_correctly_and_a_non_nullable_one_is_untouched. A 96-deep alternating NOT/AND/OR chain matches the oracle through both lowerings, and the raw chain does not. The same shape over non-nullable leaves comes back unchanged.fk::semijoin_over_a_null_fk_is_false_and_eq_via_is_unknown.Semijoin/EqU32Viacomposites are checked against an oracle where EXISTS is FALSE and the via comparison is UNKNOWN, through both lowerings.bind::tests::a_nullable_field_binds_three_valued.= 33drops it.<> 33keeps no NULL row.NULL ≠ 0(i32);NULL ≠ ''/false(u32 ordinal 0);COUNT(*)vsCOUNT(x), valid-only SUM/MIN/MAX/AVG, and no contributions ≠ zero.NOT (valid(x) ∧ x = 5)and the raw unrewritten filter are both shown to disagree with the oracle on this fixture.lowerandlower_fused.Draft::bindwithoutsql_where;Rangeexempted from gating.lance-graph-quack,lance-graph-reportand dir-simquack_bindsuites pass;-D warningsare clean.main.Board
entries/2026-10-05-quack-sql-null-3vl.mdrecords the decision and the re-evaluated SQL READ coverage.Unsigned fields, classified:
Range.No current Report / IAM / SAP workload needs class-A u32/u64 ordering, so ordered unsigned comparison is bounded P1.
No P0 SQL READ gap is demonstrated.
P1: SAP's optional fields carry NULL as in-lane sentinels. Its only query reads required fields, so it is correct today; the first predicate over an optional field must move that field to a validity plane.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DCEP2fZdHYdMCtcEVcTpS2
Generated by Claude Code
Generated by Claude Code