Repository navigation
sqleq-solver: read row slices the IR settles, FROM-less queries, computed VALUES cells and COUNT(DISTINCT) - #223
Merged
Merged
Conversation
…uted VALUES cells and COUNT(DISTINCT) These are the refusals behind most of the pairs only the QED prover decides. Measured on the IR of the qed-exp-29c3d05 run (12,198 lowered pairs), sqleq-solver now proves 85 of the 213 pairs the portfolio decided by QED alone (89 of the 222 QED proofs it lacked), 112 new proofs in all, 27 of them on pairs nothing decided before; no proof on any of the 945 pairs sqleq-fuzz refutes, and no proof lost. - card: which relations return at most one row on databases that keep the schemas' keys (a NOT NULL unique key held to one value by WHERE / inner-join ON equalities, carried through joins, projections and groups). A literal LIMIT of one or more over such a source is the identity; LIMIT 0 keeps no row, OFFSET 0 every row; a LIMIT of one or more under EXISTS is dropped. - opaque: before translation, the same slice (sort keys and counts), or the same opaque aggregate (the frontend's DISTINCT ON carrier), of two sources proved equal is replaced on both sides by one fresh table. That reads a slice as a function of its source's bag, as QED and the frontend's strip_identical_pagination already do; a slice over an ORDER BY translation erases is not replaced unless its keys cover every column, mirroring the frontend's refusal. - The FROM-less "singleton" relation is one empty row; a VALUES cell may compute its value (a parameter or cast; a subquery cell is still refused); COUNT(DISTINCT x) is the nested existential sum, refused where = is not identity; MAX/MIN ignore DISTINCT. - Sort keeps its keys (SortKey, as on the cvc5 branch); Schema::enforced_keys is shared with ic. - Four sqleq-solver pins re-blessed: two equivalent pairs unsupported -> proved, two pairs unsupported -> no-proof. Every ordering/ pin still holds. Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
bvolpato-dd
approved these changes
Nov 1, 2026
bvolpato-dd
left a comment
There was a problem hiding this comment.
Approved under Bruno Volpato's standing instruction for datadog-labs/sqleq. I checked the pull request metadata only; I did not review the diff or run the tests.
Stamped by nightwatcher, a review agent that acts for Bruno Volpato.
zvonimir-dd
pushed a commit
to mudathirmahgoub/sqleq
that referenced
this pull request
Nov 2, 2026
…n matches the bag set operations main gained datadog-labs#223, which conflicts with this branch and, once merged, does not compile without two more arms: - sqleq-solver/src/ir.rs conflicted on a doc comment beside the new UnsupportedBagSetOp variant; both comments are kept. - datadog-labs#223's card.rs `shape` and opaque.rs `each_node`/`each_expr` match on Relation exhaustively. IntersectAll/ExceptAll take Shape::unknown, as Union does (translation refuses both nodes anyway), and the walks recurse into both sides. Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
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 type of PR is this?
Description
Why
The QED prover is still the only prover that decides a share of the pairs the portfolio decides, and it is no longer maintained. sqleq-solver receives the same IR bytes (
src/lib.rsemit()), so every pair QED decides and sqleq-solver does not is a gap in sqleq-solver. Most of those pairs stop at one of four refusals: a row slice (unsupported-sort, anyLIMIT/OFFSET), aFROM-less query (rel-not-object), aVALUEScell that computes its value (values-nonliteral), andCOUNT(DISTINCT)(aggregate-distinct-unsupported). This PR reads those where the IR settles them. The frontend is unchanged.card: relations that return at most one rowA
LIMIT 1over a query that cannot return two rows keeps every row it gets:card::shapedecides that on the IR, for databases that keep the schemas' keys (the assumptionicandNormalizer::with_icsalready make):Schema::enforced_keys, now shared withic) whose columns a conjunct of aWHEREor of an inner join'sONholds to one value bounds its table to one row. "One value" means a literal, a parameter, one of the frontend's casts or conversions of one, a column of an enclosing query, or a column equated with any of these. It only counts where=on the column's type is identity.u JOIN t ON t.k = u.t_k WHERE u.id = $1), projections, deduplication, and a group whose keys are all held.OR, an order comparison, a function call or a subquery holds nothing, so the analysis can miss a bound but never invent one.On top of it,
card::slice:LIMITof one or more over such a source as the identity;LIMIT 0as no rows andOFFSET 0as every row;LIMIT ≥ 1(with noOFFSET) underEXISTS.LIMIT $nstays refused, since$nmay be bound to 0.opaque: the same slice of equal sourcesBefore translation,
abstract_opaquecollects, on each side, the nodes translation would refuse:cardcannot read;GROUP BYwith an aggregate translation does not model, which in practice means the frontend'sDISTINCT_ON#…carrier.Nodes are matched one to one across the sides. Two nodes match when they have the same sort keys and counts (or the same group keys and calls), and their sources are identical or proved equal by a recursive
prove_at(depth ≤ 3, which also covers nested slices). Each matched pair is replaced on both sides by a scan of one fresh table. A node that reads a column of an enclosing query is left alone. If any refused node has no partner, translation refuses the pair as before.The reading this rests on is the one QED's opaque
limit,strip_identical_paginationandlower::apply_paginationalready take: a slice is a function of its source's bag. Nothing beyond it is added.sort_sandwichputs every order key into the slice's source, so comparing sources as bags covers the keys.ORDER BYthat translation erases is not replaced unless its own keys cover every column, because that order decides the ties. The frontend already refuses that shape (refuse_slice_over_dropped_order); the guard is kept here too so the claim does not rest on the frontend alone. The second counterexample instrip_identical_pagination's doc is a test.Small refusals
"singleton"aFROM-less query is lowered over is read as one empty row.VALUEScell may compute its value (a parameter, a cast). It is read at its column's type, and refused if its own type differs. A subquery cell is still refused.COUNT(DISTINCT x)is SQLSolver's nested existential sum, refused where=onxis not identity, as deduplication is.MAX/MIN(DISTINCT)areMAX/MIN.SUM/AVG(DISTINCT)stay refused.IR
Relation::Sortnow keeps its keys askeys: Vec<SortKey>, the same definition #209 (cvc5 axis) adds, so the two merge without a real conflict.Pins that moved
All four are
expect sqleq-solverlines, re-blessed. None is on a pair whose truth isnot-equivalentand now proved. Everyordering/pin (a non-equivalent slice orDISTINCT ONthat no prover may prove) still holds.pagination/nested_limit_ties_under_filter.sql(equivalent):unsupported→proved. Both sides takeORDER BY "g" LIMIT 1of sources that are equal bags, because"id"is a key soDISTINCTkeeps every row. This is the reading the pair's ownargument:gives.types/union_null_branch.sql(equivalent):unsupported→proved. TheFROM-lessSELECT NULLbranch arrives as a computedVALUEScell.types/values_integer_beside_numeric.sql(equivalent):unsupported→no-proof. It is translated now, but1and1.0are two values to this prover, so it is a conservative miss.dml/insert_values_stores_numeric_literal_as_text.sql(not equivalent):unsupported→no-proof. Translated now, and not proved.Left out
The largest remaining group of pairs only QED decides needs predicate and value equivalence under bag semantics: the set solver only runs where every sum is under a squash. That, correlated subqueries (
correlated), and dedup over unvouchedVARBINARYare the next steps.How it was tested
cargo test -p sqleq-solver: unit tests includingcard's, plus the newtests/limit_slices.rs, which pairs each rule with the negative controls it must refuse:<=,OR,=on REAL;LIMIT $n,OFFSET 1;LIMIT $3vsLIMIT $4;strip_identical_pagination's doc;COUNT(DISTINCT)on a nullable column.cardalso has a property test: relations it calls "at most one row" are translated and evaluated on random databases that keep the keys, and none ever has two rows. The test fails if no relation ever had two rows, so it cannot pass vacuously.cargo clippy -p sqleq-solver --all-targets -- -D warnings, andcargo doc --no-deps -p sqleq-solverwith-D warnings, with and without private items.cargo test -p sqleq-check --test real_solveragainst the built binary.sqleq-check --expect pinned --axes sqleq-solver tests/pairs examples/*.sql: 471/471 after the bless above.Corpus cross-check (CONTRIBUTING rule 2). The corpus is private, so these counts are for scale only.
The release binary was run over the IR of the latest all-axes run of the benchmark corpus, which was lowered at the same frontend revision as
main, and joined row by row against that run's verdicts:sqleq-fuzzrefutes, and no proof lost.sqleq-fuzzfound no counterexample on 105 and skipped 7 by construction. Six areEXISTS (SELECT $1 …)againstEXISTS (SELECT 1 …), a parameter on one side only. One is a slice whose ties make it nondeterministic.sqleq-check --portfoliorun.Checklist
sqleq-check --portfolio(the provers and
sqleq-fuzzat once; a proof with a counterexample is analarm), and noneof the newly proven pairs has a counterexample. (A row-by-row join against the corpus run's stored
sqleq-fuzzverdicts, as described above, not a fresh--portfoliorun.)tests/pairs/and was seen failing on a build from before the fix. (Not a defect fix.)
tests/pairs/that moved is explained in the description.LICENSE-3rdparty.csvwas regenerated withsh tools/update_license_3rdparty.sh. (No dependency change.)AI code assistants
Assisted-by: Claude Code (Claude Opus 5.5)
🤖 Generated with Claude Code