diff --git a/docs/sql-engine.md b/docs/sql-engine.md index 3cbcdd0..9ebf30a 100644 --- a/docs/sql-engine.md +++ b/docs/sql-engine.md @@ -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)`, 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` 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` 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` 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>` 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 ''`, `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).) diff --git a/src/sql/executor.rs b/src/sql/executor.rs index 3636cfd..c223708 100644 --- a/src/sql/executor.rs +++ b/src/sql/executor.rs @@ -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 @@ -759,18 +764,25 @@ fn execute_select_rows_joined(query: SelectQuery, db: &Database) -> Result] = &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> = 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> = 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 @@ -785,7 +797,7 @@ fn execute_select_rows_joined(query: SelectQuery, db: &Database) -> Result