Documentation
¶
Overview ¶
Package multikey is the fixture and query corpus for correlated subqueries that correlate on MORE THAN ONE column.
Why it exists. Every correlated-subquery entry in every other corpus here correlates on exactly ONE equality. #562 is what that blind spot cost: a two-column correlated EXISTS answered ZERO rows, its NOT EXISTS twin answered EVERY row, and neither the type matrix, the TPC-H corpus, the DuckDB fingerprint corpus, the PostgreSQL oracle nor the shape fuzzer contained a single query that could show it. The defect was in the build side's NDV narrowing (dedupSemiAntiBuildSide): it read the join keys out of the condition TEXT with a split on " and " while a decorrelation renders " AND ", so it kept the FIRST conjunct's key and projected the build side down to that one column — deleting the column the second conjunct compares.
A two-column correlated existence check is what a BI client emits for a compound-key lookup, so the shape is ordinary and the failure was total and silent.
This package holds no assertions and no expected answers beyond the ones PostgreSQL gave: three gates consume it.
wadjet.TestMultiKeyCorrelatedSubqueries — the embedded engine coordinator.TestMultiKeyCorrelatedTwoPath — stage DAG vs single process (PostgresSetup renders the same fixture for the container that decided every Want below.)
Index ¶
Constants ¶
const ( DNOuter = "dn_outer" DNInner = "dn_inner" DNWide = "dn_wide" DNDim = "dn_dim" )
The DISTINCT-NAME arm.
The shared-schema arm (multikey.go) is where #562 was found, and it gates the wrong half of the pass. Its two relations carry ONE schema, so every correlated conjunct reads `s = s` — a name that resolves on both sides, which extractRightJoinKeys cannot attribute and therefore DECLINES. That arm proves the decline is right; it never once makes the narrowing fire.
These tables give the outer, inner and wide relations DIFFERENT column prefixes (p_ / q_ / w_), so `b.q_s = a.p_s AND b.q_n = a.p_n` attributes cleanly, the pass narrows the build side to Project(q_s, q_n) → Distinct, and the code path #562 lives on is the one under test.
It found a second silent-zero on that path immediately: the Project aliases every key to its BARE name, so a key the condition spells QUALIFIED is renamed out from under it. See the dn_selfjoin_* entries.
const ( DNOuterRows = 40 DNInnerRows = 24 DNWideRows = 260 )
Row counts, mirroring the shared-schema arm: Wide > 3 × Outer is the estimator's semi/anti swap threshold, Inner < Outer is its control.
const ( Outer = "mk_outer" Inner = "mk_inner" Wide = "mk_wide" Dim = "mk_dim" )
The fixture tables. Outer is the probe relation; Inner is SMALLER than it and Wide is more than three times LARGER, which is the estimator's threshold for swapping a semi/anti join onto its other side (exec's RightSemiJoin / RightAntiJoin). Both sides of that decision have to answer the same, so the corpus runs the same shape against each.
const ( OuterRows = 40 InnerRows = 24 WideRows = 260 )
Row counts. Wide > 3 × Outer is the swap gate; Inner < Outer is its control.
Variables ¶
This section is empty.
Functions ¶
func DNData ¶
DNData builds one distinct-name table's rows. Same coprime periods and the same NULL discipline as the shared-schema arm — the FIRST column of a pair nulls on the probe side and the SECOND on the build side — so the two arms differ in exactly one thing: whether the pass can attribute the keys.
func Data ¶
Data builds one table's rows. Every value is derived from the row index, so the fixture is identical in every process — and in PostgreSQL, which PostgresSetup renders from this same function.
NULLs are placed so that for each keyed PAIR, one side nulls the FIRST column and the other side nulls the SECOND. PostgreSQL's rule is that a NULL key never matches anything, including another NULL, and a multi-column key gives an implementation two chances to get that wrong — once per column, and once more in whatever composite it builds out of them.
func DimData ¶
DimData is five groups, one more than g's four, so the dimension join is not a no-op filter and one dim row matches nothing.
func DimSchema ¶
DimSchema is the join partner that turns the subquery's inner into a JOIN, which is the shape whose key spelling only reorderJoins settles (ADR-0021).
func PostgresSetup ¶
func PostgresSetup() string
PostgresSetup renders the fixture as PostgreSQL DDL and INSERTs, from the same Data() the engines load. It is how every Want above was decided, and it is here so that deciding them again is a re-run rather than an archaeology exercise: TestCorpusAnswersComeFromPostgres (multikey_test.go) loads this script into a postgres:17-alpine container, runs every Corpus() query against it, and asserts each Want is what PostgreSQL says. It SKIPS when no container is reachable, exactly like the TPC-H oracle.
The text columns are COLLATE "C" for the same reason the TPC-H oracle's are: wadjet compares strings by bytes.
func Schema ¶
Schema is the shape of Outer, Inner and Wide: an id, three PAIRS of columns that a correlated subquery can key on together, and a low-cardinality g for the third key and the dimension join.
The pairs are chosen so a multi-column key reaches three different key encodings in the hash join: STRING+INT64 mixes a serialized column with an integer one, DECIMAL+DATE is two columns neither of which is a plain integer, and CIDR+UUID is two network-native types that only this fixture and the type matrix carry at all.
Each pair's two columns cycle on COPRIME periods (6 and 5, 7 and 5, 9 and 4), so the PAIR is far more selective than either column alone and Inner covers only part of the product. That is what makes a dropped key visible as a wrong non-zero answer and not only as the zero #562 produced: the two-key answer sits strictly between the one-key answers, which the exists_one_key control pins from the other side.
Types ¶
type Case ¶
type Case struct {
Name string
SQL string
Want int64
// Keys is how many equality conjuncts the decorrelation should produce,
// for the report when an entry fails. Not asserted — the plan shape is
// asserted in internal/planner/logical.
Keys int
// KnownBug pins a divergence from Want that is NOT this corpus's subject
// and is tracked in Issue. The comparison still RUNS and Want stays
// exactly as PostgreSQL wrote it: a pinned entry that starts AGREEING
// fails, so deleting the pin is the whole of "the fix landed"
// (ADR-0013 §Pins). Empty for every other entry.
KnownBug string
// Issue is the tracker reference for a KnownBug.
Issue string
}
Case is one corpus entry: a query and the number of rows PostgreSQL 17 answers it with over this fixture.
Want is an ABSOLUTE answer, not an agreement between two wadjet paths. The #562 defect took a semi join to zero and its anti twin to everything, and both of those are answers two agreeing arms will happily produce together — the stage DAG and the single-process pipeline share the logical optimizer, so a planner defect hits them identically. Only an outside authority can see it, which is why every Want here came from the container.
func Corpus ¶
func Corpus() []Case
Corpus is both arms. The shared-schema arm is where #562 was found and where the pass DECLINES (every conjunct reads `s = s`); the distinct-name arm (distinct_names.go) is where it FIRES. A gate that runs only the first would prove the decline and never touch the narrowing.
func DistinctNameCorpus ¶
func DistinctNameCorpus() []Case
DistinctNameCorpus is the arm on which the narrowing actually FIRES.
type FixtureTable ¶
FixtureTable is one loadable table: the four shared-schema relations and the four distinct-name ones. Every consumer loads this list, so an arm cannot be added to the corpus and forgotten in one of the four gates.