Skip to content

feat(sql): JOINs — INNER / LEFT / RIGHT / FULL OUTER (SQLR-5)#99

Merged
joaoh82 merged 1 commit into
mainfrom
feat/sql-joins
May 6, 2026
Merged

feat(sql): JOINs — INNER / LEFT / RIGHT / FULL OUTER (SQLR-5)#99
joaoh82 merged 1 commit into
mainfrom
feat/sql-joins

Conversation

@joaoh82

@joaoh82 joaoh82 commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements the four-flavor JOIN quartet (INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER) with explicit ON conditions and table aliases.
  • SQLite ships only INNER and LEFT OUTER; we ship all four because the per-flavor delta on top of a shared nested-loop driver is just NULL-padding policy. See docs/design-decisions.md §14a for the reasoning.
  • Adds a RowScope trait so the existing single-table fast path is preserved verbatim while JoinedScope handles multi-table resolution with proper NULL padding.

What changed

Engine (src/sql/executor.rs)

  • New RowScope trait + SingleTableScope (legacy) and JoinedScope (multi-table) implementations.
  • eval_expr / eval_predicate / eval_function / json_fn_* / FTS helpers / vector-arg extractor refactored to take &dyn RowScope.
  • execute_select_rows_joined: left-folded nested-loop driver, O(N×M) per join level. ON only sees tables in scope at that join level (forward references error). ON reuses eval_predicate_scope so non-zero ints are truthy, matching WHERE.

Parser (src/sql/parser/select.rs)

  • SelectQuery gains table_alias and joins: Vec<JoinClause>.
  • ProjectionKind::Column carries an optional qualifier so t.col references can disambiguate across tables.
  • USING / NATURAL / CROSS / comma joins surface as NotImplemented.
  • Aggregates / GROUP BY / DISTINCT over JOIN rejected at parse time.

Tests: 17 new tests covering all four flavors, NULL padding both directions, qualified/unqualified column resolution, ambiguity detection, self-join rejection, three-table chaining, chained LEFT OUTER, NULL ordering on outer joins, ON-references-later-table errors, and truthy-int ON. Total 526 tests passing, 0 failures.

Docs: README.md, docs/supported-sql.md (new "JOIN semantics" section + per-flavor table), docs/design-decisions.md (§14a rationale), docs/architecture.md, docs/sql-engine.md all updated.

Caveats / follow-ups (filed separately)

  • JoinedScope errors on missing/ambiguous columns while SingleTableScope returns Value::Null — long-standing inconsistency, project-wide.
  • Many JoinedScope errors classify as SQLRiteError::Internal when they're really user-input issues.
  • Allocation pressure in the inner join loop is 2·N·M heap allocs; reusable scratch buffer is a future optimization.
  • JOIN ... USING and NATURAL JOIN still NotImplemented.
  • Aggregates / GROUP BY / DISTINCT over JOIN need wiring through the existing aggregator.

Test plan

  • cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs — 526 pass, 0 fail
  • cargo fmt --all -- --check
  • cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets — no new warnings
  • REPL smoke-tested LEFT OUTER and FULL OUTER on a customers/orders fixture

🤖 Generated with Claude Code

Adds the four-flavor JOIN quartet with explicit ON conditions and
table aliases. SQLite ships only INNER and LEFT OUTER; we implement
RIGHT and FULL OUTER as well because the per-flavor delta on top of
a shared nested-loop driver is just NULL-padding policy. See
docs/design-decisions.md §14a for the reasoning.

Engine changes:
  - New RowScope trait abstracts column lookup; SingleTableScope
    preserves the legacy fast path verbatim, JoinedScope handles
    multi-table resolution with NULL padding.
  - eval_expr / eval_predicate / eval_function / json_fn_* / FTS
    helpers / vector-arg extractor refactored to take &dyn RowScope.
  - execute_select_rows_joined: left-folded nested-loop driver,
    O(N×M) per join level. ON only sees tables in scope at that
    join level (forward refs error). ON reuses eval_predicate_scope
    so non-zero ints are truthy, matching WHERE.
  - Parser: SelectQuery gains table_alias + joins; ProjectionKind
    Column carries an optional qualifier for t.col disambiguation.
    USING / NATURAL / CROSS / comma joins, plus aggregates / GROUP
    BY / DISTINCT over JOIN, surface as friendly NotImplemented.

17 new tests covering all four flavors, NULL padding both ways,
qualified/unqualified resolution, ambiguity, self-join rejection,
three-table chaining, chained LEFT OUTER, NULL ordering on outer
joins, ON-references-later-table errors, and truthy-int ON.

Docs: README + supported-sql.md + design-decisions.md +
architecture.md + sql-engine.md updated.

526 tests passing, 0 failures. Build, fmt, clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit 998329e into main May 6, 2026
16 checks passed
joaoh82 added a commit that referenced this pull request Jun 2, 2026
PR #99 shipped INNER/LEFT/RIGHT/FULL OUTER JOIN ... ON; the three
related shapes SQLite supports were still rejected as NotImplemented.
This wires all three through the existing nested-loop join driver.

Parser (src/sql/parser/select.rs):
- JoinClause.on: Expr → constraint: JoinConstraintKind (On/Using/Natural).
- USING (cols) is narrowed to a list of column names; NATURAL is carried
  as-is (it needs schemas the parser doesn't have); CROSS JOIN is
  rewritten to ON true at parse time.

Executor (src/sql/executor.rs):
- resolve_join_constraint lowers USING/NATURAL into the synthesized
  `left.col = right.col [AND …]` predicate, schema-aware: the left
  qualifier is picked per column so join chains resolve correctly, and
  NATURAL auto-discovers the shared column names (none ⇒ cross product,
  matching SQLite).
- SELECT * de-duplicates USING/NATURAL columns per the SQLite
  convention — the joined-on column shows once, taking the left copy.

Tests: 8 new executor tests (USING≡ON rows, SELECT* dedup for USING and
NATURAL, NATURAL multi-column AND, NATURAL-without-common-cols cross
product, CROSS cartesian product, LEFT OUTER USING, USING unknown-column
error), replacing the old "returns NotImplemented" test. Docs updated
(supported-sql, sql-engine, roadmap, README, web docs).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant