Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sql-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The `sqlparser` AST is designed to cover every SQL dialect, so its types are hug

`SelectQuery::projection` is now `Projection::All | Projection::Items(Vec<ProjectionItem>)`, where each item carries a `ProjectionKind::Column { qualifier, name }` (qualifier is `Some` for `t.col` shapes, used by JOIN execution to disambiguate) or `ProjectionKind::Aggregate(AggregateCall)` plus an optional `AS alias`. `AggregateCall` covers `COUNT(*)`, `COUNT([DISTINCT] col)`, `SUM` / `AVG` / `MIN` / `MAX` of a column reference (optionally qualified, `SUM(o.amount)`). `group_by` is a `Vec<GroupByKey>` of optionally-qualified column references (`GROUP BY dept`, `GROUP BY customers.name`; empty = no GROUP BY). The parser validates that every non-aggregate projection item appears in `GROUP BY` for single-table queries; joined queries defer that check to the executor, which resolves qualifiers against the in-scope table schemas (SQLR-6).

`SelectQuery::joins` (SQLR-5) is a `Vec<JoinClause>` evaluated left-to-right by `execute_select_rows_joined`. Each clause carries a `JoinType` (`Inner` / `LeftOuter` / `RightOuter` / `FullOuter`), the right-table name + optional alias, and a required `ON` expression. Empty = single-table SELECT, the existing fast path with HNSW / FTS / bounded-heap optimizations.
`SelectQuery::joins` (SQLR-5) is a `Vec<JoinClause>` evaluated left-to-right by `execute_select_rows_joined`. Each clause carries a `JoinType` (`Inner` / `LeftOuter` / `RightOuter` / `FullOuter`), the right-table name + optional alias, and a required `ON` expression. Empty = single-table SELECT, the existing fast path with HNSW / FTS / bounded-heap optimizations. Nested-loop matching still walks every (left, right) pair; SQLR-4 reuses a scratch `Vec<Option<i64>>` per join fold so non-matching pairs no longer heap-allocate (matches still clone into the accumulator).

Each parser module still rejects features we don't implement with `SQLRiteError::NotImplemented` — comma joins (`FROM a, b`), `HAVING` without `GROUP BY`, `DISTINCT ON (...)`, `GROUP BY` on expressions, `LIKE … ESCAPE '<char>'`, `IN (subquery)`, `OFFSET`, multi-table DELETE, tuple assignment targets, etc. These errors carry the feature name in the message so the user knows what isn't there. (`JOIN ... USING`, `NATURAL JOIN`, and `CROSS JOIN` are now supported — see [`supported-sql.md`](supported-sql.md#join-semantics-sqlr-5).)

Expand Down
28 changes: 20 additions & 8 deletions src/sql/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,11 @@ fn col_eq(left_scope: &str, right_scope: &str, col: &str) -> Expr {
// learning database" niche; a future phase could layer hash / merge
// joins on equi-join shapes without changing the surface API.
//
// SQLR-4 — the nested loop used to clone the left row for every
// right candidate (`N×M` heap allocs, most dropped on non-match).
// A reused scratch vec now allocates only on matches (plus one
// buffer per join fold). The algorithmic bound is unchanged.
//
// SQLR-6 — aggregates / GROUP BY / DISTINCT compose with joins: the
// fully-joined row stream feeds the same scope-generic aggregation
// pipeline the single-table path uses (Stage 3.5 below), and DISTINCT
Expand Down Expand Up @@ -759,18 +764,25 @@ fn execute_select_rows_joined(query: SelectQuery, db: &Database) -> Result<Selec
// silently `NULL → false`-ing every row.
let on_scope_tables: &[JoinedTableRef<'_>] = &joined_tables[..=right_pos];

// SQLR-4 — one scratch row for this join fold. Capacity is
// the eventual full join width so later folds do not grow
// the buffer; `len` is the in-scope width (`right_pos + 1`)
// so `JoinedScope` still sees only tables joined so far.
let mut scratch: Vec<Option<i64>> = Vec::with_capacity(joined_tables.len());

for left_row in acc.into_iter() {
// Build a row prefix and extend it with each candidate
// right rowid; record whether any matched (for outer
// padding on the left side).
// Fill the left prefix once, then overwrite the trailing
// slot per right rowid. Non-matches no longer allocate.
let mut left_match_count = 0usize;
scratch.clear();
scratch.extend_from_slice(&left_row);
scratch.push(None);
debug_assert_eq!(scratch.len(), on_scope_tables.len());
for (r_idx, &rrid) in right_rowids.iter().enumerate() {
let mut on_rowids: Vec<Option<i64>> = left_row.clone();
on_rowids.push(Some(rrid));
debug_assert_eq!(on_rowids.len(), on_scope_tables.len());
scratch[right_pos] = Some(rrid);
let scope = JoinedScope {
tables: on_scope_tables,
rowids: &on_rowids,
rowids: &scratch,
};
// Reuse `eval_predicate_scope` so ON shares the same
// truthiness rule WHERE uses — non-zero integers are
Expand All @@ -785,7 +797,7 @@ fn execute_select_rows_joined(query: SelectQuery, db: &Database) -> Result<Selec
// as join levels processed so far; the next
// iteration extends them again. No trailing
// padding needed here.
next_acc.push(on_rowids);
next_acc.push(scratch.clone());
}
}

Expand Down
Loading