Skip to content

exec, plan-codec: existence join for IN/EXISTS subqueries (P2-T115 unit 1) - #237

Merged
techgopal merged 2 commits into
mainfrom
p2b/t115-mark-join
Oct 25, 2026
Merged

techgopal merged 2 commits into
mainfrom
p2b/t115-mark-join

Conversation

@techgopal

Copy link
Copy Markdown
Contributor

Existence and single-match hash joins (P2-T115 unit 1, design P2-T166 §3.2)

Adds the physical join forms general subquery decorrelation needs; the analyzer does not produce them yet.

  • JoinType::Existence (Spark's ExistenceJoin): every probe row once plus a non-nullable boolean exists, TRUE when some build row makes the whole condition (keys and residual) TRUE. Runs in the hash join (in memory and grace spill, both key formats) and the nested-loop join.
  • single_match on a LeftOuter hash join: a second match for a probe row fails with SCALAR_SUBQUERY_TOO_MANY_ROWS (SQLSTATE 21000). Orientation never flips a single-match join to RightOuter.
  • Plan format: JOIN_TYPE_EXISTENCE (feature existence_join), single_match (feature join_single_match), distinct_build (ignorable, validated: left semi/anti/existence without a residual; the executor still stores every row).

Spark 4.2.0 results (ANSI on and off identical)

t(id,k)=(1,1),(2,2),(3,NULL),(4,4), un={1,NULL}, nn={1,2}, e={}, c(g,k)=(1,1),(2,NULL),(4,9),(3,5):

expression id 1..4
k IN (SELECT k FROM un) true, false, false, false
k IN nn true, true, false, false
k IN e false ×4
k NOT IN un false ×4
k NOT IN nn false, false, false, true
k NOT IN e true ×4
(k IN un) IS NULL false ×4
CASE WHEN k IN un THEN 'y' WHEN NOT (k IN un) THEN 'n' ELSE 'u' END y, u, u, u
(k IN un) OR k = 2 true, true, NULL, false
k IN (SELECT c.k FROM c WHERE c.g = t.id) true, false, false, false
k NOT IN (SELECT c.k FROM c WHERE c.g = t.id) false, false, false, true
EXISTS (SELECT 1 FROM c WHERE c.g = t.id AND c.k = t.k) true, false, false, false

The same values appear under OR in WHERE, in GROUP BY, in an aggregate FILTER and in a join ON condition. An IN subquery in ORDER BY is rejected (UNSUPPORTED_IN_EXISTS_SUBQUERY). Spark's plans: IN is a hash ExistenceJoin on the key; uncorrelated NOT IN is NOT of a nested-loop ExistenceJoin on (k = s.k) OR isnull(k = s.k); correlated NOT IN is NOT of a hash ExistenceJoin on the correlation keys with that residual. A literal NULL probe and one-row literal subqueries are folded before any join (in-nullability.sql.out); that belongs to the analyzer unit.

Tests replay these statements in Spark's plan shapes, plus in-null-semantics.sql.out and exists-outside-filter.sql.out from tests/spark-golden. A reference model written from Spark's definition is checked on edge cases, random data, spill, and co-partitioned execution.

shehatathe and others added 2 commits October 25, 2026 07:22
General subquery decorrelation needs two more join forms in the
executor: a mark join for EXISTS/IN subqueries that are not top-level
filter conjuncts, and a single-match left outer join for correlated
scalar subqueries without an aggregate. This change adds both to the
existing hash join build/probe code and its grace-spill path, with
their plan data and one new codec feature. The analyzer does not
produce them yet.

MarkExists emits every probe row once with a non-null boolean mark
(TRUE on a match, else FALSE). MarkIn (one key, no null-safe keys)
is TRUE on a match, else NULL when the probe key is NULL or the
build side holds a NULL key, else FALSE; an empty build side gives
FALSE for every row, NULL keys included, matching Spark's default
nullInEmptyListBehavior=false. A residual condition, when present,
decides matches as in the other join types (NULL is not a match).
A LeftOuter join with single_match set fails the query with
SCALAR_SUBQUERY_TOO_MANY_ROWS (SQLSTATE 21000) when a probe row
matches more than one build row. Weighted build sides are rejected
exactly like the other non-inner join types; probe weights pass
through. The plan codec carries the new join types and the
single-match flag of HashJoinNode under one new semantic feature,
mark_join. Nested-loop joins refuse mark joins; reorientation and
role-reversed grace joins never apply to them.

Unverified: diff coverage was not tool-measured (new paths are
exercised by the tests below, except one defensive unreachable arm
in the mark tail). The legacy nullInEmptyListBehavior=true
semantics (NULL for NULL IN (empty)) is deliberately not followed:
the task pins FALSE for an empty build. The mark output column is
named mark; no analyzer consumer exists yet.

Signed-off-by: techysneha14 <343708768+shehatathe@users.noreply.github.com>
Spark 4.2 plans EXISTS and IN outside a filter conjunct as an
ExistenceJoin whose exists column is never NULL: k IN (subquery) is
false, not NULL, when the subquery holds a NULL or k is NULL, in the
SELECT list, under OR, in CASE, GROUP BY, aggregate FILTER and join
conditions alike. NOT IN is the negated existence join on
(k = c) OR isnull(k = c), by nested loops when uncorrelated and with
the correlation keys as hash keys otherwise.

Replace MarkExists and MarkIn with one Existence join type that
follows this definition. A probe row's exists depends only on the
build rows with its key, so the build-wide NULL and emptiness facts
are gone, the residual condition decides per pair, and the join is
correct broadcast or partitioned. The nested-loop join runs it too.

The plan format follows design P2-T166: JOIN_TYPE_EXISTENCE under the
existence_join feature, the single-match flag under its own
join_single_match feature, and an ignorable distinct_build flag,
validated in the decoder and at instantiation. Orientation no longer
turns a single-match left outer join into a right outer join.

Tests use a reference model written from Spark's definition, replay
the Spark 4.2.0 results in the shapes Spark plans, replay
in-null-semantics and exists-outside-filter from tests/spark-golden,
and check co-partitioned execution against one join.

Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between c03aa5f and 7bc0578.

Files ignored due to path filters: 3
  • crates/krishiv-plan-codec/tests/golden/existence_join/hash_join_existence.bin is excluded by !**/*.bin
  • crates/krishiv-plan-codec/tests/golden/existence_join/nested_loop_existence.bin is excluded by !**/*.bin
  • crates/krishiv-plan-codec/tests/golden/join_single_match/hash_join_single_match.bin is excluded by !**/*.bin
Files selected for processing: 23
  • crates/krishiv-exec/benches/hash_join.rs
  • crates/krishiv-exec/examples/join_index_matrix.rs
  • crates/krishiv-exec/src/join/grace.rs
  • crates/krishiv-exec/src/join/mod.rs
  • crates/krishiv-exec/src/join/nested.rs
  • crates/krishiv-exec/src/join/probe.rs
  • crates/krishiv-exec/src/physical/instantiate.rs
  • crates/krishiv-exec/src/physical/mod.rs
  • crates/krishiv-exec/src/planner/join.rs
  • crates/krishiv-exec/src/planner/orient.rs
  • crates/krishiv-exec/tests/hash_join/existence.rs
  • crates/krishiv-exec/tests/hash_join/main.rs
  • crates/krishiv-exec/tests/hash_join/payload_rows.rs
  • crates/krishiv-exec/tests/hash_join/tpch.rs
  • crates/krishiv-exec/tests/planner/physical.rs
  • crates/krishiv-plan-codec/src/decode.rs
  • crates/krishiv-plan-codec/src/encode.rs
  • crates/krishiv-plan-codec/src/features.rs
  • crates/krishiv-plan-codec/src/proto/krishiv.plan.v1.pb.rs
  • crates/krishiv-plan-codec/tests/common/inventory.rs
  • crates/krishiv-plan-codec/tests/features.rs
  • crates/krishiv-plan-codec/tests/roundtrip.rs
  • proto/krishiv/plan/v1/plan.proto

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Walkthrough

This change adds existence joins and single-match left outer joins. It updates join planning and execution, extends the plan schema and codec, and adds execution and compatibility tests.

Changes: Join Semantics and Plan Support

Layer / File(s) Summary
Join and plan contracts
crates/krishiv-exec/src/join/mod.rs, crates/krishiv-exec/src/physical/mod.rs, proto/krishiv/plan/v1/plan.proto, crates/krishiv-plan-codec/src/proto/krishiv.plan.v1.pb.rs
The join and plan definitions add Existence, single_match, and distinct_build. They define existence output as probe columns plus a non-nullable Boolean and constrain single-match and distinct-build configurations.
Planning and plan instantiation
crates/krishiv-exec/src/physical/instantiate.rs, crates/krishiv-exec/src/planner/join.rs, crates/krishiv-exec/src/planner/orient.rs, crates/krishiv-exec/tests/planner/physical.rs
Plan construction initializes the new flags. Orientation does not flip single-match left outer joins. Instantiation rejects distinct-build plans that do not meet the eligibility rules.
Existence and single-match execution
crates/krishiv-exec/src/join/nested.rs, crates/krishiv-exec/src/join/probe.rs, crates/krishiv-exec/tests/hash_join/*, crates/krishiv-exec/benches/hash_join.rs, crates/krishiv-exec/examples/join_index_matrix.rs, crates/krishiv-exec/src/join/grace.rs
Hash and nested-loop joins emit existence results. Hash joins in single-match mode return a scalar-subquery error when a probe row has multiple qualifying matches. Tests cover null handling, residuals, spilling, weights, partitions, randomized inputs, and Spark-shaped cases.
Plan encoding and feature checks
crates/krishiv-plan-codec/src/{decode.rs,encode.rs,features.rs}, crates/krishiv-plan-codec/tests/{common/inventory.rs,features.rs,roundtrip.rs}
The codec reads and writes the new join type and flags. Feature checks classify existence and single-match joins, and decoding rejects invalid flag combinations.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • KrishivAI/krishiv-engine#166: Adds hash-join orientation and right-side join variants. This change updates the orientation contract for existence joins and single-match left outer joins.
Merge Risk: Minimal · up to 7bc05

No actionable issue is established for the new join behavior or plan compatibility. The change appears mergeable after normal checks.

Security architecture risk: Low · up to 7bc05

The new join modes change query results and saved-plan compatibility. Validation and reader checks constrain the change, and no introduced security vulnerability was established. Cancellation, concurrent reuse, and production exposure remain incompletely verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is callers already able to supply executable plans or construct joins, with effects on query results, cardinality errors, and query spill resources. These paths do not establish a new tenant, credential, or service authority boundary; production exposure remains unverified.

Trust Boundaries and Controls

  • observed — Serialized feature declarations are not accepted as sufficient authority to enable semantics: the decoder checks reader support and independently derives the features used by the decoded plan, rejecting mismatches and understated legacy reader requirements.

Resilience and Maintainability Implications

  • observed — Spill ownership has drop-based cleanup, and the added duplicate-match spill test asserts both memory release and spill-file deletion after failure. Forced-spill existence tests compare results in both key layouts. These checks do not establish cancellation-mid-replay or concurrent-reuse behavior.
  • observed — Whole-input-batch tail output predates this PR in semi, anti, and unmatched outer joins. The new existence tail follows that pattern and emits no more than its input rows. The documented batch-size discrepancy is therefore not established as a new maximum-scope security exposure.

Hardening Proposals

  • proposed — Consider enforcing and asserting the configured output-batch bound across all tail-emitting joins, including grace replay. This would strengthen resource-containment guarantees for an existing behavior, not remediate a verified PR-introduced vulnerability.
Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Description check Warning The description provides a detailed summary, task reference, Spark behavior, and test coverage. It does not include the required Tests run section with commands and result lines, and it omits the requ… Add a Tests run section with the exact commands executed and their result lines. Add and complete the repository checklist, including DCO, licensing, test coverage, error handling, distributed execution, API, dependency, and performance ite…
Docstring Coverage Warning Docstring coverage is 55.68% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 88 functions across 22 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: adding existence joins for IN/EXISTS subqueries in the execution and plan-codec components. It is concise and specific.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description provides a detailed summary, task reference, Spark behavior, and test coverage. It does not include the required Tests run section with commands and result lines, and it omits the required checklist.

Resolution

Add a Tests run section with the exact commands executed and their result lines. Add and complete the repository checklist, including DCO, licensing, test coverage, error handling, distributed execution, API, dependency, and performance items.

Full details: Docstring Coverage

Explanation

Docstring coverage is 55.68% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 88 functions across 22 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/krishiv-exec/src/physical/instantiate.rs 42.8% 4 Missing ⚠️
crates/krishiv-exec/src/join/probe.rs 96.4% 1 Missing ⚠️
crates/krishiv-exec/src/physical/mod.rs 80.0% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 9 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 121 untouched benchmarks
⏩ 4 skipped benchmarks1


Comparing p2b/t115-mark-join (7bc0578) with main (c03aa5f)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@techgopal
techgopal added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit b5e06db Oct 25, 2026
47 checks passed
@techgopal
techgopal deleted the p2b/t115-mark-join branch October 25, 2026 21:02
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.

2 participants