multikey

package
v0.18.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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

func DNData(table string) []map[string]any

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 DNSchema

func DNSchema(table string) parquet.Schema

DNSchema is one distinct-name table's schema.

func Data

func Data(table string) []map[string]any

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

func DimData() []map[string]any

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

func DimSchema() parquet.Schema

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

func Schema() parquet.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
	// LoudLike and LoudLikeDAG are the substrings the entry's ERROR must
	// carry, per ARM, where the pinned divergence is a REFUSAL rather than a
	// wrong number. Empty means the entry is pinned on its VALUE and an error
	// from that arm is a failure, as it was before any entry became loud.
	//
	// They exist so that "this entry is pinned" cannot mean "this entry may
	// fail in any way at all". Four entries went from a wrong number to a
	// refusal when #734/#679/#535's consumer half landed, and a harness that
	// simply stopped asserting on error for every pinned entry would have
	// swallowed a future regression of any class in any of them.
	//
	// TWO fields because the two engines refuse these for two DIFFERENT
	// reasons, both pre-existing: the single-process path fails inside the
	// per-row re-run (an unparseable rebuild, or a reference it refuses to
	// resolve standalone), while the DAG never gets that far — its worker has
	// no SubqueryRunner and the filter compile refuses first. One substring
	// for both would have had to be short enough to match neither precisely.
	LoudLike    string
	LoudLikeDAG 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

type FixtureTable struct {
	Name   string
	Schema parquet.Schema
	Rows   []map[string]any
}

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.

func Tables

func Tables() []FixtureTable

Tables is the whole fixture.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL