From edd6aa8f41fce1c48f7437e8a32072f98fc1ddf9 Mon Sep 17 00:00:00 2001 From: Jepson Taylor <130782313+jepsontaylor@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:44:40 +0000 Subject: [PATCH] Decode each unordered table-scan row once. The heap scan used to load every visible row to drop it, then load the survivors again while projecting. It now keeps that first load, applies WHERE, and projects into the static batch that already honors LIMIT and OFFSET. Point and index probes still fetch by rowid. The routed scan, when enabled, uses the same single load. Claim: AGENT_CHAT.md 2026-09-25T19:10Z perf/one-pass-scan. Acceptance: cargo test -p redlinedb-sql --offline --test ci_safe_order --test smoke_select --- AGENT_CHAT.md | 4 +++ crates/sql/src/exec/morsel/route.rs | 10 ++---- crates/sql/src/exec/select_top.rs | 51 ++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/AGENT_CHAT.md b/AGENT_CHAT.md index b3986c62..53fcd0c0 100644 --- a/AGENT_CHAT.md +++ b/AGENT_CHAT.md @@ -1,5 +1,9 @@ # RedlineDB Agent Chat +## 2026-09-25T19:10Z perf/one-pass-scan + +Claim: canonical checkout `/home/ubuntu/redlineDB`, branch `perf/one-pass-scan` from `origin/main` `1e21f23caa5842ca2469ec444a6edc4ac4bf36fa`. An unordered table scan keeps the row from the visibility load instead of decoding it again. Index probes stay rowid lookups. Join order and `LIMIT 1` stay as locked. No speed ratio. No worktree. + ## 2026-09-25T18:40Z fix/kernel-p0-gates Claim: canonical checkout `/home/ubuntu/redlineDB`, branch `fix/kernel-p0-gates` from `origin/main` `76ba2a52c469c8917929418ff1f4fe8f5b6623cf`. Clock eviction can decay a hot clean frame, a free row lock is not granted ahead of a queued waiter, a WAL segment that fills exactly does not grow past its limit, and a morsel arena offset that does not fit in `u32` aborts instead of truncating. No speed claim. No worktree. diff --git a/crates/sql/src/exec/morsel/route.rs b/crates/sql/src/exec/morsel/route.rs index 6a58f95b..b3e463d3 100644 --- a/crates/sql/src/exec/morsel/route.rs +++ b/crates/sql/src/exec/morsel/route.rs @@ -629,14 +629,10 @@ pub(crate) fn execute_routed_scan( } }; - let rowids = super::super::collect_table_rowids(engine, tx, table)?; - let mut out: Vec> = Vec::with_capacity(rowids.len()); - - for rowid in rowids { - let Some(fresh) = super::super::load_table_row_by_rowid(engine, tx, table, rowid)? else { - continue; - }; + let rows = super::super::collect_table_rows(engine, tx, table)?; + let mut out: Vec> = Vec::with_capacity(rows.len()); + for fresh in rows { // W4-A3/A5: predicate evaluation before projection. SQL's three- // valued comparison: NULL anything = NULL, treated as // "not matching" (same as the tuple path's filter). For a diff --git a/crates/sql/src/exec/select_top.rs b/crates/sql/src/exec/select_top.rs index 28a838c3..95dc6505 100644 --- a/crates/sql/src/exec/select_top.rs +++ b/crates/sql/src/exec/select_top.rs @@ -3,6 +3,31 @@ use super::select_parallel::{ }; use super::*; +/// One visibility load, then project. The unordered heap scan used to +/// decode each row to drop invisible versions and decode the survivors +/// again in the table cursor. +fn visible_table_scan( + conn: &Connection, + tx: &mut Txn, + table: &Arc, + plan: &crate::statement::SelectPlan, + bindings: &[Option], +) -> Result { + let loaded = collect_table_rows(conn.engine(), tx, table)?; + let mut projected = Vec::with_capacity(loaded.len()); + for row in loaded { + let row = SqlRow::Table(row); + if !selection_passes(&plan.selection, &row, bindings)? { + continue; + } + projected.push(project_row(&plan.projection, &row, bindings)?); + } + Ok(SelectRuntimeSource::StaticRows { + rows: Arc::from(projected), + cursor: 0, + }) +} + pub(super) fn begin_select_tx(conn: &Connection) -> Result<(SelectRuntimeTx, bool)> { if let Some(tx_ptr) = current_tx() { return Ok((SelectRuntimeTx::Borrowed(tx_ptr), false)); @@ -400,8 +425,12 @@ fn build_select_runtime( } else { selection_rowid_eq(table, &plan.selection, bindings)? }; - let rowids = if let Some(rowid) = rowid_candidate { - vec![rowid] + if let Some(rowid) = rowid_candidate { + SelectRuntimeSource::Table { + table: Arc::clone(table), + rowids: vec![rowid], + cursor: 0, + } } else if let Some(matched) = index_access::try_match_index_access_hinted( conn.engine(), table, @@ -419,24 +448,24 @@ fn build_select_runtime( // executor can satisfy them, but a lagging // schema snapshot can still exist. if index_access::open_handle(conn.engine(), &matched.index).is_some() { - index_access::execute_index_probe( + let rowids = index_access::execute_index_probe( conn.engine(), tx, table, &matched.index, &matched.probe, - )? + )?; + SelectRuntimeSource::Table { + table: Arc::clone(table), + rowids, + cursor: 0, + } } else { - collect_table_rowids(conn.engine(), tx, table)? + visible_table_scan(conn, tx, table, plan, bindings)? } } else { let tx = tx.as_mut().expect("tx present"); - collect_table_rowids(conn.engine(), tx, table)? - }; - SelectRuntimeSource::Table { - table: Arc::clone(table), - rowids, - cursor: 0, + visible_table_scan(conn, tx, table, plan, bindings)? } } else if let Some(rowids) = try_ordered_index_limit_path( conn,