use std::fmt;
use std::sync::Arc;
use fsqlite_btree::{BtCursor, BtreeCursorOps, PageWriter};
use fsqlite_error::FrankenError;
use fsqlite_types::record::{RecordProfileScope, enter_record_profile_scope, parse_record_into};
use fsqlite_types::value::SqliteValue;
use fsqlite_types::{Cx, PageNumber};
use crate::vectorized::{Batch, BatchFormatError, Column, ColumnData, ColumnSpec, SelectionVector};
const MAX_BATCH_CAPACITY: usize = u16::MAX as usize;
pub type RowPredicate = Arc<dyn Fn(i64, &[SqliteValue]) -> bool + Send + Sync + 'static>;
#[derive(Debug)]
pub enum VectorizedScanError {
Cursor(FrankenError),
Batch(BatchFormatError),
InvalidMorsel {
start: PageNumber,
end: PageNumber,
},
InvalidBatchCapacity(usize),
RecordDecode {
rowid: i64,
payload_len: usize,
},
SelectionIndexOverflow(usize),
SelectionIndexOutOfBounds {
column: String,
row_idx: usize,
row_count: usize,
},
OffsetOutOfBounds {
column: String,
row_idx: usize,
start: usize,
end: usize,
data_len: usize,
},
InvalidUtf8 {
column: String,
row_idx: usize,
},
}
impl fmt::Display for VectorizedScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cursor(err) => write!(f, "cursor error: {err}"),
Self::Batch(err) => write!(f, "batch format error: {err}"),
Self::InvalidMorsel { start, end } => {
write!(
f,
"invalid morsel range: start page {start} > end page {end}"
)
}
Self::InvalidBatchCapacity(capacity) => write!(
f,
"batch capacity must be in 1..={MAX_BATCH_CAPACITY}, got {capacity}"
),
Self::RecordDecode { rowid, payload_len } => write!(
f,
"failed to decode record payload for rowid {rowid} (payload_len={payload_len})"
),
Self::SelectionIndexOverflow(idx) => write!(
f,
"selection index {idx} does not fit into u16 selection vector entry"
),
Self::SelectionIndexOutOfBounds {
column,
row_idx,
row_count,
} => write!(
f,
"selection index {row_idx} is out of bounds for column {column} \
(row_count={row_count})"
),
Self::OffsetOutOfBounds {
column,
row_idx,
start,
end,
data_len,
} => write!(
f,
"column {column} has invalid offset range [{start}, {end}) for row {row_idx} \
(data_len={data_len})"
),
Self::InvalidUtf8 { column, row_idx } => {
write!(f, "column {column} row {row_idx} contains invalid UTF-8")
}
}
}
}
impl std::error::Error for VectorizedScanError {}
impl From<FrankenError> for VectorizedScanError {
fn from(value: FrankenError) -> Self {
Self::Cursor(value)
}
}
impl From<BatchFormatError> for VectorizedScanError {
fn from(value: BatchFormatError) -> Self {
Self::Batch(value)
}
}
pub type ScanResult<T> = std::result::Result<T, VectorizedScanError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageMorsel {
pub start_page: PageNumber,
pub end_page: PageNumber,
}
impl PageMorsel {
pub fn new(start_page: PageNumber, end_page: PageNumber) -> ScanResult<Self> {
if start_page > end_page {
return Err(VectorizedScanError::InvalidMorsel {
start: start_page,
end: end_page,
});
}
Ok(Self {
start_page,
end_page,
})
}
#[must_use]
pub fn contains(self, page_no: PageNumber) -> bool {
page_no >= self.start_page && page_no <= self.end_page
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScanBatchStats {
pub rows_scanned: usize,
pub rows_selected: usize,
pub pages_touched: Vec<PageNumber>,
pub prefetch_hints_issued: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScanBatch {
pub batch: Batch,
pub stats: ScanBatchStats,
}
pub struct VectorizedTableScan<P>
where
P: PageWriter,
{
cursor: BtCursor<P>,
cx: Cx,
specs: Vec<ColumnSpec>,
batch_capacity: usize,
predicate: Option<RowPredicate>,
morsel: Option<PageMorsel>,
payload_buf: Vec<u8>,
row_buffers: Vec<Vec<SqliteValue>>,
started: bool,
finished: bool,
last_prefetched_page: Option<PageNumber>,
}
impl<P> VectorizedTableScan<P>
where
P: PageWriter,
{
pub fn try_new(
cx: &Cx,
cursor: BtCursor<P>,
specs: Vec<ColumnSpec>,
batch_capacity: usize,
) -> ScanResult<Self> {
if batch_capacity == 0 || batch_capacity > MAX_BATCH_CAPACITY {
return Err(VectorizedScanError::InvalidBatchCapacity(batch_capacity));
}
Ok(Self {
cursor,
cx: cx.create_child(),
specs,
batch_capacity,
predicate: None,
morsel: None,
payload_buf: Vec::new(),
row_buffers: Vec::with_capacity(batch_capacity),
started: false,
finished: false,
last_prefetched_page: None,
})
}
#[must_use]
pub fn with_morsel(mut self, morsel: PageMorsel) -> Self {
self.morsel = Some(morsel);
self
}
#[must_use]
pub fn with_predicate(mut self, predicate: RowPredicate) -> Self {
self.predicate = Some(predicate);
self
}
#[allow(clippy::too_many_lines)]
pub fn next_batch(&mut self) -> ScanResult<Option<ScanBatch>> {
let _record_profile_scope =
enter_record_profile_scope(RecordProfileScope::VdbeVectorizedScan);
if self.finished {
return Ok(None);
}
if !self.started {
self.started = true;
self.cursor.clear_witness_keys();
if !self.cursor.first(&self.cx)? {
self.finished = true;
return Ok(None);
}
}
let mut selection_indices = Vec::with_capacity(self.batch_capacity);
let mut last_page_seen = None;
let mut pages_touched = Vec::new();
let mut prefetch_hints_issued = 0usize;
let mut row_count = 0usize;
let predicate = self.predicate.clone();
while row_count < self.batch_capacity {
if self.cursor.eof() {
self.finished = true;
break;
}
let current_page = self.current_page_or_internal_error()?;
if let Some(morsel) = self.morsel {
if current_page < morsel.start_page {
if !self.cursor.next(&self.cx)? {
self.finished = true;
break;
}
continue;
}
if current_page > morsel.end_page {
if !self.cursor.next(&self.cx)? {
self.finished = true;
break;
}
continue;
}
}
if last_page_seen != Some(current_page) {
pages_touched.push(current_page);
if let Some(prefetch_page) = self.next_prefetch_page(current_page) {
if self.last_prefetched_page != Some(prefetch_page) {
self.cursor.prefetch_page_hint(&self.cx, prefetch_page);
self.last_prefetched_page = Some(prefetch_page);
prefetch_hints_issued = prefetch_hints_issued.saturating_add(1);
}
}
last_page_seen = Some(current_page);
}
let rowid = self.cursor.rowid(&self.cx)?;
self.cursor.payload_into(&self.cx, &mut self.payload_buf)?;
let row = if row_count < self.row_buffers.len() {
&mut self.row_buffers[row_count]
} else {
self.row_buffers.push(Vec::new());
self.row_buffers
.last_mut()
.expect("row buffer push must yield a buffer")
};
parse_record_into(&self.payload_buf, row).ok_or(VectorizedScanError::RecordDecode {
rowid,
payload_len: self.payload_buf.len(),
})?;
let row_index = row_count;
if predicate
.as_ref()
.is_none_or(|predicate| predicate(rowid, row))
{
let selected = u16::try_from(row_index)
.map_err(|_| VectorizedScanError::SelectionIndexOverflow(row_index))?;
selection_indices.push(selected);
}
row_count += 1;
if !self.cursor.next(&self.cx)? {
self.finished = true;
break;
}
}
if row_count == 0 {
return Ok(None);
}
let mut batch = Batch::from_rows(
&self.row_buffers[..row_count],
&self.specs,
self.batch_capacity,
)?;
if selection_indices.len() != row_count {
batch.apply_selection(SelectionVector::from_indices(selection_indices))?;
}
let stats = ScanBatchStats {
rows_scanned: row_count,
rows_selected: batch.selection().len(),
pages_touched,
prefetch_hints_issued,
};
Ok(Some(ScanBatch { batch, stats }))
}
fn current_page_or_internal_error(&self) -> ScanResult<PageNumber> {
self.cursor.current_page().ok_or_else(|| {
VectorizedScanError::Cursor(FrankenError::internal(
"cursor positioned on row without a current page",
))
})
}
fn next_prefetch_page(&self, current_page: PageNumber) -> Option<PageNumber> {
let candidate = current_page
.get()
.checked_add(1)
.and_then(PageNumber::new)?;
if let Some(morsel) = self.morsel {
if candidate > morsel.end_page {
return None;
}
if candidate < morsel.start_page {
return None;
}
}
Some(candidate)
}
}
pub fn materialize_selected_rows(batch: &Batch) -> ScanResult<Vec<Vec<SqliteValue>>> {
let mut rows = Vec::with_capacity(batch.selection().len());
for &selected in batch.selection().as_slice() {
let row_idx = usize::from(selected);
let mut row = Vec::with_capacity(batch.columns().len());
for column in batch.columns() {
row.push(column_value_at(column, row_idx)?);
}
rows.push(row);
}
Ok(rows)
}
fn column_value_at(column: &Column, row_idx: usize) -> ScanResult<SqliteValue> {
if row_idx >= column.len() {
return Err(VectorizedScanError::SelectionIndexOutOfBounds {
column: column.spec.name.clone(),
row_idx,
row_count: column.len(),
});
}
if !column.validity.is_valid(row_idx) {
return Ok(SqliteValue::Null);
}
match &column.data {
ColumnData::Int8(values) => Ok(SqliteValue::Integer(i64::from(values.as_slice()[row_idx]))),
ColumnData::Int16(values) => {
Ok(SqliteValue::Integer(i64::from(values.as_slice()[row_idx])))
}
ColumnData::Int32(values) => {
Ok(SqliteValue::Integer(i64::from(values.as_slice()[row_idx])))
}
ColumnData::Int64(values) => Ok(SqliteValue::Integer(values.as_slice()[row_idx])),
ColumnData::Float32(values) => {
Ok(SqliteValue::Float(f64::from(values.as_slice()[row_idx])))
}
ColumnData::Float64(values) => Ok(SqliteValue::Float(values.as_slice()[row_idx])),
ColumnData::Binary { offsets, data } => {
let (start, end) =
checked_offset_span(offsets, data.len(), row_idx, &column.spec.name)?;
Ok(SqliteValue::Blob(data[start..end].to_vec().into()))
}
ColumnData::Text { offsets, data } => {
let (start, end) =
checked_offset_span(offsets, data.len(), row_idx, &column.spec.name)?;
let text = std::str::from_utf8(&data[start..end]).map_err(|_| {
VectorizedScanError::InvalidUtf8 {
column: column.spec.name.clone(),
row_idx,
}
})?;
Ok(SqliteValue::Text(text.to_owned().into()))
}
}
}
fn checked_offset_span(
offsets: &[u32],
data_len: usize,
row_idx: usize,
column: &str,
) -> ScanResult<(usize, usize)> {
if row_idx + 1 >= offsets.len() {
return Err(VectorizedScanError::OffsetOutOfBounds {
column: column.to_owned(),
row_idx,
start: 0,
end: 0,
data_len,
});
}
let start =
usize::try_from(offsets[row_idx]).map_err(|_| VectorizedScanError::OffsetOutOfBounds {
column: column.to_owned(),
row_idx,
start: 0,
end: 0,
data_len,
})?;
let end = usize::try_from(offsets[row_idx + 1]).map_err(|_| {
VectorizedScanError::OffsetOutOfBounds {
column: column.to_owned(),
row_idx,
start: 0,
end: 0,
data_len,
}
})?;
if start > end || end > data_len {
return Err(VectorizedScanError::OffsetOutOfBounds {
column: column.to_owned(),
row_idx,
start,
end,
data_len,
});
}
Ok((start, end))
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::rc::Rc;
use fsqlite_btree::{MemPageStore, PageReader};
use fsqlite_types::WitnessKey;
use fsqlite_types::record::{
parse_record, record_profile_enabled, record_profile_snapshot, reset_record_profile,
serialize_record, set_record_profile_enabled,
};
use super::*;
use crate::vectorized::{ColumnSpec, ColumnVectorType, DEFAULT_BATCH_ROW_CAPACITY};
const PAGE_SIZE: u32 = 512;
const ROOT_PAGE: u32 = 2;
const BEAD_ID: &str = "bd-14vp7.2";
#[derive(Clone, Debug)]
struct SharedTrackingPageIo {
store: Rc<RefCell<MemPageStore>>,
hinted_pages: Rc<RefCell<Vec<PageNumber>>>,
}
impl SharedTrackingPageIo {
fn new(page_size: u32, root_page: PageNumber) -> Self {
Self {
store: Rc::new(RefCell::new(MemPageStore::with_empty_table(
root_page, page_size,
))),
hinted_pages: Rc::new(RefCell::new(Vec::new())),
}
}
fn hinted_pages(&self) -> Vec<PageNumber> {
self.hinted_pages.borrow().clone()
}
}
impl PageReader for SharedTrackingPageIo {
fn read_page(&self, cx: &Cx, page_no: PageNumber) -> fsqlite_error::Result<Vec<u8>> {
self.store.borrow().read_page(cx, page_no)
}
fn prefetch_page_hint(&self, _cx: &Cx, page_no: PageNumber) {
self.hinted_pages.borrow_mut().push(page_no);
}
}
impl fsqlite_btree::PageWriter for SharedTrackingPageIo {
fn write_page(
&mut self,
cx: &Cx,
page_no: PageNumber,
data: &[u8],
) -> fsqlite_error::Result<()> {
self.store.borrow_mut().write_page(cx, page_no, data)
}
fn allocate_page(&mut self, cx: &Cx) -> fsqlite_error::Result<PageNumber> {
self.store.borrow_mut().allocate_page(cx)
}
fn free_page(&mut self, cx: &Cx, page_no: PageNumber) -> fsqlite_error::Result<()> {
self.store.borrow_mut().free_page(cx, page_no)
}
fn record_write_witness(&mut self, _cx: &Cx, _key: WitnessKey) {}
}
fn specs() -> Vec<ColumnSpec> {
vec![
ColumnSpec::new("c0", ColumnVectorType::Int64),
ColumnSpec::new("c1", ColumnVectorType::Float64),
ColumnSpec::new("c2", ColumnVectorType::Text),
ColumnSpec::new("c3", ColumnVectorType::Binary),
]
}
fn row_for_rowid(rowid: i64) -> Vec<SqliteValue> {
vec![
SqliteValue::Integer(rowid * 7),
SqliteValue::Float(rowid as f64 * 0.5),
SqliteValue::Text(format!("row-{rowid:05}").into()),
SqliteValue::Blob(
vec![
u8::try_from(rowid.rem_euclid(251)).expect("mod value should fit into u8"),
u8::try_from((rowid * 3).rem_euclid(251))
.expect("mod value should fit into u8"),
u8::try_from((rowid * 7).rem_euclid(251))
.expect("mod value should fit into u8"),
]
.into(),
),
]
}
fn build_fixture(row_count: usize) -> (SharedTrackingPageIo, PageNumber) {
let root_page = PageNumber::new(ROOT_PAGE).expect("root page should be non-zero");
let io = SharedTrackingPageIo::new(PAGE_SIZE, root_page);
let mut writer = BtCursor::new(io.clone(), root_page, PAGE_SIZE, true);
let cx = Cx::new();
for idx in 0..row_count {
let rowid = i64::try_from(idx + 1).expect("rowid should fit into i64");
let row = row_for_rowid(rowid);
let payload = serialize_record(&row);
writer
.table_insert(&cx, rowid, &payload)
.expect("table_insert should succeed");
}
(io, root_page)
}
fn collect_rows_row_at_a_time<F>(
io: SharedTrackingPageIo,
root_page: PageNumber,
morsel: Option<PageMorsel>,
predicate: F,
) -> (Vec<Vec<SqliteValue>>, Vec<PageNumber>)
where
F: Fn(i64, &[SqliteValue]) -> bool,
{
let mut cursor = BtCursor::new(io, root_page, PAGE_SIZE, true);
let cx = Cx::new();
let mut rows = Vec::new();
let mut pages = Vec::new();
let mut seen_pages = BTreeSet::new();
if !cursor.first(&cx).expect("first should succeed") {
return (rows, pages);
}
loop {
if cursor.eof() {
break;
}
let current_page = cursor
.current_page()
.expect("cursor at row should have current leaf page");
if let Some(m) = morsel {
if current_page < m.start_page {
if !cursor.next(&cx).expect("next should succeed") {
break;
}
continue;
}
if current_page > m.end_page {
if !cursor.next(&cx).expect("next should succeed") {
break;
}
continue;
}
}
if seen_pages.insert(current_page) {
pages.push(current_page);
}
let rowid = cursor.rowid(&cx).expect("rowid should succeed");
let payload = cursor.payload(&cx).expect("payload should succeed");
let row = parse_record(&payload).expect("payload should decode");
if predicate(rowid, &row) {
rows.push(row);
}
if !cursor.next(&cx).expect("next should succeed") {
break;
}
}
(rows, pages)
}
#[test]
fn scan_output_matches_row_at_a_time_output() {
let (io, root_page) = build_fixture(2_000);
let cx = Cx::new();
let scan_cursor = BtCursor::new(io.clone(), root_page, PAGE_SIZE, true);
let mut scan =
VectorizedTableScan::try_new(&cx, scan_cursor, specs(), DEFAULT_BATCH_ROW_CAPACITY)
.expect("scan should initialize");
let mut actual_rows = Vec::new();
let mut scanned_pages = BTreeSet::new();
while let Some(output) = scan.next_batch().expect("batch should scan successfully") {
for page in output.stats.pages_touched {
scanned_pages.insert(page);
}
let selected =
materialize_selected_rows(&output.batch).expect("selected rows should materialize");
actual_rows.extend(selected);
}
let (expected_rows, _) =
collect_rows_row_at_a_time(io, root_page, None, |_rowid, _row| true);
assert_eq!(
actual_rows, expected_rows,
"bead_id={BEAD_ID} full scan mismatch"
);
assert!(
scanned_pages.len() > 1,
"bead_id={BEAD_ID} expected multi-page scan to validate leaf traversal"
);
}
#[test]
fn filter_pushdown_updates_selection_vector() {
let (io, root_page) = build_fixture(1_500);
let cx = Cx::new();
let predicate: RowPredicate = Arc::new(|rowid, _row| rowid % 3 == 0);
let scan_cursor = BtCursor::new(io.clone(), root_page, PAGE_SIZE, true);
let mut scan = VectorizedTableScan::try_new(&cx, scan_cursor, specs(), 256)
.expect("scan should initialize")
.with_predicate(predicate.clone());
let mut actual_rows = Vec::new();
let mut saw_pushdown = false;
while let Some(output) = scan.next_batch().expect("batch should scan successfully") {
if output.stats.rows_selected < output.stats.rows_scanned {
saw_pushdown = true;
}
actual_rows.extend(
materialize_selected_rows(&output.batch).expect("selected rows should materialize"),
);
}
let (expected_rows, _) =
collect_rows_row_at_a_time(io, root_page, None, |rowid, _row| rowid % 3 == 0);
assert_eq!(
actual_rows, expected_rows,
"bead_id={BEAD_ID} predicate pushdown mismatch"
);
assert!(
saw_pushdown,
"bead_id={BEAD_ID} expected at least one filtered batch"
);
}
#[test]
fn scan_respects_page_morsel_boundaries() {
let (io, root_page) = build_fixture(3_000);
let cx = Cx::new();
let (_all_rows, all_pages) =
collect_rows_row_at_a_time(io.clone(), root_page, None, |_rowid, _row| true);
assert!(
all_pages.len() >= 3,
"bead_id={BEAD_ID} expected at least 3 pages for morsel boundary test"
);
let morsel = PageMorsel::new(all_pages[1], all_pages[2]).expect("morsel should be valid");
let scan_cursor = BtCursor::new(io.clone(), root_page, PAGE_SIZE, true);
let mut scan = VectorizedTableScan::try_new(&cx, scan_cursor, specs(), 192)
.expect("scan should initialize")
.with_morsel(morsel);
let mut actual_rows = Vec::new();
let mut touched_pages = BTreeSet::new();
while let Some(output) = scan.next_batch().expect("batch should scan successfully") {
for page in output.stats.pages_touched {
assert!(
morsel.contains(page),
"bead_id={BEAD_ID} page {page} escaped morsel {:?}",
morsel
);
touched_pages.insert(page);
}
actual_rows.extend(
materialize_selected_rows(&output.batch).expect("selected rows should materialize"),
);
}
let (expected_rows, expected_pages) =
collect_rows_row_at_a_time(io, root_page, Some(morsel), |_rowid, _row| true);
assert_eq!(
actual_rows, expected_rows,
"bead_id={BEAD_ID} morsel output mismatch"
);
let expected_page_set: BTreeSet<PageNumber> = expected_pages.into_iter().collect();
assert_eq!(
touched_pages, expected_page_set,
"bead_id={BEAD_ID} touched page set mismatch"
);
}
#[test]
fn prefetch_hints_are_emitted_during_scan() {
let (io, root_page) = build_fixture(2_500);
let cx = Cx::new();
let (all_rows, pages) =
collect_rows_row_at_a_time(io.clone(), root_page, None, |_rowid, _row| true);
assert!(
pages.len() >= 2,
"bead_id={BEAD_ID} expected at least two pages for prefetch test"
);
assert!(!all_rows.is_empty(), "fixture should contain rows");
let morsel = PageMorsel::new(pages[0], pages[1]).expect("morsel should be valid");
let scan_cursor = BtCursor::new(io.clone(), root_page, PAGE_SIZE, true);
let mut scan = VectorizedTableScan::try_new(&cx, scan_cursor, specs(), 128)
.expect("scan should initialize")
.with_morsel(morsel);
let mut total_hints = 0usize;
while let Some(output) = scan.next_batch().expect("batch should scan successfully") {
total_hints = total_hints.saturating_add(output.stats.prefetch_hints_issued);
}
let hinted_pages = io.hinted_pages();
assert!(
total_hints > 0,
"bead_id={BEAD_ID} expected scan to issue explicit prefetch hints"
);
assert!(
!hinted_pages.is_empty(),
"bead_id={BEAD_ID} expected page-reader prefetch hints"
);
}
#[test]
fn scan_rejects_batch_capacity_beyond_selection_vector_limit() {
let (io, root_page) = build_fixture(1);
let cx = Cx::new();
let scan_cursor = BtCursor::new(io, root_page, PAGE_SIZE, true);
let capacity = MAX_BATCH_CAPACITY.saturating_add(1);
let err = match VectorizedTableScan::try_new(&cx, scan_cursor, specs(), capacity) {
Ok(_) => panic!("oversized batch capacity should be rejected up front"),
Err(err) => err,
};
assert!(matches!(
err,
VectorizedScanError::InvalidBatchCapacity(value) if value == capacity
));
}
#[test]
fn column_value_at_reports_out_of_bounds_selection_index() {
let column = Column {
spec: ColumnSpec::new("c0", ColumnVectorType::Int64),
data: ColumnData::Int64(
crate::vectorized::AlignedValues::from_vec(vec![7_i64], 8)
.expect("aligned values should build"),
),
validity: crate::vectorized::NullBitmap::all_valid(1),
};
let err = column_value_at(&column, 3).expect_err("out-of-bounds row index should error");
assert!(matches!(
err,
VectorizedScanError::SelectionIndexOutOfBounds {
column,
row_idx: 3,
row_count: 1,
} if column == "c0"
));
}
#[test]
fn scan_reuses_decode_scratch_and_avoids_full_record_parse_calls() {
let (io, root_page) = build_fixture(257);
let prev_record_profile_enabled = record_profile_enabled();
reset_record_profile();
set_record_profile_enabled(true);
let cx = Cx::new();
let scan_cursor = BtCursor::new(io, root_page, PAGE_SIZE, true);
let mut scan = VectorizedTableScan::try_new(&cx, scan_cursor, specs(), 64)
.expect("scan should initialize");
let first_batch = scan
.next_batch()
.expect("first batch should scan successfully")
.expect("first batch should exist");
assert_eq!(first_batch.stats.rows_scanned, 64);
let first_row_buf_ptr = scan.row_buffers[0].as_ptr() as usize;
let first_payload_buf_ptr = scan.payload_buf.as_ptr() as usize;
let second_batch = scan
.next_batch()
.expect("second batch should scan successfully")
.expect("second batch should exist");
assert_eq!(second_batch.stats.rows_scanned, 64);
assert_eq!(
scan.row_buffers[0].as_ptr() as usize,
first_row_buf_ptr,
"bead_id={BEAD_ID} first row buffer should be reused across batches"
);
assert_eq!(
scan.payload_buf.as_ptr() as usize,
first_payload_buf_ptr,
"bead_id={BEAD_ID} payload buffer should be reused across batches"
);
let mut total_rows = first_batch.stats.rows_scanned + second_batch.stats.rows_scanned;
while let Some(output) = scan
.next_batch()
.expect("scan should continue successfully")
{
total_rows += output.stats.rows_scanned;
}
let snapshot = record_profile_snapshot();
assert_eq!(total_rows, 257);
assert_eq!(
snapshot
.callsite_breakdown
.vdbe_vectorized_scan
.parse_record_calls,
0,
"bead_id={BEAD_ID} vectorized scan should avoid full parse_record calls"
);
assert_eq!(
snapshot
.callsite_breakdown
.vdbe_vectorized_scan
.parse_record_into_calls,
257,
"bead_id={BEAD_ID} vectorized scan should decode through reusable parse_record_into scratch"
);
set_record_profile_enabled(prev_record_profile_enabled);
}
}