Skip to main content

fsqlite_vdbe/
engine.rs

1//! VDBE bytecode interpreter — the fetch-execute engine.
2//!
3//! Takes a [`VdbeProgram`] (produced by codegen) and executes it instruction by
4//! instruction. The engine maintains a register file (`Vec<SqliteValue>`) and
5//! accumulates result rows emitted by `OP_ResultRow`.
6//!
7//! This implementation covers the core opcode set needed for expression
8//! evaluation, control flow, arithmetic, comparison, and row output.
9//! Cursor-based opcodes (OpenRead, Rewind, Next, Column, etc.) are stubbed
10//! and will be wired to the B-tree layer in Phase 5.
11
12#![allow(
13    clippy::match_same_arms,
14    clippy::inline_always,
15    clippy::unnecessary_wraps,
16    clippy::option_option,
17    clippy::useless_let_if_seq,
18    clippy::collapsible_if
19)]
20use hashbrown::{HashMap, HashSet};
21use std::any::Any;
22use std::borrow::Cow;
23#[cfg(test)]
24use std::cell::Cell;
25use std::cell::RefCell;
26use std::cmp::Ordering;
27use std::collections::{BTreeMap, BTreeSet, VecDeque};
28
29use fsqlite_btree::swiss_index::SwissIndex;
30use fsqlite_types::PageSize;
31use std::rc::Rc;
32
33/// bd-perf (V1.6): Flat array replacing HashMap for cursor storage.
34/// Cursor IDs are small non-negative integers (0-15 typical). Direct array
35/// indexing is ~10-20x faster than HashMap probing for every cursor access.
36/// API matches SwissIndex<i32, V> so call sites need minimal changes.
37#[derive(Debug)]
38struct CursorSlots<V> {
39    slots: Vec<Option<V>>,
40}
41
42impl<V> Default for CursorSlots<V> {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48#[allow(clippy::trivially_copy_pass_by_ref)]
49impl<V> CursorSlots<V> {
50    #[inline]
51    fn new() -> Self {
52        Self { slots: Vec::new() }
53    }
54
55    #[inline]
56    fn ensure_slot(&mut self, id: i32) {
57        // SAFETY FIX: Guard against negative IDs which would wrap to usize::MAX
58        if id < 0 {
59            return;
60        }
61        let idx = id as usize;
62        if idx >= self.slots.len() {
63            self.slots.resize_with(idx + 1, || None);
64        }
65    }
66
67    #[inline]
68    pub fn get(&self, id: &i32) -> Option<&V> {
69        if *id < 0 {
70            return None;
71        }
72        self.slots.get(*id as usize).and_then(Option::as_ref)
73    }
74
75    #[inline]
76    pub fn get_mut(&mut self, id: &i32) -> Option<&mut V> {
77        if *id < 0 {
78            return None;
79        }
80        self.slots.get_mut(*id as usize).and_then(Option::as_mut)
81    }
82
83    #[inline]
84    pub fn insert(&mut self, id: i32, value: V) -> Option<V> {
85        if id < 0 {
86            return None;
87        }
88        self.ensure_slot(id);
89        self.slots[id as usize].replace(value)
90    }
91
92    #[inline]
93    pub fn remove(&mut self, id: &i32) -> Option<V> {
94        if *id < 0 {
95            return None;
96        }
97        let idx = *id as usize;
98        if idx < self.slots.len() {
99            self.slots[idx].take()
100        } else {
101            None
102        }
103    }
104
105    #[inline]
106    pub fn contains_key(&self, id: &i32) -> bool {
107        if *id < 0 {
108            return false;
109        }
110        self.slots.get(*id as usize).is_some_and(Option::is_some)
111    }
112
113    #[inline]
114    pub fn clear(&mut self) {
115        for slot in &mut self.slots {
116            *slot = None;
117        }
118    }
119
120    #[inline]
121    pub fn is_empty(&self) -> bool {
122        self.slots.iter().all(Option::is_none)
123    }
124
125    pub fn values(&self) -> impl Iterator<Item = &V> {
126        self.slots.iter().filter_map(Option::as_ref)
127    }
128
129    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
130        self.slots.iter_mut().filter_map(Option::as_mut)
131    }
132
133    /// Iterate over occupied (key, &value) pairs. Keys are derived from slot indices.
134    /// Used by diagnostic/parity-cert paths, not on the hot path.
135    #[allow(clippy::cast_possible_wrap)]
136    pub fn iter(&self) -> impl Iterator<Item = (i32, &V)> {
137        self.slots
138            .iter()
139            .enumerate()
140            .filter_map(|(i, slot)| slot.as_ref().map(|v| (i as i32, v)))
141    }
142}
143use fsqlite_types::sync_primitives::{Duration, Instant};
144use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
145use std::sync::{Arc, LazyLock, Mutex};
146
147use fsqlite_btree::cursor::{CursorPositionStamp, FirstIndexKeyIntegerLocalRunSegment};
148use fsqlite_btree::{
149    BtCursor, BtreeCursorOps, BtreePageHeader, BtreePageType, MemPageStore, PageReader, PageWriter,
150    SeekResult, header_offset_for_page,
151};
152use fsqlite_error::{ErrorCode, FrankenError, Result};
153use fsqlite_func::collation::CollationRegistry;
154use fsqlite_func::vtab::ColumnContext;
155use fsqlite_func::{
156    ApplicationFunctionKind, ErasedAggregateFunction, ErasedWindowFunction, FunctionRegistry,
157    ResolvedScalarFunction,
158};
159use fsqlite_mvcc::ConcurrentPageState;
160use fsqlite_mvcc::{
161    AllocatorKey, CommitIndex, CommitLog, ConcurrentRowIdAllocator, InProcessPageLockTable,
162    MvccError, SharedConcurrentHandle, TimeTravelSnapshot, TimeTravelTarget, VersionStore,
163    concurrent_clear_page_state, concurrent_free_page, concurrent_has_page_state,
164    concurrent_page_is_synthetic_conflict_only, concurrent_page_read_status, concurrent_page_state,
165    concurrent_prepare_write_page, concurrent_restore_page_state,
166    concurrent_stage_prepared_write_marker, concurrent_track_write_conflict_page,
167    create_time_travel_snapshot,
168};
169#[cfg(test)]
170use fsqlite_mvcc::{concurrent_read_page, concurrent_write_page};
171use fsqlite_pager::{TransactionHandle, TransactionKind};
172use fsqlite_types::cx::Cx;
173use fsqlite_types::opcode::{
174    Opcode, P4, SORTER_COMPARE_TOP_N_PREFLIGHT, SORTER_OPEN_TOP_N_REGISTER, VdbeOp,
175};
176use fsqlite_types::record::{
177    ColumnOffset, PrecomputedSerialTypeKind, RecordProfileScope, enter_record_profile_scope,
178    parse_record, record_iter_with_precomputed_header_exact_size, serialize_record,
179    serialize_record_iter_with_precomputed_header_into,
180    serialize_record_iter_with_precomputed_header_into_slice, simd_serialize_integer_record,
181};
182use fsqlite_types::serial_type::{
183    SerialTypeClass, classify_serial_type, read_varint, serial_type_len,
184};
185use fsqlite_types::value::{
186    SmallText, SqlLikeFastPathKind, SqliteValue, pool_return_reusable,
187    sql_like_fast_path_matches_cased,
188};
189use fsqlite_types::{
190    CommitSeq, DATABASE_HEADER_SIZE, DatabaseHeader, PageData, PageNumber, RowId, RowIdMode,
191    SchemaEpoch, StrictColumnType, TableId, WitnessKey,
192};
193
194use crate::{
195    SchemaEvaluationContext, TableIndexMetaMap, VdbeProgram, enter_vdbe_decode_profile_stage,
196    enter_vdbe_execute_profile_stage,
197    jit::{
198        CompiledProgram, CompiledRecordBuilder, ConstantResultRowTemplate, FullScanSelectTemplate,
199        InsertValueSource, RowidLookupSelectTemplate, SimpleInsertTemplate, try_compile_program,
200    },
201    opcode_register_spans,
202};
203
204const VDBE_EXECUTION_CHECKPOINT_INTERVAL: u64 = 4096;
205
206fn validate_schema_function_invocation(
207    context: SchemaEvaluationContext,
208    function_name: &str,
209    safety: fsqlite_func::ScalarSchemaSafety,
210    args: &[SqliteValue],
211) -> Result<()> {
212    let safe = match safety {
213        fsqlite_func::ScalarSchemaSafety::Always => true,
214        // SQLite deliberately permits ordinary nondeterministic functions and
215        // CURRENT_* literals in CHECK constraints. Index and generated-column
216        // values, by contrast, must remain reproducible from stored row data.
217        fsqlite_func::ScalarSchemaSafety::Never => {
218            context == SchemaEvaluationContext::CheckConstraint
219        }
220        fsqlite_func::ScalarSchemaSafety::DateTimeConditional => {
221            fsqlite_func::datetime::is_datetime_invocation_safe_for_schema(function_name, args)
222        }
223    };
224    if safe {
225        Ok(())
226    } else {
227        let owner = match context {
228            SchemaEvaluationContext::Index => "an index",
229            SchemaEvaluationContext::GeneratedColumn => "a generated column",
230            SchemaEvaluationContext::CheckConstraint => "a CHECK constraint",
231        };
232        Err(FrankenError::function_error(format!(
233            "non-deterministic use of {}() in {owner}",
234            function_name.to_ascii_lowercase(),
235        )))
236    }
237}
238
239/// FrankenSQLite-specific p5 flag for `Insert`/`Delete` opcodes emitted from
240/// UPDATE rewrites.
241///
242/// The low 4 bits of `Insert.p5` are reserved for OE_* conflict behavior in
243/// this engine, so the UPDATE marker must live above them.
244const OPFLAG_ISUPDATE: u16 = 0x10;
245/// Marks a WITHOUT ROWID table-row `IdxDelete` whose deleted logical row is an
246/// exact REPLACE victim needed by connection-layer inbound FK enforcement.
247const OPFLAG_REPLACE_VICTIM: u16 = 0x20;
248/// Counts a successful clustered-table `IdxInsert` as one logical row change.
249/// This is reserved for WITHOUT ROWID table roots, never secondary indexes.
250const OPFLAG_IDX_NCHANGE: u16 = 0x40;
251/// Marks a `Halt(SQLITE_CONSTRAINT)` emitted after a non-mutating UNIQUE
252/// preflight. P4 contains the constraint's column label.
253const OPFLAG_HALT_UNIQUE: u16 = 0x01;
254const STORAGE_CURSOR_LAYOUT_PREFIX_BYTES: usize = 256;
255#[cfg(test)]
256const VDBE_ENGINE_INLINE_SIZE_BUDGET_BYTES: usize = 3 * 1024;
257static BUILTIN_COLLATION_REGISTRY: LazyLock<CollationRegistry> =
258    LazyLock::new(CollationRegistry::default);
259
260#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
261struct StatementColdState(u16);
262
263impl StatementColdState {
264    const AGGREGATES: Self = Self(1 << 0);
265    const CONFLICT_TRACKING: Self = Self(1 << 1);
266    const ROWSETS: Self = Self(1 << 2);
267    const SEQUENCE_COUNTERS: Self = Self(1 << 3);
268    const VTAB_CURSORS: Self = Self(1 << 4);
269    const WINDOW_CONTEXTS: Self = Self(1 << 5);
270    const REGISTER_SUBTYPES: Self = Self(1 << 6);
271    const BLOOM_FILTERS: Self = Self(1 << 7);
272
273    #[must_use]
274    const fn empty() -> Self {
275        Self(0)
276    }
277
278    #[cfg(test)]
279    #[must_use]
280    #[allow(dead_code)]
281    const fn contains(self, other: Self) -> bool {
282        self.0 & other.0 != 0
283    }
284
285    #[must_use]
286    const fn is_empty(self) -> bool {
287        self.0 == 0
288    }
289
290    fn insert(&mut self, other: Self) {
291        self.0 |= other.0;
292    }
293
294    fn clear(&mut self) {
295        self.0 = 0;
296    }
297}
298
299#[inline]
300fn observe_execution_cancellation(cx: &Cx) -> Result<()> {
301    cx.checkpoint().map_err(|_| FrankenError::Abort)
302}
303
304const MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES: usize = 9;
305const MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN: usize = 64;
306const MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MAX: usize = 512;
307
308#[inline]
309fn compute_record_header_size_hint(content_size: usize) -> usize {
310    let mut header_size = content_size + 1;
311    loop {
312        let needed = fsqlite_types::serial_type::varint_len(header_size as u64) + content_size;
313        if needed <= header_size {
314            return header_size;
315        }
316        header_size = needed;
317    }
318}
319
320#[inline]
321fn make_record_variable_width_reserve(page_size: PageSize) -> usize {
322    usize::try_from(page_size.get())
323        .unwrap_or(fsqlite_types::limits::DEFAULT_PAGE_SIZE as usize)
324        .div_ceil(16)
325        .clamp(
326            MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN,
327            MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MAX,
328        )
329}
330
331fn estimate_make_record_capacity_for_affinity(
332    affinity: Option<&str>,
333    n_cols: usize,
334    variable_width_hint: usize,
335) -> usize {
336    let chars = affinity.map_or_else(
337        || smallvec::SmallVec::<[char; 16]>::from_elem('C', n_cols),
338        |aff| aff.chars().collect(),
339    );
340    let column_count = chars.len().max(n_cols);
341    let mut header_content_size = 0usize;
342    let mut body_size = 0usize;
343
344    for idx in 0..column_count {
345        match chars.get(idx).copied().unwrap_or('C') {
346            'X' => {
347                header_content_size += 1;
348            }
349            'C' | 'D' | 'E' => {
350                header_content_size += 1;
351                body_size += MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES;
352            }
353            _ => {
354                header_content_size += 2;
355                body_size += variable_width_hint;
356            }
357        }
358    }
359
360    compute_record_header_size_hint(header_content_size) + body_size
361}
362
363fn estimate_make_record_buffer_capacity(program: &VdbeProgram, page_size: PageSize) -> usize {
364    let variable_width_hint = make_record_variable_width_reserve(page_size);
365    program
366        .ops()
367        .iter()
368        .filter(|op| op.opcode == Opcode::MakeRecord)
369        .map(|op| -> usize {
370            let n_cols = usize::try_from(op.p2.max(0)).unwrap_or(0);
371            let capacity = match &op.p4 {
372                P4::Affinity(aff) => estimate_make_record_capacity_for_affinity(
373                    Some(aff.as_str()),
374                    n_cols,
375                    variable_width_hint,
376                ),
377                P4::PrecomputedHeader(header) => {
378                    let body_size = header
379                        .slots
380                        .iter()
381                        .map(|slot| match slot.kind {
382                            PrecomputedSerialTypeKind::NullPlaceholder => 0,
383                            PrecomputedSerialTypeKind::AnyOneByteVarintOrNull => {
384                                MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN
385                            }
386                            PrecomputedSerialTypeKind::IntegerOrNull
387                            | PrecomputedSerialTypeKind::RealOrNull => {
388                                MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES
389                            }
390                        })
391                        .sum::<usize>();
392                    header.template.len() + body_size
393                }
394                _ => estimate_make_record_capacity_for_affinity(None, n_cols, variable_width_hint),
395            };
396            capacity
397        })
398        .max()
399        .unwrap_or(0)
400}
401
402#[inline]
403fn vtab_exec_outcome(opcode: &str, err: FrankenError) -> Result<ExecOutcome> {
404    if matches!(err, FrankenError::Abort) {
405        return Err(FrankenError::Abort);
406    }
407    Ok(ExecOutcome::Error {
408        code: 1,
409        message: format!("{opcode} error: {err}"),
410    })
411}
412
413#[inline]
414fn duration_ns_saturating(duration: Duration) -> u64 {
415    u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
416}
417
418#[inline]
419fn transient_mem_root_pgno(root_pgno: PageNumber) -> PageNumber {
420    if root_pgno.get() == 1 {
421        // MemPageStore pages do not include the 100-byte SQLite database
422        // header, so synthetic cursors must avoid page 1.
423        PageNumber::new(2).expect("page 2 must be a valid transient root page")
424    } else {
425        root_pgno
426    }
427}
428
429#[inline]
430fn add_vdbe_counter(counter: &AtomicU64, delta: u64) {
431    if FSQLITE_VDBE_METRICS_ENABLED.load(AtomicOrdering::Relaxed) {
432        counter.fetch_add(delta, AtomicOrdering::Relaxed);
433    }
434}
435
436#[inline]
437fn add_vdbe_counter_if(enabled: bool, counter: &AtomicU64, delta: u64) {
438    if enabled {
439        counter.fetch_add(delta, AtomicOrdering::Relaxed);
440    }
441}
442
443#[inline]
444fn add_vdbe_duration_if(enabled: bool, counter: &AtomicU64, started: Instant) {
445    if enabled {
446        counter.fetch_add(
447            duration_ns_saturating(started.elapsed()),
448            AtomicOrdering::Relaxed,
449        );
450    }
451}
452
453#[derive(Debug, Clone, Copy)]
454struct BtreeCursorPageLayout {
455    usable_size: u32,
456    page_size: u32,
457}
458
459impl BtreeCursorPageLayout {
460    const fn no_reserved_bytes(page_size: PageSize) -> Self {
461        let page_size = page_size.get();
462        Self {
463            usable_size: page_size,
464            page_size,
465        }
466    }
467}
468
469async fn btree_cursor_page_layout_from_page_one<P: PageReader>(
470    page_reader: &P,
471    cx: &Cx,
472) -> Option<BtreeCursorPageLayout> {
473    let page_one = page_reader.read_page(cx, PageNumber::ONE).await.ok()?;
474    let header_prefix: [u8; DATABASE_HEADER_SIZE] =
475        page_one.get(..DATABASE_HEADER_SIZE)?.try_into().ok()?;
476    let header = DatabaseHeader::from_bytes(&header_prefix).ok()?;
477    let usable_size = header.page_size.usable(header.reserved_per_page);
478    if usable_size <= 4 {
479        return None;
480    }
481    Some(BtreeCursorPageLayout {
482        usable_size,
483        page_size: header.page_size.get(),
484    })
485}
486
487async fn btree_cursor_page_layout_for_reader_or_default<P: PageReader>(
488    page_reader: &P,
489    cx: &Cx,
490    default_page_size: PageSize,
491) -> BtreeCursorPageLayout {
492    // Synthetic pager mocks used in unit tests often do not populate page 1
493    // with a real SQLite database header. Fall back to the engine's configured
494    // page size in that case, but prefer the real header whenever it exists so
495    // transaction-backed cursors honor reserved bytes.
496    btree_cursor_page_layout_from_page_one(page_reader, cx)
497        .await
498        .unwrap_or_else(|| BtreeCursorPageLayout::no_reserved_bytes(default_page_size))
499}
500
501fn configure_btree_cursor_page_size<P>(
502    cursor: &mut BtCursor<P>,
503    page_layout: BtreeCursorPageLayout,
504) {
505    if page_layout.page_size != page_layout.usable_size {
506        cursor.set_page_size(page_layout.page_size);
507    }
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
511pub struct ReusableTableExecutionStateOutcome {
512    pub metadata_rebind_count: u32,
513    /// W1: Track function cache clears so callers can verify the optimization.
514    pub function_cache_cleared: bool,
515}
516
517#[derive(Clone)]
518#[allow(clippy::struct_excessive_bools)]
519pub struct ReusableTableExecutionState {
520    pub func_registry: Arc<FunctionRegistry>,
521    pub collation_registry: Arc<Mutex<CollationRegistry>>,
522    pub schema_cookie: u32,
523    pub autoincrement_seq_by_root_page: HashMap<i32, i64>,
524    pub rowid_alias_col_by_root_page: Arc<HashMap<i32, usize>>,
525    pub table_column_count_by_root_page: Arc<HashMap<i32, usize>>,
526    pub first_not_null_non_ipk_col_by_root_page: Arc<HashMap<i32, usize>>,
527    pub column_defaults_by_root_page: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
528    pub index_desc_flags_by_root_page: Arc<HashMap<i32, Vec<bool>>>,
529    pub index_collations_by_root_page: Arc<HashMap<i32, Vec<Option<String>>>>,
530    pub reject_mem_fallback: bool,
531    pub memdb_rows_loaded: bool,
532    pub storage_cursor_memdb_count_shortcuts_safe: bool,
533    pub version_store: Option<Arc<VersionStore>>,
534    pub collect_result_rows: bool,
535    pub max_collected_result_rows: Option<usize>,
536}
537
538// ── In-Memory Table Store ──────────────────────────────────────────────────
539//
540// Phase 4 in-memory cursor backend. Allows the VDBE engine to execute
541// CREATE TABLE / INSERT / SELECT / UPDATE / DELETE against a lightweight
542// row store without requiring the full B-tree + pager + VFS stack.
543
544/// A row in an in-memory table: (rowid, column values).
545pub type MemRowValues = Vec<SqliteValue>;
546
547/// A row in an in-memory table: (rowid, column values).
548#[derive(Debug, Clone, PartialEq)]
549struct MemRow {
550    rowid: i64,
551    values: MemRowValues,
552}
553
554/// In-memory table storage (Phase 4 backend).
555#[derive(Debug, Clone)]
556struct UniqueConstraintState {
557    columns: Vec<usize>,
558    collations: Vec<Option<String>>,
559    index: Option<BTreeMap<Vec<u8>, BTreeSet<i64>>>,
560}
561
562fn unique_constraint_collation_is_indexable(collation: Option<&str>) -> bool {
563    match collation {
564        None => true,
565        Some(name)
566            if name.eq_ignore_ascii_case("BINARY")
567                || name.eq_ignore_ascii_case("NOCASE")
568                || name.eq_ignore_ascii_case("RTRIM") =>
569        {
570            true
571        }
572        Some(_) => false,
573    }
574}
575
576fn unique_constraint_supports_index(collations: &[Option<String>]) -> bool {
577    collations
578        .iter()
579        .all(|collation| unique_constraint_collation_is_indexable(collation.as_deref()))
580}
581
582fn append_unique_constraint_key_component(
583    key: &mut Vec<u8>,
584    value: &SqliteValue,
585    collation: Option<&str>,
586) -> bool {
587    if !unique_constraint_collation_is_indexable(collation) {
588        return false;
589    }
590
591    let is_nocase = collation.is_some_and(|name| name.eq_ignore_ascii_case("NOCASE"));
592    let is_rtrim = collation.is_some_and(|name| name.eq_ignore_ascii_case("RTRIM"));
593
594    match value {
595        SqliteValue::Null => return false,
596        SqliteValue::Integer(i) => {
597            key.push(1);
598            key.extend_from_slice(&i.to_le_bytes());
599        }
600        SqliteValue::Float(f) => {
601            if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(f) {
602                #[allow(clippy::cast_possible_truncation)]
603                let i = *f as i64;
604                #[allow(clippy::cast_precision_loss)]
605                if (i as f64) == *f {
606                    key.push(1);
607                    key.extend_from_slice(&i.to_le_bytes());
608                    return true;
609                }
610            }
611            key.push(2);
612            key.extend_from_slice(&f.to_bits().to_le_bytes());
613        }
614        SqliteValue::Text(text) => {
615            key.push(3);
616            let text_bytes = text.as_bytes_direct();
617            if is_nocase {
618                #[allow(clippy::cast_possible_truncation)]
619                key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
620                key.extend(text_bytes.iter().map(u8::to_ascii_lowercase));
621            } else if is_rtrim {
622                let trimmed = trim_rtrim_collation_text(text_bytes);
623                #[allow(clippy::cast_possible_truncation)]
624                key.extend_from_slice(&(trimmed.len() as u64).to_le_bytes());
625                key.extend_from_slice(trimmed);
626            } else {
627                #[allow(clippy::cast_possible_truncation)]
628                key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
629                key.extend_from_slice(text_bytes);
630            }
631        }
632        SqliteValue::Blob(bytes) => {
633            key.push(4);
634            #[allow(clippy::cast_possible_truncation)]
635            key.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
636            key.extend_from_slice(bytes);
637        }
638    }
639
640    true
641}
642
643impl UniqueConstraintState {
644    fn new(columns: Vec<usize>, mut collations: Vec<Option<String>>) -> Self {
645        if collations.len() < columns.len() {
646            collations.resize(columns.len(), None);
647        } else if collations.len() > columns.len() {
648            collations.truncate(columns.len());
649        }
650        let index = unique_constraint_supports_index(&collations).then(BTreeMap::new);
651        Self {
652            columns,
653            collations,
654            index,
655        }
656    }
657}
658
659#[derive(Debug, Clone)]
660pub struct MemTable {
661    /// Column count for this table (used when creating the table;
662    /// actual row widths may vary).
663    pub num_columns: usize,
664    /// Rows kept in ascending rowid order.
665    rows: Vec<MemRow>,
666    /// Next auto-increment rowid.
667    next_rowid: i64,
668    /// UNIQUE constraints tracked for MemDatabase-side enforcement. Builtin
669    /// collations (`BINARY`, `NOCASE`, `RTRIM`) use canonical byte-key
670    /// indexes; other collations fall back to an exact row scan through the
671    /// shared collation registry.
672    unique_constraints: Vec<UniqueConstraintState>,
673    /// Shared collation registry consulted when a UNIQUE constraint uses a
674    /// non-builtin collation that cannot be normalized into a byte key.
675    collation_registry: Arc<Mutex<CollationRegistry>>,
676}
677
678impl MemTable {
679    /// Create a new empty table with the given column count.
680    fn new(num_columns: usize) -> Self {
681        Self {
682            num_columns,
683            rows: Vec::new(),
684            next_rowid: 1,
685            unique_constraints: Vec::new(),
686            collation_registry: Arc::new(Mutex::new(CollationRegistry::new())),
687        }
688    }
689
690    pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
691        self.collation_registry = registry;
692    }
693
694    /// Register a group of columns that together form a UNIQUE constraint.
695    pub fn add_unique_column_group(&mut self, cols: Vec<usize>) {
696        self.add_unique_column_group_with_collations(cols, Vec::new());
697    }
698
699    /// Register a group of columns that together form a UNIQUE constraint
700    /// with explicit per-column collation metadata.
701    pub fn add_unique_column_group_with_collations(
702        &mut self,
703        cols: Vec<usize>,
704        collations: Vec<Option<String>>,
705    ) {
706        let mut constraint = UniqueConstraintState::new(cols, collations);
707        if let Some(index) = constraint.index.as_mut() {
708            for row in &self.rows {
709                if let Some(key) = Self::unique_key_for_constraint(
710                    &row.values,
711                    &constraint.columns,
712                    &constraint.collations,
713                ) {
714                    index.entry(key).or_default().insert(row.rowid);
715                }
716            }
717        }
718        self.unique_constraints.push(constraint);
719    }
720
721    /// Remove one matching UNIQUE constraint from the in-memory table.
722    ///
723    /// TEMP indexes are represented by direct `MemTable` constraints rather
724    /// than a separate B-tree, so `DROP INDEX` uses this to retire exactly the
725    /// enforcement rule that `CREATE UNIQUE INDEX` installed.
726    pub fn remove_unique_column_group_with_collations(
727        &mut self,
728        cols: &[usize],
729        collations: &[Option<String>],
730    ) -> bool {
731        let normalized = UniqueConstraintState::new(cols.to_vec(), collations.to_vec());
732        let Some(position) = self.unique_constraints.iter().rposition(|constraint| {
733            constraint.columns == normalized.columns
734                && constraint.collations.len() == normalized.collations.len()
735                && constraint
736                    .collations
737                    .iter()
738                    .zip(&normalized.collations)
739                    .all(|(left, right)| match (left, right) {
740                        (Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
741                        (None, None) => true,
742                        _ => false,
743                    })
744        }) else {
745            return false;
746        };
747        self.unique_constraints.remove(position);
748        true
749    }
750
751    /// Return whether the current rows satisfy a proposed UNIQUE constraint.
752    ///
753    /// This is used by TEMP `CREATE UNIQUE INDEX` before mutating schema
754    /// metadata. Builtin collations use the same canonical key encoding as the
755    /// installed constraint; custom collations use the exact comparison path.
756    pub fn unique_column_group_is_valid(
757        &self,
758        cols: &[usize],
759        collations: &[Option<String>],
760    ) -> bool {
761        let constraint = UniqueConstraintState::new(cols.to_vec(), collations.to_vec());
762        if constraint.index.is_some() {
763            let mut seen = HashSet::new();
764            for row in &self.rows {
765                if let Some(key) = Self::unique_key_for_constraint(
766                    &row.values,
767                    &constraint.columns,
768                    &constraint.collations,
769                ) && !seen.insert(key)
770                {
771                    return false;
772                }
773            }
774            return true;
775        }
776
777        let registry = self
778            .collation_registry
779            .lock()
780            .unwrap_or_else(|poisoned| poisoned.into_inner());
781        for (idx, row) in self.rows.iter().enumerate() {
782            if self.rows[idx + 1..].iter().any(|other| {
783                Self::unique_constraint_matches_row(
784                    &row.values,
785                    &other.values,
786                    &constraint.columns,
787                    &constraint.collations,
788                    &registry,
789                )
790            }) {
791                return false;
792            }
793        }
794        true
795    }
796
797    /// Find rows that conflict with `new_values` on any UNIQUE constraint.
798    /// Returns the rowids of all conflicting rows.
799    pub fn find_unique_conflicts(&self, new_values: &[SqliteValue]) -> Vec<i64> {
800        let mut conflicts = BTreeSet::new();
801        let mut collations = None;
802        for constraint in &self.unique_constraints {
803            if let Some(index) = &constraint.index {
804                if let Some(key) = Self::unique_key_for_constraint(
805                    new_values,
806                    &constraint.columns,
807                    &constraint.collations,
808                ) && let Some(rowids) = index.get(&key)
809                {
810                    conflicts.extend(rowids.iter().copied());
811                }
812                continue;
813            }
814
815            let registry = collations.get_or_insert_with(|| {
816                self.collation_registry
817                    .lock()
818                    .unwrap_or_else(|poisoned| poisoned.into_inner())
819            });
820            for row in &self.rows {
821                if Self::unique_constraint_matches_row(
822                    &row.values,
823                    new_values,
824                    &constraint.columns,
825                    &constraint.collations,
826                    registry,
827                ) {
828                    conflicts.insert(row.rowid);
829                }
830            }
831        }
832        conflicts.into_iter().collect()
833    }
834
835    /// Allocate a new unique rowid.
836    pub fn alloc_rowid(&mut self) -> i64 {
837        let id = self.next_rowid;
838        self.next_rowid = self.next_rowid.saturating_add(1);
839        id
840    }
841
842    /// Return the next implicit rowid that would be allocated for this table.
843    #[must_use]
844    pub fn next_rowid_hint(&self) -> i64 {
845        self.next_rowid
846    }
847
848    /// Return the exact visible row count.
849    #[must_use]
850    pub fn row_count(&self) -> usize {
851        self.rows.len()
852    }
853
854    /// Return the maximum currently visible rowid.
855    #[must_use]
856    pub fn max_visible_rowid(&self) -> Option<i64> {
857        self.rows.last().map(|row| row.rowid)
858    }
859
860    /// Insert a row with the given rowid and values.
861    fn insert<V>(&mut self, rowid: i64, values: V)
862    where
863        V: Into<MemRowValues>,
864    {
865        let values = values.into();
866        // Update next_rowid if needed.
867        if rowid >= self.next_rowid {
868            self.next_rowid = rowid.saturating_add(1);
869        }
870        let new_keys = self.unique_keys_for_values(&values);
871        if self.rows.last().is_none_or(|row| row.rowid < rowid) {
872            self.rows.push(MemRow { rowid, values });
873            self.insert_unique_keys(rowid, &new_keys);
874            return;
875        }
876        // Replace if rowid already exists (UPSERT semantics).
877        match self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
878            Ok(idx) => {
879                let old_values = {
880                    let row = &mut self.rows[idx];
881                    std::mem::replace(&mut row.values, values)
882                };
883                self.remove_unique_entries(rowid, &old_values);
884                self.insert_unique_keys(rowid, &new_keys);
885            }
886            Err(idx) => {
887                self.rows.insert(idx, MemRow { rowid, values });
888                self.insert_unique_keys(rowid, &new_keys);
889            }
890        }
891    }
892
893    /// Delete a row by rowid. Returns true if a row was found and deleted.
894    pub fn delete_by_rowid(&mut self, rowid: i64) -> bool {
895        if let Ok(idx) = self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
896            let row = self.rows.remove(idx);
897            self.remove_unique_entries(row.rowid, &row.values);
898            true
899        } else {
900            false
901        }
902    }
903
904    /// Remove all rows from the table.
905    pub fn clear(&mut self) {
906        self.rows.clear();
907        for constraint in &mut self.unique_constraints {
908            if let Some(index) = constraint.index.as_mut() {
909                index.clear();
910            }
911        }
912    }
913
914    /// Pad all existing rows to have at least `target_cols` columns,
915    /// appending `default_val` for any missing trailing columns.
916    /// Used after ALTER TABLE ADD COLUMN to make pre-existing rows
917    /// visible with the new column's default value.
918    pub fn pad_rows_to_column_count(&mut self, target_cols: usize, default_val: &SqliteValue) {
919        self.num_columns = target_cols;
920        for row in &mut self.rows {
921            while row.values.len() < target_cols {
922                row.values.push(default_val.clone());
923            }
924        }
925        self.rebuild_unique_indexes();
926    }
927
928    /// Remove one declared column from every materialized row.
929    ///
930    /// TEMP tables use `MemTable` as their authoritative storage.  Keep the
931    /// table's direct UNIQUE-constraint column positions aligned with the
932    /// rewritten row image when `ALTER TABLE ... DROP COLUMN` removes a slot.
933    fn remove_column_from_rows(&mut self, removed_slot: usize) {
934        self.num_columns = self.num_columns.saturating_sub(1);
935        for row in &mut self.rows {
936            if removed_slot < row.values.len() {
937                row.values.remove(removed_slot);
938            }
939        }
940        for constraint in &mut self.unique_constraints {
941            for column in &mut constraint.columns {
942                if *column > removed_slot {
943                    *column -= 1;
944                }
945            }
946        }
947        self.rebuild_unique_indexes();
948    }
949
950    /// Find a row by rowid. Returns the index.
951    #[inline]
952    pub fn find_by_rowid(&self, rowid: i64) -> Option<usize> {
953        if let Some(idx) = self.dense_rowid_offset(rowid) {
954            return Some(idx);
955        }
956        self.rows.binary_search_by_key(&rowid, |r| r.rowid).ok()
957    }
958
959    #[inline]
960    fn dense_rowid_offset(&self, rowid: i64) -> Option<usize> {
961        let first_rowid = self.rows.first()?.rowid;
962        let offset = rowid.checked_sub(first_rowid)?;
963        let idx = usize::try_from(offset).ok()?;
964        let row = self.rows.get(idx)?;
965        (row.rowid == rowid).then_some(idx)
966    }
967
968    fn rowid_lower_bound(&self, rowid: i64) -> usize {
969        let Some(first) = self.rows.first() else {
970            return 0;
971        };
972        if rowid <= first.rowid {
973            return 0;
974        }
975        if self.rows.last().is_some_and(|last| rowid > last.rowid) {
976            return self.rows.len();
977        }
978        if let Some(idx) = self.dense_rowid_offset(rowid) {
979            return idx;
980        }
981        match self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
982            Ok(idx) | Err(idx) => idx,
983        }
984    }
985
986    /// Return the values for a rowid, if present.
987    #[inline]
988    pub fn row_values_by_rowid(&self, rowid: i64) -> Option<&[SqliteValue]> {
989        let idx = self.find_by_rowid(rowid)?;
990        Some(self.rows[idx].values.as_slice())
991    }
992
993    /// Count rows whose rowid is in `[lower_inclusive, upper_exclusive)`.
994    pub fn count_rowid_range(&self, lower_inclusive: i64, upper_exclusive: i64) -> usize {
995        let start = self.rowid_lower_bound(lower_inclusive);
996        let end = self.rowid_lower_bound(upper_exclusive);
997        end.saturating_sub(start)
998    }
999
1000    /// Iterate rows whose rowid is in `[lower_inclusive, upper_exclusive)`.
1001    pub fn iter_rows_in_rowid_range(
1002        &self,
1003        lower_inclusive: i64,
1004        upper_exclusive: i64,
1005    ) -> impl Iterator<Item = (i64, &[SqliteValue])> + '_ {
1006        let start = self.rowid_lower_bound(lower_inclusive);
1007        let end = self.rowid_lower_bound(upper_exclusive);
1008        let end = end.max(start);
1009        self.rows[start..end]
1010            .iter()
1011            .map(|row| (row.rowid, row.values.as_slice()))
1012    }
1013
1014    /// Iterate all rows as `(rowid, values)` pairs.
1015    ///
1016    /// Used by the compat persistence layer to dump table contents to
1017    /// real SQLite format files.
1018    pub fn iter_rows(&self) -> impl Iterator<Item = (i64, &[SqliteValue])> + '_ {
1019        self.rows.iter().map(|r| (r.rowid, r.values.as_slice()))
1020    }
1021
1022    /// Insert a row with an explicit rowid (for loading from file).
1023    ///
1024    /// This is the public entry point used by the compat persistence
1025    /// loader. It delegates to the private `insert` method.
1026    pub fn insert_row<V>(&mut self, rowid: i64, values: V)
1027    where
1028        V: Into<MemRowValues>,
1029    {
1030        self.insert(rowid, values);
1031    }
1032
1033    fn unique_key_for_constraint(
1034        values: &[SqliteValue],
1035        columns: &[usize],
1036        collations: &[Option<String>],
1037    ) -> Option<Vec<u8>> {
1038        let mut key = Vec::with_capacity(columns.len() * 8);
1039        for (position, &col_idx) in columns.iter().enumerate() {
1040            let value = values.get(col_idx)?;
1041            if matches!(value, SqliteValue::Null) {
1042                return None;
1043            }
1044            let collation = collations.get(position).and_then(|c| c.as_deref());
1045            if !append_unique_constraint_key_component(&mut key, value, collation) {
1046                return None;
1047            }
1048        }
1049        Some(key)
1050    }
1051
1052    fn unique_constraint_matches_row(
1053        existing_values: &[SqliteValue],
1054        new_values: &[SqliteValue],
1055        columns: &[usize],
1056        collations: &[Option<String>],
1057        registry: &CollationRegistry,
1058    ) -> bool {
1059        for (position, &col_idx) in columns.iter().enumerate() {
1060            let Some(existing_value) = existing_values.get(col_idx) else {
1061                return false;
1062            };
1063            let Some(new_value) = new_values.get(col_idx) else {
1064                return false;
1065            };
1066            if existing_value.is_null() || new_value.is_null() {
1067                return false;
1068            }
1069            let collation = collations.get(position).and_then(|c| c.as_deref());
1070            if cmp_values_collated(existing_value, new_value, collation, registry)
1071                != Ordering::Equal
1072            {
1073                return false;
1074            }
1075        }
1076        true
1077    }
1078
1079    fn remove_unique_entries(&mut self, rowid: i64, values: &[SqliteValue]) {
1080        for constraint in &mut self.unique_constraints {
1081            let Some(index) = constraint.index.as_mut() else {
1082                continue;
1083            };
1084            let Some(key) = Self::unique_key_for_constraint(
1085                values,
1086                &constraint.columns,
1087                &constraint.collations,
1088            ) else {
1089                continue;
1090            };
1091            let remove_entry = if let Some(rowids) = index.get_mut(&key) {
1092                rowids.remove(&rowid);
1093                rowids.is_empty()
1094            } else {
1095                false
1096            };
1097            if remove_entry {
1098                index.remove(&key);
1099            }
1100        }
1101    }
1102
1103    fn unique_keys_for_values(&self, values: &[SqliteValue]) -> Vec<Option<Vec<u8>>> {
1104        self.unique_constraints
1105            .iter()
1106            .map(|constraint| {
1107                constraint.index.as_ref().and_then(|_| {
1108                    Self::unique_key_for_constraint(
1109                        values,
1110                        &constraint.columns,
1111                        &constraint.collations,
1112                    )
1113                })
1114            })
1115            .collect()
1116    }
1117
1118    fn insert_unique_keys(&mut self, rowid: i64, keys: &[Option<Vec<u8>>]) {
1119        for (maybe_key, constraint) in keys.iter().zip(&mut self.unique_constraints) {
1120            if let (Some(key), Some(index)) = (maybe_key, constraint.index.as_mut()) {
1121                index.entry(key.clone()).or_default().insert(rowid);
1122            }
1123        }
1124    }
1125
1126    fn rebuild_unique_indexes(&mut self) {
1127        for constraint in &mut self.unique_constraints {
1128            if let Some(index) = constraint.index.as_mut() {
1129                index.clear();
1130            }
1131        }
1132        for row in &self.rows {
1133            for constraint in &mut self.unique_constraints {
1134                let Some(index) = constraint.index.as_mut() else {
1135                    continue;
1136                };
1137                if let Some(key) = Self::unique_key_for_constraint(
1138                    &row.values,
1139                    &constraint.columns,
1140                    &constraint.collations,
1141                ) {
1142                    index.entry(key).or_default().insert(row.rowid);
1143                }
1144            }
1145        }
1146    }
1147}
1148
1149/// Cursor state for traversing an in-memory table.
1150#[derive(Debug, Clone)]
1151struct MemCursor {
1152    /// Root page (used as table identifier).
1153    root_page: i32,
1154    /// Whether this cursor is writable (enforced at the Connection level).
1155    #[allow(dead_code)]
1156    writable: bool,
1157    /// Current row position (None = not positioned).
1158    position: Option<usize>,
1159    /// Pseudo-table data (for OpenPseudo: a single row set by RowData/MakeRecord).
1160    pseudo_row: Option<Vec<SqliteValue>>,
1161    /// Cached pseudo-row values parsed from `pseudo_reg`.
1162    cached_pseudo_row: Option<(SqliteValue, Vec<SqliteValue>)>,
1163    /// Register containing the pseudo-row data blob.
1164    pseudo_reg: Option<i32>,
1165    /// Whether this is a pseudo cursor (OpenPseudo).
1166    is_pseudo: bool,
1167}
1168
1169impl MemCursor {
1170    fn new(root_page: i32, writable: bool) -> Self {
1171        Self {
1172            root_page,
1173            writable,
1174            position: None,
1175            pseudo_row: None,
1176            cached_pseudo_row: None,
1177            pseudo_reg: None,
1178            is_pseudo: false,
1179        }
1180    }
1181
1182    fn new_pseudo(reg: i32) -> Self {
1183        Self {
1184            root_page: -1,
1185            writable: false,
1186            position: None,
1187            pseudo_row: None,
1188            cached_pseudo_row: None,
1189            pseudo_reg: Some(reg),
1190            is_pseudo: true,
1191        }
1192    }
1193}
1194
1195/// A single row in the sorter.
1196///
1197/// Stores only the decoded **sort-key prefix** (first `key_columns` values)
1198/// for comparison, plus the raw record blob for output.  Prior to the lazy
1199/// key decode optimization, ALL columns were eagerly decoded into `values`
1200/// — now only the sort-key columns are materialized.
1201#[derive(Debug, Clone)]
1202struct SorterRow {
1203    /// Decoded sort-key columns (first `key_columns` values only).
1204    values: Vec<SqliteValue>,
1205    /// Raw serialized record for output via `SorterData`.
1206    blob: Vec<u8>,
1207    /// Source-row order, used only to make bounded-heap ties stable.
1208    source_sequence: u64,
1209}
1210
1211/// Cursor state for sorter opcodes (`SorterOpen`, `SorterInsert`, ...).
1212///
1213/// Supports external merge sort: when in-memory rows exceed `spill_threshold`
1214/// bytes, the current batch is sorted and flushed to a temporary file as a
1215/// "run".  At `SorterSort` time, all runs (plus any remaining in-memory rows)
1216/// are merged via k-way merge.
1217#[derive(Clone)]
1218struct SorterCursor {
1219    /// Number of leading columns used as sort key.
1220    key_columns: usize,
1221    /// Per-key sort direction (length == key_columns).
1222    sort_key_orders: Vec<SortKeyOrder>,
1223    /// Per-key collation sequence (e.g. "NOCASE"). `None` means BINARY.
1224    collations: Vec<Option<String>>,
1225    /// Shared collation registry consulted during comparison.
1226    collation_registry: Arc<Mutex<CollationRegistry>>,
1227    /// Inserted records.
1228    rows: Vec<SorterRow>,
1229    /// Current position after `SorterSort`/`SorterNext`.
1230    position: Option<usize>,
1231    /// Position for which the lazy output decode cache is currently valid.
1232    cached_row_position: Option<usize>,
1233    /// Sorter-owned decode/materialization scratch for the current output row.
1234    cached_row_decode: RowDecodeScratch,
1235    /// Estimated bytes consumed by `rows` (approximate).
1236    memory_used: usize,
1237    /// Memory limit before spilling to disk (default 100 MiB).
1238    spill_threshold: usize,
1239    /// Sorted runs that have been spilled to disk.
1240    spill_runs: Vec<SpillRun>,
1241    /// Keep only the best N rows during insertion when ORDER BY is paired with
1242    /// LIMIT. Retained rows form a max-heap whose root is the current worst
1243    /// row, so candidate admission and replacement are O(log N).
1244    ///
1245    /// While the heap is still growing, native spillable builds can safely
1246    /// downgrade it after crossing the spill threshold because no input row
1247    /// has been discarded yet. Browser builds retain top-N mode because their
1248    /// spill path remains in memory. Once the heap is full, rejected rows make
1249    /// any downgrade unsound; a later retained replacement with a giant
1250    /// payload can therefore still exceed the spill threshold.
1251    top_n_limit: Option<usize>,
1252    /// Monotonic source-row sequence for stable top-N tie handling.
1253    next_source_sequence: u64,
1254    /// Total rows sorted (across all runs + final merge).
1255    rows_sorted_total: u64,
1256    /// Total pages spilled to disk.
1257    spill_pages_total: u64,
1258}
1259
1260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1261enum SortKeyOrder {
1262    /// ASC with NULLS FIRST (SQLite default for ASC).
1263    Asc,
1264    /// DESC with NULLS LAST (SQLite default for DESC).
1265    Desc,
1266    /// ASC with NULLS LAST (explicit NULLS LAST).
1267    AscNullsLast,
1268    /// DESC with NULLS FIRST (explicit NULLS FIRST).
1269    DescNullsFirst,
1270}
1271
1272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1273enum DecodeCacheInvalidationReason {
1274    PositionChange,
1275    WriteMutation,
1276    PseudoRowChange,
1277}
1278
1279/// Default spill threshold: 100 MiB.
1280///
1281/// When in-memory sort data exceeds this limit, the sorter flushes sorted
1282/// runs to a temporary file. C SQLite's equivalent is configurable via
1283/// `SQLITE_CONFIG_PMASZ`; this is a compile-time default that could be
1284/// exposed via `PRAGMA temp_store_max_size` in a future enhancement.
1285const SORTER_DEFAULT_SPILL_THRESHOLD: usize = 100 * 1024 * 1024;
1286
1287/// Number of 64-bit words in a Bloom filter (8192 bits = 1 KiB).
1288const BLOOM_FILTER_WORDS: usize = 128;
1289
1290/// Compute a simple hash of a `SqliteValue` for Bloom filter lookups.
1291fn bloom_hash(val: &SqliteValue) -> u64 {
1292    // FNV-1a-style hash — sufficient for a Bloom filter.
1293    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1294    let bytes: &[u8] = match val {
1295        SqliteValue::Null => &[0],
1296        SqliteValue::Integer(i) => {
1297            // Hash inline to avoid allocation.
1298            h ^= 1;
1299            h = h.wrapping_mul(0x0100_0000_01b3);
1300            for b in i.to_le_bytes() {
1301                h ^= u64::from(b);
1302                h = h.wrapping_mul(0x0100_0000_01b3);
1303            }
1304            return h;
1305        }
1306        SqliteValue::Float(f) => {
1307            h ^= 2;
1308            h = h.wrapping_mul(0x0100_0000_01b3);
1309            for b in f.to_le_bytes() {
1310                h ^= u64::from(b);
1311                h = h.wrapping_mul(0x0100_0000_01b3);
1312            }
1313            return h;
1314        }
1315        SqliteValue::Text(s) => s.as_bytes_direct(),
1316        SqliteValue::Blob(b) => b,
1317    };
1318    for &b in bytes {
1319        h ^= u64::from(b);
1320        h = h.wrapping_mul(0x0100_0000_01b3);
1321    }
1322    h
1323}
1324
1325/// Approximate page size for spill accounting.
1326///
1327/// Used only for counting pages written during sorter spill operations.
1328/// Ideally this would come from the database's actual page size, but the
1329/// sorter runs at the VDBE level where the page size is available via
1330/// `self.page_size`. For now, use the default. The spill page count metric
1331/// is informational — it does not affect correctness.
1332#[cfg(not(target_arch = "wasm32"))]
1333const SORTER_SPILL_PAGE_SIZE: usize = fsqlite_types::limits::DEFAULT_PAGE_SIZE as usize;
1334
1335/// A sorted run that has been flushed to a temporary file.
1336///
1337/// Each run stores length-prefixed serialized records in sorted order.
1338/// Format per record: `[u32-le length][serialized record bytes]`.
1339#[derive(Debug, Clone)]
1340struct SpillRun {
1341    /// Path to the temporary file containing serialized sorted records.
1342    path: std::path::PathBuf,
1343    /// Number of records in this run (used for merge accounting).
1344    #[allow(dead_code)]
1345    record_count: u64,
1346    /// Total bytes written (used for page accounting).
1347    #[allow(dead_code)]
1348    bytes_written: u64,
1349}
1350
1351impl SorterCursor {
1352    #[cfg(test)]
1353    fn new(
1354        key_columns: usize,
1355        sort_key_orders: Vec<SortKeyOrder>,
1356        collations: Vec<Option<String>>,
1357    ) -> Self {
1358        Self::with_collation_registry(
1359            key_columns,
1360            sort_key_orders,
1361            collations,
1362            Arc::new(Mutex::new(CollationRegistry::new())),
1363            None,
1364        )
1365    }
1366
1367    fn with_collation_registry(
1368        key_columns: usize,
1369        mut sort_key_orders: Vec<SortKeyOrder>,
1370        collations: Vec<Option<String>>,
1371        collation_registry: Arc<Mutex<CollationRegistry>>,
1372        top_n_limit: Option<usize>,
1373    ) -> Self {
1374        let key_columns = key_columns.max(1);
1375        if sort_key_orders.len() < key_columns {
1376            sort_key_orders.resize(key_columns, SortKeyOrder::Asc);
1377        }
1378        sort_key_orders.truncate(key_columns);
1379        Self {
1380            key_columns,
1381            sort_key_orders,
1382            collations,
1383            collation_registry,
1384            rows: Vec::new(),
1385            position: None,
1386            cached_row_position: None,
1387            cached_row_decode: RowDecodeScratch::default(),
1388            memory_used: 0,
1389            spill_threshold: SORTER_DEFAULT_SPILL_THRESHOLD,
1390            spill_runs: Vec::new(),
1391            top_n_limit,
1392            next_source_sequence: 0,
1393            rows_sorted_total: 0,
1394            spill_pages_total: 0,
1395        }
1396    }
1397
1398    fn compare_top_n_rows(
1399        &self,
1400        lhs: &SorterRow,
1401        rhs: &SorterRow,
1402        collation_registry: &CollationRegistry,
1403    ) -> Ordering {
1404        let key_ordering = compare_sorter_rows(
1405            &lhs.values,
1406            &rhs.values,
1407            self.key_columns,
1408            &self.sort_key_orders,
1409            &self.collations,
1410            collation_registry,
1411        );
1412        if key_ordering == Ordering::Equal {
1413            lhs.source_sequence.cmp(&rhs.source_sequence)
1414        } else {
1415            key_ordering
1416        }
1417    }
1418
1419    fn sift_top_n_heap_up(&mut self, mut index: usize, collation_registry: &CollationRegistry) {
1420        while index > 0 {
1421            let parent = (index - 1) / 2;
1422            if self.compare_top_n_rows(&self.rows[parent], &self.rows[index], collation_registry)
1423                != Ordering::Less
1424            {
1425                break;
1426            }
1427            self.rows.swap(parent, index);
1428            index = parent;
1429        }
1430    }
1431
1432    fn sift_top_n_heap_down(&mut self, mut index: usize, collation_registry: &CollationRegistry) {
1433        loop {
1434            let left = index * 2 + 1;
1435            if left >= self.rows.len() {
1436                break;
1437            }
1438            let right = left + 1;
1439            let mut worse_child = left;
1440            if right < self.rows.len()
1441                && self.compare_top_n_rows(&self.rows[left], &self.rows[right], collation_registry)
1442                    == Ordering::Less
1443            {
1444                worse_child = right;
1445            }
1446            if self.compare_top_n_rows(
1447                &self.rows[index],
1448                &self.rows[worse_child],
1449                collation_registry,
1450            ) != Ordering::Less
1451            {
1452                break;
1453            }
1454            self.rows.swap(index, worse_child);
1455            index = worse_child;
1456        }
1457    }
1458
1459    /// Whether a key-only candidate would survive the bounded top-N heap.
1460    ///
1461    /// Equal-key candidates are rejected once the heap is full because they
1462    /// occur later in source order. This is the same tie rule used by
1463    /// `insert_row`, and lets codegen skip evaluating payload expressions for
1464    /// rows that cannot reach the result.
1465    fn would_retain_top_n(
1466        &self,
1467        candidate_values: &[SqliteValue],
1468        collation_registry: &CollationRegistry,
1469    ) -> bool {
1470        let Some(limit) = self.top_n_limit else {
1471            return true;
1472        };
1473        if limit == 0 {
1474            return false;
1475        }
1476        if self.rows.len() < limit {
1477            return true;
1478        }
1479        self.rows.first().is_some_and(|worst| {
1480            compare_sorter_rows(
1481                candidate_values,
1482                &worst.values,
1483                self.key_columns,
1484                &self.sort_key_orders,
1485                &self.collations,
1486                collation_registry,
1487            ) == Ordering::Less
1488        })
1489    }
1490
1491    /// Estimate the memory footprint of a sorter row.
1492    fn estimate_row_size(values: &[SqliteValue], blob: &[u8]) -> usize {
1493        // Base overhead per Vec element + per-value overhead + blob size
1494        let mut size = std::mem::size_of::<SorterRow>() + values.len() * 24 + blob.len();
1495        for val in values {
1496            match val {
1497                SqliteValue::Text(s) => size += s.len(),
1498                SqliteValue::Blob(b) => size += b.len(),
1499                _ => {}
1500            }
1501        }
1502        size
1503    }
1504
1505    /// Insert a row and spill to disk if memory exceeds the threshold.
1506    fn insert_row(&mut self, values: Vec<SqliteValue>, blob: Vec<u8>) -> Result<()> {
1507        if self.top_n_limit == Some(0) {
1508            return Ok(());
1509        }
1510
1511        let new_row_size = Self::estimate_row_size(&values, &blob);
1512        if let Some(limit) = self.top_n_limit {
1513            // Rejection and replacement begin only once the heap reaches
1514            // `limit`, and the heap never shrinks afterward. Therefore
1515            // `len() < limit` proves that the retained prefix is still complete.
1516            #[cfg(not(target_arch = "wasm32"))]
1517            let can_downgrade_before_discarding = self.rows.len() < limit
1518                && self.memory_used.saturating_add(new_row_size) >= self.spill_threshold;
1519            #[cfg(target_arch = "wasm32")]
1520            let can_downgrade_before_discarding = false;
1521            if can_downgrade_before_discarding {
1522                // The heap may have reordered equal-key rows. Restore scan
1523                // order before handing the complete input prefix to the stable
1524                // ordinary sorter and its spill path.
1525                self.rows.sort_by_key(|row| row.source_sequence);
1526                self.top_n_limit = None;
1527                self.next_source_sequence = 0;
1528                self.invalidate_output_row_cache();
1529            } else {
1530                let new_row = SorterRow {
1531                    values,
1532                    blob,
1533                    source_sequence: self.next_source_sequence,
1534                };
1535                self.next_source_sequence = self.next_source_sequence.saturating_add(1);
1536                let collation_registry = Arc::clone(&self.collation_registry);
1537                let coll_guard = collation_registry.lock().unwrap_or_else(|e| e.into_inner());
1538                if self.rows.len() < limit {
1539                    self.rows.push(new_row);
1540                    self.sift_top_n_heap_up(self.rows.len() - 1, &coll_guard);
1541                    self.memory_used += new_row_size;
1542                    self.invalidate_output_row_cache();
1543                    return Ok(());
1544                }
1545
1546                if self.would_retain_top_n(&new_row.values, &coll_guard) {
1547                    let removed_row = std::mem::replace(&mut self.rows[0], new_row);
1548                    let removed_size =
1549                        Self::estimate_row_size(&removed_row.values, &removed_row.blob);
1550                    self.memory_used = self.memory_used.saturating_sub(removed_size);
1551                    self.memory_used = self.memory_used.saturating_add(new_row_size);
1552                    self.sift_top_n_heap_down(0, &coll_guard);
1553                    self.invalidate_output_row_cache();
1554                }
1555                return Ok(());
1556            }
1557        }
1558
1559        self.memory_used += new_row_size;
1560        self.rows.push(SorterRow {
1561            values,
1562            blob,
1563            source_sequence: 0,
1564        });
1565        self.invalidate_output_row_cache();
1566
1567        if self.memory_used >= self.spill_threshold {
1568            self.spill_to_disk()?;
1569        }
1570        Ok(())
1571    }
1572
1573    /// Sort the in-memory rows, write them to a temp file, and clear them.
1574    #[cfg(not(target_arch = "wasm32"))]
1575    fn spill_to_disk(&mut self) -> Result<()> {
1576        use std::io::Write;
1577
1578        if self.rows.is_empty() {
1579            return Ok(());
1580        }
1581
1582        // Sort current batch — lock collation registry once for entire sort.
1583        let key_columns = self.key_columns;
1584        let orders = self.sort_key_orders.clone();
1585        let colls = self.collations.clone();
1586        let coll_guard = self
1587            .collation_registry
1588            .lock()
1589            .unwrap_or_else(|e| e.into_inner());
1590        self.rows.sort_by(|lhs, rhs| {
1591            compare_sorter_rows(
1592                &lhs.values,
1593                &rhs.values,
1594                key_columns,
1595                &orders,
1596                &colls,
1597                &coll_guard,
1598            )
1599        });
1600        drop(coll_guard);
1601
1602        // Write to temp file.  We use `keep()` to detach the auto-delete
1603        // guard so the file persists until we explicitly remove it in
1604        // `sort()` / `reset()`.
1605        let tmp = tempfile::NamedTempFile::new()
1606            .map_err(|e| FrankenError::internal(format!("sorter spill tempfile: {e}")))?;
1607        let (file, path) = tmp
1608            .keep()
1609            .map_err(|e| FrankenError::internal(format!("sorter spill keep: {e}")))?;
1610        let mut writer = std::io::BufWriter::new(file);
1611
1612        let record_count = self.rows.len() as u64;
1613        let mut bytes_written: u64 = 0;
1614        for row in &self.rows {
1615            // Write the raw blob directly instead of re-serializing from
1616            // decoded values.  The blob is the original record bytes from
1617            // MakeRecord and is identical to what serialize_record would
1618            // produce (but without the decode→re-encode round-trip).
1619            #[allow(clippy::cast_possible_truncation)]
1620            let len_bytes = (row.blob.len() as u32).to_le_bytes();
1621            writer
1622                .write_all(&len_bytes)
1623                .map_err(|e| FrankenError::internal(format!("sorter spill write len: {e}")))?;
1624            writer
1625                .write_all(&row.blob)
1626                .map_err(|e| FrankenError::internal(format!("sorter spill write data: {e}")))?;
1627            bytes_written += 4 + row.blob.len() as u64;
1628        }
1629        writer
1630            .flush()
1631            .map_err(|e| FrankenError::internal(format!("sorter spill flush: {e}")))?;
1632
1633        #[allow(clippy::cast_possible_truncation)]
1634        let pages = (bytes_written as usize).div_ceil(SORTER_SPILL_PAGE_SIZE);
1635        self.spill_pages_total += pages as u64;
1636
1637        tracing::warn!(
1638            rows = record_count,
1639            bytes = bytes_written,
1640            pages,
1641            run_index = self.spill_runs.len(),
1642            "sorter spilling to disk"
1643        );
1644
1645        self.spill_runs.push(SpillRun {
1646            path,
1647            record_count,
1648            bytes_written,
1649        });
1650
1651        self.rows_sorted_total += record_count;
1652        self.rows.clear();
1653        self.invalidate_output_row_cache();
1654        self.memory_used = 0;
1655        Ok(())
1656    }
1657
1658    /// Browser builds do not have a portable temp-file story yet, so sorter
1659    /// spill currently degrades to a pure in-memory sort path.
1660    #[cfg(target_arch = "wasm32")]
1661    fn spill_to_disk(&mut self) -> Result<()> {
1662        self.spill_threshold = usize::MAX;
1663        tracing::warn!(
1664            rows = self.rows.len(),
1665            bytes = self.memory_used,
1666            "sorter spill requested on wasm; keeping rows in memory"
1667        );
1668        Ok(())
1669    }
1670
1671    /// Sort the sorter, merging any spilled runs with in-memory rows.
1672    ///
1673    /// After this call, `self.rows` contains the fully sorted result and
1674    /// `self.spill_runs` is drained.
1675    #[allow(clippy::too_many_lines)]
1676    fn sort(&mut self) -> Result<()> {
1677        // Lock collation registry once for entire sort operation.
1678        let coll_guard = self
1679            .collation_registry
1680            .lock()
1681            .unwrap_or_else(|e| e.into_inner());
1682
1683        if self.spill_runs.is_empty() {
1684            if self.top_n_limit.is_some() {
1685                let key_columns = self.key_columns;
1686                let orders = self.sort_key_orders.clone();
1687                let colls = self.collations.clone();
1688                self.rows.sort_by(|lhs, rhs| {
1689                    let key_ordering = compare_sorter_rows(
1690                        &lhs.values,
1691                        &rhs.values,
1692                        key_columns,
1693                        &orders,
1694                        &colls,
1695                        &coll_guard,
1696                    );
1697                    if key_ordering == Ordering::Equal {
1698                        lhs.source_sequence.cmp(&rhs.source_sequence)
1699                    } else {
1700                        key_ordering
1701                    }
1702                });
1703                self.rows_sorted_total += self.rows.len() as u64;
1704                return Ok(());
1705            }
1706
1707            // Pure in-memory sort — fast path.
1708            let key_columns = self.key_columns;
1709            let orders = self.sort_key_orders.clone();
1710            let colls = self.collations.clone();
1711            self.rows.sort_by(|lhs, rhs| {
1712                compare_sorter_rows(
1713                    &lhs.values,
1714                    &rhs.values,
1715                    key_columns,
1716                    &orders,
1717                    &colls,
1718                    &coll_guard,
1719                )
1720            });
1721            self.rows_sorted_total += self.rows.len() as u64;
1722            return Ok(());
1723        }
1724
1725        // Sort remaining in-memory rows as one more "run".
1726        let key_columns = self.key_columns;
1727        let orders = self.sort_key_orders.clone();
1728        let colls = self.collations.clone();
1729        self.rows.sort_by(|lhs, rhs| {
1730            compare_sorter_rows(
1731                &lhs.values,
1732                &rhs.values,
1733                key_columns,
1734                &orders,
1735                &colls,
1736                &coll_guard,
1737            )
1738        });
1739
1740        // Collect all runs: disk runs first, then in-memory remainder.
1741        let mut run_iters: Vec<RunIterator> = Vec::with_capacity(self.spill_runs.len() + 1);
1742        for run in &self.spill_runs {
1743            run_iters.push(RunIterator::from_file(&run.path, self.key_columns)?);
1744        }
1745        if !self.rows.is_empty() {
1746            let mem_rows = std::mem::take(&mut self.rows);
1747            self.rows_sorted_total += mem_rows.len() as u64;
1748            run_iters.push(RunIterator::from_memory(mem_rows));
1749        }
1750
1751        // K-way merge using a simple tournament approach.
1752        let mut merged: Vec<SorterRow> = Vec::with_capacity(self.rows_sorted_total as usize);
1753
1754        // Advance all iterators to their first element.
1755        for iter in &mut run_iters {
1756            iter.advance()?;
1757        }
1758
1759        loop {
1760            // Find the run with the smallest current element.
1761            let mut best_idx: Option<usize> = None;
1762            for (i, iter) in run_iters.iter().enumerate() {
1763                let Some(row) = iter.current_values() else {
1764                    continue;
1765                };
1766                if let Some(bi) = best_idx {
1767                    if let Some(best_row) = run_iters[bi].current_values() {
1768                        if compare_sorter_rows(
1769                            row,
1770                            best_row,
1771                            key_columns,
1772                            &orders,
1773                            &colls,
1774                            &coll_guard,
1775                        ) == Ordering::Less
1776                        {
1777                            best_idx = Some(i);
1778                        }
1779                    }
1780                } else {
1781                    best_idx = Some(i);
1782                }
1783            }
1784
1785            let Some(idx) = best_idx else {
1786                break; // All runs exhausted.
1787            };
1788
1789            if let Some(row) = run_iters[idx].take_current() {
1790                merged.push(row);
1791            }
1792            run_iters[idx].advance()?;
1793        }
1794
1795        tracing::debug!(
1796            rows = merged.len(),
1797            runs = self.spill_runs.len() + 1,
1798            "sorter merge complete"
1799        );
1800
1801        // Clean up temp files.
1802        for run in &self.spill_runs {
1803            let _ = std::fs::remove_file(&run.path);
1804        }
1805        self.spill_runs.clear();
1806        self.rows = merged;
1807        drop(coll_guard);
1808        self.invalidate_output_row_cache();
1809        self.memory_used = 0;
1810        Ok(())
1811    }
1812
1813    /// Clear all rows and spill state (for `ResetSorter`).
1814    fn reset(&mut self) {
1815        self.rows.clear();
1816        self.position = None;
1817        self.invalidate_output_row_cache();
1818        self.memory_used = 0;
1819        self.next_source_sequence = 0;
1820        // Clean up temp files.
1821        for run in &self.spill_runs {
1822            let _ = std::fs::remove_file(&run.path);
1823        }
1824        self.spill_runs.clear();
1825    }
1826
1827    fn invalidate_output_row_cache(&mut self) {
1828        self.cached_row_position = None;
1829        self.cached_row_decode.invalidate();
1830    }
1831}
1832
1833impl Drop for SorterCursor {
1834    fn drop(&mut self) {
1835        for run in &self.spill_runs {
1836            let _ = std::fs::remove_file(&run.path);
1837        }
1838    }
1839}
1840
1841/// Iterator over records in a sorted run (either disk-backed or in-memory).
1842enum RunIterator {
1843    /// Records read from a temporary file.
1844    File {
1845        reader: std::io::BufReader<std::fs::File>,
1846        current: Option<SorterRow>,
1847        /// Number of leading sort-key columns to decode from spilled records.
1848        key_columns: usize,
1849    },
1850    /// Records from an in-memory Vec (used for the final unsorted batch).
1851    Memory {
1852        rows: std::vec::IntoIter<SorterRow>,
1853        current: Option<SorterRow>,
1854    },
1855}
1856
1857impl RunIterator {
1858    fn from_file(path: &std::path::Path, key_columns: usize) -> Result<Self> {
1859        let file = std::fs::File::open(path)
1860            .map_err(|e| FrankenError::internal(format!("sorter run open: {e}")))?;
1861        Ok(Self::File {
1862            reader: std::io::BufReader::new(file),
1863            current: None,
1864            key_columns,
1865        })
1866    }
1867
1868    fn from_memory(rows: Vec<SorterRow>) -> Self {
1869        Self::Memory {
1870            rows: rows.into_iter(),
1871            current: None,
1872        }
1873    }
1874
1875    fn current_values(&self) -> Option<&Vec<SqliteValue>> {
1876        match self {
1877            Self::File { current, .. } | Self::Memory { current, .. } => {
1878                current.as_ref().map(|r| &r.values)
1879            }
1880        }
1881    }
1882
1883    fn take_current(&mut self) -> Option<SorterRow> {
1884        match self {
1885            Self::File { current, .. } | Self::Memory { current, .. } => current.take(),
1886        }
1887    }
1888
1889    fn advance(&mut self) -> Result<()> {
1890        match self {
1891            Self::File {
1892                reader,
1893                current,
1894                key_columns,
1895            } => {
1896                use std::io::Read;
1897                let mut len_buf = [0u8; 4];
1898                match reader.read_exact(&mut len_buf) {
1899                    Ok(()) => {
1900                        let len = u32::from_le_bytes(len_buf) as usize;
1901                        let mut buf = vec![0u8; len];
1902                        reader
1903                            .read_exact(&mut buf)
1904                            .map_err(|e| FrankenError::internal(format!("sorter run read: {e}")))?;
1905                        // Decode only the sort-key prefix — not all columns.
1906                        let values = fsqlite_types::record::parse_record_prefix(&buf, *key_columns)
1907                            .ok_or_else(|| {
1908                                FrankenError::internal("sorter run: malformed record")
1909                            })?;
1910                        *current = Some(SorterRow {
1911                            values,
1912                            blob: buf,
1913                            source_sequence: 0,
1914                        });
1915                    }
1916                    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
1917                        *current = None;
1918                    }
1919                    Err(e) => {
1920                        return Err(FrankenError::internal(format!("sorter run read len: {e}")));
1921                    }
1922                }
1923            }
1924            Self::Memory { rows, current } => {
1925                *current = rows.next();
1926            }
1927        }
1928        Ok(())
1929    }
1930}
1931
1932// ── Shared Transaction Page I/O ─────────────────────────────────────────
1933//
1934// Phase 5 (bd-2a3y): Adapter that lets multiple `BtCursor` instances
1935// share a single pager transaction via `Rc<RefCell<…>>`.  The
1936// `PageReader`/`PageWriter` impls delegate through the `RefCell` borrow
1937// so that cursors can read/write pages on the real MVCC stack.
1938
1939// ── MVCC Concurrent Context (bd-kivg / 5E.2) ────────────────────────────
1940//
1941// When concurrent mode is enabled, page-level locks must be acquired
1942// before writes. The write set is used for FCW validation at commit time.
1943
1944/// MVCC concurrent mode context for page-level locking (bd-kivg / 5E.2).
1945///
1946/// When a transaction is in concurrent mode, this context enables:
1947/// - Acquiring page-level locks before writes via [`concurrent_write_page`]
1948/// - Recording written pages in the write set for FCW validation at commit
1949#[derive(Clone)]
1950struct ConcurrentContext {
1951    /// Session ID for this concurrent transaction.
1952    session_id: u64,
1953    /// Stable transaction ID used in hot-path logging.
1954    txn_id: u64,
1955    /// Immutable snapshot upper bound for this concurrent transaction.
1956    snapshot_high: CommitSeq,
1957    /// Stable shared handle for this concurrent transaction.
1958    handle: SharedConcurrentHandle,
1959    /// Shared reference to the page-level lock table.
1960    lock_table: Arc<InProcessPageLockTable>,
1961    /// Shared reference to the FCW commit index.
1962    commit_index: Arc<CommitIndex>,
1963    /// Busy-timeout budget used when contending on page-level locks.
1964    busy_timeout_ms: u64,
1965}
1966
1967impl ConcurrentContext {
1968    fn new(
1969        session_id: u64,
1970        handle: SharedConcurrentHandle,
1971        lock_table: Arc<InProcessPageLockTable>,
1972        commit_index: Arc<CommitIndex>,
1973        busy_timeout_ms: u64,
1974    ) -> Self {
1975        let (txn_id, snapshot_high) = {
1976            let handle_guard = handle.lock();
1977            (
1978                handle_guard.txn_token().id.get(),
1979                handle_guard.snapshot().high,
1980            )
1981        };
1982        Self {
1983            session_id,
1984            txn_id,
1985            snapshot_high,
1986            handle,
1987            lock_table,
1988            commit_index,
1989            busy_timeout_ms,
1990        }
1991    }
1992}
1993
1994/// Shared wrapper around a boxed [`TransactionHandle`] so multiple
1995/// storage cursors can share one transaction.
1996///
1997/// Optionally includes a `ConcurrentContext` for MVCC page-level locking
1998/// (bd-kivg / 5E.2).
1999#[derive(Clone)]
2000pub struct SharedTxnPageIo {
2001    txn: Rc<RefCell<TransactionKind>>,
2002    /// MVCC concurrent context (bd-kivg / 5E.2). When present, enables
2003    /// page-level locking for write operations.
2004    concurrent: Rc<RefCell<Option<ConcurrentContext>>>,
2005}
2006
2007#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2008enum ConcurrentWriteTier {
2009    Tier0AlreadyOwned,
2010    Tier1FirstTouch,
2011    Tier2CommitSurfaceRare,
2012}
2013
2014#[cfg(test)]
2015std::thread_local! {
2016    static CONCURRENT_PAGE_LOCK_WINDOW_HOOK: RefCell<Option<Box<dyn FnOnce()>>> =
2017        RefCell::new(None);
2018}
2019
2020#[cfg(test)]
2021fn install_concurrent_page_lock_window_hook(hook: impl FnOnce() + 'static) {
2022    CONCURRENT_PAGE_LOCK_WINDOW_HOOK.with(|slot| {
2023        let replaced = slot.borrow_mut().replace(Box::new(hook));
2024        assert!(
2025            replaced.is_none(),
2026            "concurrent page-lock window hook already installed"
2027        );
2028    });
2029}
2030
2031#[inline]
2032fn fire_concurrent_page_lock_window_hook() {
2033    #[cfg(test)]
2034    CONCURRENT_PAGE_LOCK_WINDOW_HOOK.with(|slot| {
2035        if let Some(hook) = slot.borrow_mut().take() {
2036            hook();
2037        }
2038    });
2039}
2040
2041impl std::fmt::Debug for SharedTxnPageIo {
2042    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2043        f.debug_struct("SharedTxnPageIo")
2044            .field("rc_count", &Rc::strong_count(&self.txn))
2045            .field("concurrent", &self.concurrent.borrow().is_some())
2046            .finish()
2047    }
2048}
2049
2050impl SharedTxnPageIo {
2051    fn from_parts(txn: TransactionKind, concurrent: Option<ConcurrentContext>) -> Self {
2052        Self {
2053            txn: Rc::new(RefCell::new(txn)),
2054            concurrent: Rc::new(RefCell::new(concurrent)),
2055        }
2056    }
2057
2058    #[cfg(test)]
2059    fn new(txn: impl Into<TransactionKind>) -> Self {
2060        Self::from_parts(txn.into(), None)
2061    }
2062
2063    /// Create with MVCC concurrent context (bd-kivg / 5E.2).
2064    pub fn with_concurrent(
2065        txn: impl Into<TransactionKind>,
2066        session_id: u64,
2067        handle: SharedConcurrentHandle,
2068        lock_table: Arc<InProcessPageLockTable>,
2069        commit_index: Arc<CommitIndex>,
2070        busy_timeout_ms: u64,
2071    ) -> Self {
2072        Self::from_parts(
2073            txn.into(),
2074            Some(ConcurrentContext::new(
2075                session_id,
2076                handle,
2077                lock_table,
2078                commit_index,
2079                busy_timeout_ms,
2080            )),
2081        )
2082    }
2083
2084    /// Unwrap back to the owned transaction handle.
2085    /// Returns an error if other Rc clones still exist.
2086    pub fn into_inner(self) -> Result<TransactionKind> {
2087        match Rc::try_unwrap(self.txn) {
2088            Ok(cell) => Ok(cell.into_inner()),
2089            Err(rc) => Err(FrankenError::Internal(format!(
2090                "SharedTxnPageIo: {} outstanding Rc references",
2091                Rc::strong_count(&rc),
2092            ))),
2093        }
2094    }
2095
2096    /// bd-perf: Swap the inner transaction without breaking Rc references.
2097    /// Retained StorageCursors hold clones of the same Rc, so replacing
2098    /// the inner value keeps them valid. Returns the old transaction.
2099    fn refill(
2100        &self,
2101        txn: TransactionKind,
2102        concurrent: Option<ConcurrentContext>,
2103    ) -> TransactionKind {
2104        self.concurrent.replace(concurrent);
2105        self.txn.replace(txn)
2106    }
2107
2108    /// bd-perf: Extract the inner transaction without consuming self.
2109    /// Used by take_transaction when cursors are retained — we need to
2110    /// return the txn to the connection while keeping the Rc alive for
2111    /// retained cursors (they'll get a fresh txn via refill next time).
2112    fn drain(&self) -> TransactionKind {
2113        self.txn.replace(TransactionKind::Drained)
2114    }
2115
2116    fn concurrent_context(&self) -> Option<ConcurrentContext> {
2117        self.concurrent.borrow().clone()
2118    }
2119
2120    fn clear_stale_synthetic_pending_commit_surface(
2121        &self,
2122        _cx: &Cx,
2123        operation: &str,
2124    ) -> Result<()> {
2125        let Some(ctx) = self.concurrent_context() else {
2126            return Ok(());
2127        };
2128        // The live engine only synthesizes conflict-only tracking for page 1.
2129        // Avoid rebuilding the full pending-commit surface after every write
2130        // just to learn that page 1 is still (or is no longer) required.
2131        let page_one_is_synthetic = {
2132            let handle = ctx.handle.lock();
2133            concurrent_page_is_synthetic_conflict_only(&handle, PageNumber::ONE)
2134        };
2135        if !page_one_is_synthetic {
2136            return Ok(());
2137        }
2138        if self.txn.borrow().page_one_in_pending_commit_surface()? {
2139            return Ok(());
2140        }
2141
2142        let metrics_enabled = vdbe_metrics_enabled();
2143        let clear_started = metrics_enabled.then(Instant::now);
2144        let mut handle = ctx.handle.lock();
2145        if concurrent_page_is_synthetic_conflict_only(&handle, PageNumber::ONE) {
2146            concurrent_clear_page_state(
2147                &mut handle,
2148                &ctx.lock_table,
2149                ctx.session_id,
2150                PageNumber::ONE,
2151            )
2152            .map_err(|restore_error| {
2153                FrankenError::Internal(format!(
2154                    "MVCC pending commit surface clear failed for page {} during {operation}: {restore_error}",
2155                    PageNumber::ONE.get()
2156                ))
2157            })?;
2158            add_vdbe_counter_if(
2159                metrics_enabled,
2160                &FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL,
2161                1,
2162            );
2163            if let Some(clear_started) = clear_started {
2164                add_vdbe_duration_if(
2165                    metrics_enabled,
2166                    &FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL,
2167                    clear_started,
2168                );
2169            }
2170        }
2171
2172        Ok(())
2173    }
2174
2175    fn classify_concurrent_write_tier(
2176        ctx: &ConcurrentContext,
2177        page_no: PageNumber,
2178    ) -> ConcurrentWriteTier {
2179        if ctx.handle.lock().holds_page_lock(page_no) {
2180            return ConcurrentWriteTier::Tier0AlreadyOwned;
2181        }
2182
2183        if page_no == PageNumber::ONE {
2184            ConcurrentWriteTier::Tier2CommitSurfaceRare
2185        } else {
2186            ConcurrentWriteTier::Tier1FirstTouch
2187        }
2188    }
2189
2190    fn restore_concurrent_page_state(
2191        ctx: &ConcurrentContext,
2192        page_state: &ConcurrentPageState,
2193        restore_label: &str,
2194    ) -> Result<()> {
2195        let mut handle = ctx.handle.lock();
2196        concurrent_restore_page_state(&mut handle, &ctx.lock_table, ctx.session_id, page_state)
2197            .map_err(|restore_error| {
2198                FrankenError::Internal(format!("{restore_label}: {restore_error}"))
2199            })
2200    }
2201
2202    fn post_acquire_fcw_conflict(
2203        ctx: &ConcurrentContext,
2204        page_no: PageNumber,
2205    ) -> Result<Option<CommitSeq>> {
2206        if !ctx.handle.lock().holds_page_lock(page_no) {
2207            return Err(FrankenError::Internal(format!(
2208                "MVCC page-lock acquisition for page {} returned success without ownership",
2209                page_no.get()
2210            )));
2211        }
2212        Ok(ctx
2213            .commit_index
2214            .latest(page_no)
2215            .filter(|seq| *seq > ctx.snapshot_high))
2216    }
2217
2218    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
2219    // await in this function's body. The engine executes strictly
2220    // sequentially per connection, but the reachable-reentrancy audit and
2221    // borrow-scope repair belong to the Phase-C reconstruction.
2222    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2223    async fn write_page_tier0_already_owned(
2224        &self,
2225        cx: &Cx,
2226        ctx: &ConcurrentContext,
2227        page_no: PageNumber,
2228        page_data_base: PageData,
2229    ) -> Result<()> {
2230        add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL, 1);
2231        let mut handle = ctx.handle.lock();
2232        let prior_state = concurrent_page_state(&handle, page_no);
2233        if let Err(prepare_error) =
2234            concurrent_prepare_write_page(&mut handle, &ctx.lock_table, ctx.session_id, page_no)
2235        {
2236            return Err(FrankenError::Internal(format!(
2237                "MVCC fast-path prepare failed: {prepare_error}"
2238            )));
2239        }
2240        if let Err(stage_error) = concurrent_stage_prepared_write_marker(&mut handle, page_no) {
2241            if let Err(restore_error) = concurrent_restore_page_state(
2242                &mut handle,
2243                &ctx.lock_table,
2244                ctx.session_id,
2245                &prior_state,
2246            ) {
2247                return Err(FrankenError::Internal(format!(
2248                    "MVCC fast-path staging failed: {stage_error}; MVCC fast-path restore failed: {restore_error}"
2249                )));
2250            }
2251            return Err(FrankenError::Internal(format!(
2252                "MVCC fast-path staging failed: {stage_error}"
2253            )));
2254        }
2255        drop(handle);
2256
2257        if let Err(write_error) = self
2258            .txn
2259            .borrow_mut()
2260            .write_page_data(cx, page_no, page_data_base)
2261            .await
2262        {
2263            Self::restore_concurrent_page_state(
2264                ctx,
2265                &prior_state,
2266                &format!("pager write_page failed: {write_error}; MVCC fast-path restore failed"),
2267            )?;
2268            return Err(write_error);
2269        }
2270
2271        self.clear_stale_synthetic_pending_commit_surface(cx, "write_page_fast")?;
2272        Ok(())
2273    }
2274
2275    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
2276    // await in this function's body. The engine executes strictly
2277    // sequentially per connection, but the reachable-reentrancy audit and
2278    // borrow-scope repair belong to the Phase-C reconstruction.
2279    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2280    async fn write_page_tier1_first_touch(
2281        &self,
2282        cx: &Cx,
2283        ctx: &ConcurrentContext,
2284        page_no: PageNumber,
2285        page_data_base: PageData,
2286    ) -> Result<()> {
2287        add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL, 1);
2288        let prior_page_state = {
2289            let handle = ctx.handle.lock();
2290            concurrent_page_state(&handle, page_no)
2291        };
2292
2293        let started = Instant::now();
2294        let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2295        loop {
2296            observe_execution_cancellation(cx)?;
2297            let (write_result, conflicting_commit_seq) = {
2298                let mut handle = ctx.handle.lock();
2299                let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2300                    .then(|| ctx.commit_index.latest(page_no))
2301                    .flatten()
2302                    .filter(|seq| *seq > ctx.snapshot_high);
2303                let write_result = conflicting_commit_seq.is_none().then(|| {
2304                    fire_concurrent_page_lock_window_hook();
2305                    concurrent_prepare_write_page(
2306                        &mut handle,
2307                        &ctx.lock_table,
2308                        ctx.session_id,
2309                        page_no,
2310                    )
2311                });
2312                (write_result, conflicting_commit_seq)
2313            };
2314            let txn_id = ctx.txn_id;
2315            let snapshot_high = ctx.snapshot_high.get();
2316
2317            if let Some(conflicting_commit_seq) = conflicting_commit_seq {
2318                add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2319                let error = FrankenError::BusySnapshot {
2320                    conflicting_pages: page_no.get().to_string(),
2321                };
2322                Self::restore_concurrent_page_state(
2323                    ctx,
2324                    &prior_page_state,
2325                    &format!("{error}; MVCC state restore failed"),
2326                )?;
2327                tracing::warn!(
2328                    txn_id,
2329                    commit_seq = conflicting_commit_seq.get(),
2330                    snapshot_high,
2331                    page_id = page_no.get(),
2332                    visibility_decision = "write_snapshot_stale",
2333                    conflict_reason = "fcw_base_drift",
2334                    "mvcc write rejected due to stale snapshot"
2335                );
2336                return Err(error);
2337            }
2338
2339            match write_result.ok_or_else(|| {
2340                FrankenError::Internal("write result must exist when snapshot is valid".to_owned())
2341            })? {
2342                Ok(()) => {
2343                    match Self::post_acquire_fcw_conflict(ctx, page_no) {
2344                        Ok(None) => {}
2345                        Ok(Some(conflicting_commit_seq)) => {
2346                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2347                            let error = FrankenError::BusySnapshot {
2348                                conflicting_pages: page_no.get().to_string(),
2349                            };
2350                            Self::restore_concurrent_page_state(
2351                                ctx,
2352                                &prior_page_state,
2353                                &format!("{error}; MVCC post-acquire state restore failed"),
2354                            )?;
2355                            tracing::warn!(
2356                                txn_id,
2357                                commit_seq = conflicting_commit_seq.get(),
2358                                snapshot_high,
2359                                page_id = page_no.get(),
2360                                visibility_decision = "write_post_acquire_snapshot_stale",
2361                                conflict_reason = "fcw_base_drift",
2362                                write_tier = "tier1_first_touch",
2363                                "mvcc write rejected after page-lock acquisition"
2364                            );
2365                            return Err(error);
2366                        }
2367                        Err(error) => {
2368                            Self::restore_concurrent_page_state(
2369                                ctx,
2370                                &prior_page_state,
2371                                &format!("{error}; MVCC post-acquire state restore failed"),
2372                            )?;
2373                            return Err(error);
2374                        }
2375                    }
2376                    tracing::debug!(
2377                        txn_id,
2378                        commit_seq = snapshot_high,
2379                        snapshot_high,
2380                        page_id = page_no.get(),
2381                        visibility_decision = "write_lock_acquired",
2382                        conflict_reason = "none",
2383                        write_tier = "tier1_first_touch",
2384                        "mvcc write visibility decision"
2385                    );
2386                    break;
2387                }
2388                Err(MvccError::Busy) => {
2389                    let remaining = deadline.saturating_sub(started.elapsed());
2390                    match wait_for_page_lock_holder_change(cx, ctx, page_no, remaining) {
2391                        Ok(true) => {
2392                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL, 1);
2393                            tracing::warn!(
2394                                txn_id,
2395                                commit_seq = snapshot_high,
2396                                snapshot_high,
2397                                page_id = page_no.get(),
2398                                visibility_decision = "write_retry",
2399                                conflict_reason = "page_lock_busy",
2400                                retry_policy = "park_wake",
2401                                retry_wait_ms = remaining.as_millis(),
2402                                write_tier = "tier1_first_touch",
2403                                "mvcc write conflict detected"
2404                            );
2405                        }
2406                        Ok(false) => {
2407                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL, 1);
2408                            let error = FrankenError::Busy;
2409                            Self::restore_concurrent_page_state(
2410                                ctx,
2411                                &prior_page_state,
2412                                &format!("{error}; MVCC state restore failed"),
2413                            )?;
2414                            tracing::warn!(
2415                                txn_id,
2416                                commit_seq = snapshot_high,
2417                                snapshot_high,
2418                                page_id = page_no.get(),
2419                                visibility_decision = "write_busy_timeout",
2420                                conflict_reason = "page_lock_busy",
2421                                retry_policy = "park_wake",
2422                                write_tier = "tier1_first_touch",
2423                                "mvcc write conflict exceeded busy timeout"
2424                            );
2425                            return Err(error);
2426                        }
2427                        Err(wait_error) => {
2428                            Self::restore_concurrent_page_state(
2429                                ctx,
2430                                &prior_page_state,
2431                                &format!("{wait_error}; MVCC state restore failed"),
2432                            )?;
2433                            return Err(wait_error);
2434                        }
2435                    }
2436                }
2437                Err(e) => {
2438                    let error = FrankenError::Internal(format!("MVCC write_page failed: {e}"));
2439                    Self::restore_concurrent_page_state(
2440                        ctx,
2441                        &prior_page_state,
2442                        &format!("{error}; MVCC state restore failed"),
2443                    )?;
2444                    tracing::warn!(
2445                        txn_id,
2446                        commit_seq = snapshot_high,
2447                        snapshot_high,
2448                        page_id = page_no.get(),
2449                        visibility_decision = "write_abort",
2450                        conflict_reason = %e,
2451                        write_tier = "tier1_first_touch",
2452                        "mvcc write failed"
2453                    );
2454                    return Err(error);
2455                }
2456            }
2457        }
2458
2459        {
2460            let mut handle = ctx.handle.lock();
2461            if let Err(stage_error) = concurrent_stage_prepared_write_marker(&mut handle, page_no) {
2462                Self::restore_concurrent_page_state(
2463                    ctx,
2464                    &prior_page_state,
2465                    &format!("MVCC write staging failed: {stage_error}; MVCC state restore failed"),
2466                )?;
2467                return Err(FrankenError::Internal(format!(
2468                    "MVCC write staging failed: {stage_error}"
2469                )));
2470            }
2471        }
2472
2473        if let Err(write_error) = self
2474            .txn
2475            .borrow_mut()
2476            .write_page_data(cx, page_no, page_data_base)
2477            .await
2478        {
2479            Self::restore_concurrent_page_state(
2480                ctx,
2481                &prior_page_state,
2482                &format!("pager write_page failed: {write_error}; MVCC state restore failed"),
2483            )?;
2484            return Err(write_error);
2485        }
2486
2487        self.clear_stale_synthetic_pending_commit_surface(cx, "write_page_tier1")?;
2488        Ok(())
2489    }
2490
2491    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
2492    // await in this function's body. The engine executes strictly
2493    // sequentially per connection, but the reachable-reentrancy audit and
2494    // borrow-scope repair belong to the Phase-C reconstruction.
2495    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2496    async fn write_page_tier2_commit_surface_rare(
2497        &self,
2498        cx: &Cx,
2499        ctx: &ConcurrentContext,
2500        page_no: PageNumber,
2501        page_data_base: PageData,
2502    ) -> Result<()> {
2503        add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL, 1);
2504        let page_one_tracking_required = if page_no == PageNumber::ONE {
2505            false
2506        } else {
2507            self.txn
2508                .borrow()
2509                .write_page_requires_page_one_conflict_tracking(page_no)?
2510        };
2511        let handle = ctx.handle.lock();
2512        let prior_page_state = Some(concurrent_page_state(&handle, page_no));
2513        let page_one_state =
2514            if page_one_tracking_required && !handle.tracks_write_conflict_page(PageNumber::ONE) {
2515                Some(concurrent_page_state(&handle, PageNumber::ONE))
2516            } else {
2517                None
2518            };
2519        drop(handle);
2520
2521        let restore_concurrent_state = || -> std::result::Result<(), String> {
2522            let mut handle = ctx.handle.lock();
2523            if let Some(prior_page_state) = prior_page_state.as_ref()
2524                && let Err(restore_error) = concurrent_restore_page_state(
2525                    &mut handle,
2526                    &ctx.lock_table,
2527                    ctx.session_id,
2528                    prior_page_state,
2529                )
2530            {
2531                return Err(format!("MVCC state restore failed: {restore_error}"));
2532            }
2533            if let Some(page_one_state) = page_one_state.as_ref()
2534                && let Err(restore_error) = concurrent_restore_page_state(
2535                    &mut handle,
2536                    &ctx.lock_table,
2537                    ctx.session_id,
2538                    page_one_state,
2539                )
2540            {
2541                return Err(format!("MVCC page1 restore failed: {restore_error}"));
2542            }
2543            Ok(())
2544        };
2545
2546        if page_one_state.is_some() {
2547            track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "write_page")?;
2548        }
2549        let started = Instant::now();
2550        let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2551
2552        loop {
2553            observe_execution_cancellation(cx)?;
2554            let (write_result, conflicting_commit_seq) = {
2555                let mut handle = ctx.handle.lock();
2556                let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2557                    .then(|| ctx.commit_index.latest(page_no))
2558                    .flatten()
2559                    .filter(|seq| *seq > ctx.snapshot_high);
2560                let write_result = conflicting_commit_seq.is_none().then(|| {
2561                    fire_concurrent_page_lock_window_hook();
2562                    concurrent_prepare_write_page(
2563                        &mut handle,
2564                        &ctx.lock_table,
2565                        ctx.session_id,
2566                        page_no,
2567                    )
2568                });
2569                (write_result, conflicting_commit_seq)
2570            };
2571            let txn_id = ctx.txn_id;
2572            let snapshot_high = ctx.snapshot_high.get();
2573
2574            if let Some(conflicting_commit_seq) = conflicting_commit_seq {
2575                add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2576                let error = FrankenError::BusySnapshot {
2577                    conflicting_pages: page_no.get().to_string(),
2578                };
2579                if let Err(restore_error) = restore_concurrent_state() {
2580                    return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2581                }
2582                tracing::warn!(
2583                    txn_id,
2584                    commit_seq = conflicting_commit_seq.get(),
2585                    snapshot_high,
2586                    page_id = page_no.get(),
2587                    visibility_decision = "write_snapshot_stale",
2588                    conflict_reason = "fcw_base_drift",
2589                    write_tier = "tier2_commit_surface_rare",
2590                    "mvcc write rejected due to stale snapshot"
2591                );
2592                return Err(error);
2593            }
2594
2595            match write_result.ok_or_else(|| {
2596                FrankenError::Internal("write result must exist when snapshot is valid".to_owned())
2597            })? {
2598                Ok(()) => {
2599                    match Self::post_acquire_fcw_conflict(ctx, page_no) {
2600                        Ok(None) => {}
2601                        Ok(Some(conflicting_commit_seq)) => {
2602                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2603                            let error = FrankenError::BusySnapshot {
2604                                conflicting_pages: page_no.get().to_string(),
2605                            };
2606                            if let Err(restore_error) = restore_concurrent_state() {
2607                                return Err(FrankenError::Internal(format!(
2608                                    "{error}; {restore_error}"
2609                                )));
2610                            }
2611                            tracing::warn!(
2612                                txn_id,
2613                                commit_seq = conflicting_commit_seq.get(),
2614                                snapshot_high,
2615                                page_id = page_no.get(),
2616                                visibility_decision = "write_post_acquire_snapshot_stale",
2617                                conflict_reason = "fcw_base_drift",
2618                                write_tier = "tier2_commit_surface_rare",
2619                                "mvcc write rejected after page-lock acquisition"
2620                            );
2621                            return Err(error);
2622                        }
2623                        Err(error) => {
2624                            if let Err(restore_error) = restore_concurrent_state() {
2625                                return Err(FrankenError::Internal(format!(
2626                                    "{error}; {restore_error}"
2627                                )));
2628                            }
2629                            return Err(error);
2630                        }
2631                    }
2632                    tracing::debug!(
2633                        txn_id,
2634                        commit_seq = snapshot_high,
2635                        snapshot_high,
2636                        page_id = page_no.get(),
2637                        visibility_decision = "write_lock_acquired",
2638                        conflict_reason = "none",
2639                        write_tier = "tier2_commit_surface_rare",
2640                        "mvcc write visibility decision"
2641                    );
2642                    break;
2643                }
2644                Err(MvccError::Busy) => {
2645                    let remaining = deadline.saturating_sub(started.elapsed());
2646                    match wait_for_page_lock_holder_change(cx, ctx, page_no, remaining) {
2647                        Ok(true) => {
2648                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL, 1);
2649                            tracing::warn!(
2650                                txn_id,
2651                                commit_seq = snapshot_high,
2652                                snapshot_high,
2653                                page_id = page_no.get(),
2654                                visibility_decision = "write_retry",
2655                                conflict_reason = "page_lock_busy",
2656                                retry_policy = "park_wake",
2657                                retry_wait_ms = remaining.as_millis(),
2658                                write_tier = "tier2_commit_surface_rare",
2659                                "mvcc write conflict detected"
2660                            );
2661                        }
2662                        Ok(false) => {
2663                            add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL, 1);
2664                            let error = FrankenError::Busy;
2665                            if let Err(restore_error) = restore_concurrent_state() {
2666                                return Err(FrankenError::Internal(format!(
2667                                    "{error}; {restore_error}"
2668                                )));
2669                            }
2670                            tracing::warn!(
2671                                txn_id,
2672                                commit_seq = snapshot_high,
2673                                snapshot_high,
2674                                page_id = page_no.get(),
2675                                visibility_decision = "write_busy_timeout",
2676                                conflict_reason = "page_lock_busy",
2677                                retry_policy = "park_wake",
2678                                write_tier = "tier2_commit_surface_rare",
2679                                "mvcc write conflict exceeded busy timeout"
2680                            );
2681                            return Err(error);
2682                        }
2683                        Err(wait_error) => {
2684                            if let Err(restore_error) = restore_concurrent_state() {
2685                                return Err(FrankenError::Internal(format!(
2686                                    "{wait_error}; {restore_error}"
2687                                )));
2688                            }
2689                            return Err(wait_error);
2690                        }
2691                    }
2692                }
2693                Err(e) => {
2694                    let error = FrankenError::Internal(format!("MVCC write_page failed: {e}"));
2695                    if let Err(restore_error) = restore_concurrent_state() {
2696                        return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2697                    }
2698                    tracing::warn!(
2699                        txn_id,
2700                        commit_seq = snapshot_high,
2701                        snapshot_high,
2702                        page_id = page_no.get(),
2703                        visibility_decision = "write_abort",
2704                        conflict_reason = %e,
2705                        write_tier = "tier2_commit_surface_rare",
2706                        "mvcc write failed"
2707                    );
2708                    return Err(error);
2709                }
2710            }
2711        }
2712
2713        let stage_result = {
2714            let mut handle = ctx.handle.lock();
2715            concurrent_stage_prepared_write_marker(&mut handle, page_no)
2716        };
2717        if let Err(stage_error) = stage_result {
2718            let error = FrankenError::Internal(format!("MVCC write staging failed: {stage_error}"));
2719            if let Err(restore_error) = restore_concurrent_state() {
2720                return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2721            }
2722            return Err(error);
2723        }
2724
2725        if let Err(write_error) = self
2726            .txn
2727            .borrow_mut()
2728            .write_page_data(cx, page_no, page_data_base)
2729            .await
2730        {
2731            let mut handle = ctx.handle.lock();
2732            if let Some(prior_page_state) = prior_page_state.as_ref()
2733                && let Err(restore_error) = concurrent_restore_page_state(
2734                    &mut handle,
2735                    &ctx.lock_table,
2736                    ctx.session_id,
2737                    prior_page_state,
2738                )
2739            {
2740                return Err(FrankenError::Internal(format!(
2741                    "pager write_page failed: {write_error}; MVCC state restore failed: {restore_error}"
2742                )));
2743            }
2744            if let Some(page_one_state) = page_one_state.as_ref()
2745                && let Err(restore_error) = concurrent_restore_page_state(
2746                    &mut handle,
2747                    &ctx.lock_table,
2748                    ctx.session_id,
2749                    page_one_state,
2750                )
2751            {
2752                return Err(FrankenError::Internal(format!(
2753                    "pager write_page failed: {write_error}; MVCC page1 restore failed: {restore_error}"
2754                )));
2755            }
2756            return Err(write_error);
2757        }
2758
2759        self.clear_stale_synthetic_pending_commit_surface(cx, "write_page")?;
2760        Ok(())
2761    }
2762
2763    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
2764    // await in this function's body. The engine executes strictly
2765    // sequentially per connection, but the reachable-reentrancy audit and
2766    // borrow-scope repair belong to the Phase-C reconstruction.
2767    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2768    async fn write_page_internal(
2769        &self,
2770        cx: &Cx,
2771        page_no: PageNumber,
2772        page_data_base: PageData,
2773    ) -> Result<()> {
2774        let Some(ctx) = self.concurrent_context() else {
2775            return self
2776                .txn
2777                .borrow_mut()
2778                .write_page_data(cx, page_no, page_data_base)
2779                .await;
2780        };
2781
2782        match Self::classify_concurrent_write_tier(&ctx, page_no) {
2783            ConcurrentWriteTier::Tier0AlreadyOwned => {
2784                self.write_page_tier0_already_owned(cx, &ctx, page_no, page_data_base)
2785                    .await
2786            }
2787            ConcurrentWriteTier::Tier1FirstTouch => {
2788                self.write_page_tier1_first_touch(cx, &ctx, page_no, page_data_base)
2789                    .await
2790            }
2791            ConcurrentWriteTier::Tier2CommitSurfaceRare => {
2792                self.write_page_tier2_commit_surface_rare(cx, &ctx, page_no, page_data_base)
2793                    .await
2794            }
2795        }
2796    }
2797}
2798
2799const PAGE_LOCK_WAIT_CANCELLATION_POLL: Duration = Duration::from_millis(5);
2800const PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL: Duration = Duration::from_millis(50);
2801
2802fn normalize_owned_page_data(page_size: usize, data: &[u8]) -> Result<PageData> {
2803    let metrics_enabled = vdbe_metrics_enabled();
2804    add_vdbe_counter_if(
2805        metrics_enabled,
2806        &FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL,
2807        1,
2808    );
2809    if data.len() == page_size {
2810        add_vdbe_counter_if(
2811            metrics_enabled,
2812            &FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL,
2813            1,
2814        );
2815        add_vdbe_counter_if(
2816            metrics_enabled,
2817            &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2818            u64::try_from(data.len()).unwrap_or(u64::MAX),
2819        );
2820        return Ok(PageData::from_vec(data.to_vec()));
2821    }
2822    if data.len() > page_size {
2823        return Err(FrankenError::Internal(format!(
2824            "page buffer exceeds page size invariant: {} > {}",
2825            data.len(),
2826            page_size
2827        )));
2828    }
2829
2830    let mut page = vec![0_u8; page_size];
2831    page[..data.len()].copy_from_slice(data);
2832    add_vdbe_counter_if(
2833        metrics_enabled,
2834        &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2835        u64::try_from(data.len()).unwrap_or(u64::MAX),
2836    );
2837    add_vdbe_counter_if(
2838        metrics_enabled,
2839        &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2840        u64::try_from(page_size.saturating_sub(data.len())).unwrap_or(u64::MAX),
2841    );
2842    Ok(PageData::from_vec(page))
2843}
2844
2845fn normalize_page_data_to_size(page_size: usize, data: PageData) -> Result<PageData> {
2846    let metrics_enabled = vdbe_metrics_enabled();
2847    add_vdbe_counter_if(
2848        metrics_enabled,
2849        &FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL,
2850        1,
2851    );
2852    if data.len() == page_size {
2853        add_vdbe_counter_if(
2854            metrics_enabled,
2855            &FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL,
2856            1,
2857        );
2858        return Ok(data);
2859    }
2860    if data.len() > page_size {
2861        return Err(FrankenError::Internal(format!(
2862            "page buffer exceeds page size invariant: {} > {}",
2863            data.len(),
2864            page_size
2865        )));
2866    }
2867
2868    let payload_len = data.len();
2869    let zero_fill_len = page_size.saturating_sub(payload_len);
2870    let mut data = data;
2871    if data.try_zero_extend_owned_to(page_size) {
2872        add_vdbe_counter_if(
2873            metrics_enabled,
2874            &FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL,
2875            1,
2876        );
2877        add_vdbe_counter_if(
2878            metrics_enabled,
2879            &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2880            u64::try_from(zero_fill_len).unwrap_or(u64::MAX),
2881        );
2882        return Ok(data);
2883    }
2884
2885    let mut page = vec![0_u8; page_size];
2886    page[..payload_len].copy_from_slice(data.as_bytes());
2887    add_vdbe_counter_if(
2888        metrics_enabled,
2889        &FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL,
2890        1,
2891    );
2892    add_vdbe_counter_if(
2893        metrics_enabled,
2894        &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2895        u64::try_from(payload_len).unwrap_or(u64::MAX),
2896    );
2897    add_vdbe_counter_if(
2898        metrics_enabled,
2899        &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2900        u64::try_from(zero_fill_len).unwrap_or(u64::MAX),
2901    );
2902    Ok(PageData::from_vec(page))
2903}
2904
2905fn wait_for_page_lock_holder_change(
2906    cx: &Cx,
2907    ctx: &ConcurrentContext,
2908    page_no: PageNumber,
2909    remaining: Duration,
2910) -> Result<bool> {
2911    // Outer check runs the full `Cx::checkpoint` (observes e-process oracle +
2912    // native cx plane) before entering the wait loop.
2913    observe_execution_cancellation(cx)?;
2914
2915    let Some(holder) = ctx.lock_table.holder(page_no) else {
2916        return Ok(true);
2917    };
2918    if remaining.is_zero() {
2919        return Ok(false);
2920    }
2921
2922    let metrics_enabled = vdbe_metrics_enabled();
2923    let started = Instant::now();
2924    let mut next_full_checkpoint = PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL;
2925    loop {
2926        let mut elapsed = started.elapsed();
2927        // Keep the hot per-slice path cheap, but do not make e-process/native
2928        // cancellation wait for the whole busy timeout when the same holder
2929        // stays parked on the page. The old code ran a full checkpoint every
2930        // 5 ms; this bounds non-local cancellation latency to 50 ms while
2931        // preserving most of the perf win from the cheap local check.
2932        if elapsed >= next_full_checkpoint {
2933            observe_execution_cancellation(cx)?;
2934            elapsed = started.elapsed();
2935            next_full_checkpoint = elapsed.saturating_add(PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL);
2936        } else if cx.is_cancel_requested() {
2937            return Err(FrankenError::Abort);
2938        }
2939
2940        let wait_budget = remaining.saturating_sub(elapsed);
2941        if wait_budget.is_zero() {
2942            return Ok(false);
2943        }
2944
2945        let wait_slice = wait_budget.min(PAGE_LOCK_WAIT_CANCELLATION_POLL);
2946        if ctx
2947            .lock_table
2948            .wait_for_holder_change(page_no, holder, wait_slice)
2949        {
2950            add_vdbe_counter_if(metrics_enabled, &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL, 1);
2951            add_vdbe_duration_if(
2952                metrics_enabled,
2953                &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL,
2954                started,
2955            );
2956            return Ok(true);
2957        }
2958
2959        if wait_budget == wait_slice {
2960            add_vdbe_counter_if(metrics_enabled, &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL, 1);
2961            add_vdbe_duration_if(
2962                metrics_enabled,
2963                &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL,
2964                started,
2965            );
2966            return Ok(false);
2967        }
2968    }
2969}
2970
2971fn track_concurrent_conflict_only_page(
2972    cx: &Cx,
2973    ctx: &ConcurrentContext,
2974    page_no: PageNumber,
2975    operation: &str,
2976) -> Result<()> {
2977    let metrics_enabled = vdbe_metrics_enabled();
2978    let track_started = metrics_enabled.then(Instant::now);
2979    let started = Instant::now();
2980    let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2981    let prior_page_state = {
2982        let handle = ctx.handle.lock();
2983        concurrent_page_state(&handle, page_no)
2984    };
2985
2986    loop {
2987        observe_execution_cancellation(cx)?;
2988        let (track_result, already_tracked, conflicting_commit_seq) = {
2989            let mut handle = ctx.handle.lock();
2990            let already_tracked = handle.tracks_write_conflict_page(page_no);
2991            let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2992                .then(|| ctx.commit_index.latest(page_no))
2993                .flatten()
2994                .filter(|seq| *seq > ctx.snapshot_high);
2995            let track_result = conflicting_commit_seq.is_none().then(|| {
2996                fire_concurrent_page_lock_window_hook();
2997                concurrent_track_write_conflict_page(
2998                    &mut handle,
2999                    &ctx.lock_table,
3000                    ctx.session_id,
3001                    page_no,
3002                )
3003            });
3004            (track_result, already_tracked, conflicting_commit_seq)
3005        };
3006        let txn_id = ctx.txn_id;
3007        let snapshot_high = ctx.snapshot_high.get();
3008
3009        if already_tracked {
3010            tracing::debug!(
3011                txn_id,
3012                commit_seq = snapshot_high,
3013                snapshot_high,
3014                page_id = page_no.get(),
3015                visibility_decision = "conflict_only_already_tracked",
3016                conflict_reason = "none",
3017                operation,
3018                "mvcc conflict-only page already tracked"
3019            );
3020            return Ok(());
3021        }
3022
3023        if let Some(conflicting_commit_seq) = conflicting_commit_seq {
3024            tracing::warn!(
3025                txn_id,
3026                commit_seq = conflicting_commit_seq.get(),
3027                snapshot_high,
3028                page_id = page_no.get(),
3029                visibility_decision = "conflict_only_snapshot_stale",
3030                conflict_reason = "fcw_base_drift",
3031                operation,
3032                "mvcc conflict-only page rejected due to stale snapshot"
3033            );
3034            return Err(FrankenError::BusySnapshot {
3035                conflicting_pages: page_no.get().to_string(),
3036            });
3037        }
3038
3039        match track_result.ok_or_else(|| {
3040            FrankenError::Internal("track result must exist when snapshot is valid".to_owned())
3041        })? {
3042            Ok(()) => {
3043                match SharedTxnPageIo::post_acquire_fcw_conflict(ctx, page_no) {
3044                    Ok(None) => {}
3045                    Ok(Some(conflicting_commit_seq)) => {
3046                        let error = FrankenError::BusySnapshot {
3047                            conflicting_pages: page_no.get().to_string(),
3048                        };
3049                        SharedTxnPageIo::restore_concurrent_page_state(
3050                            ctx,
3051                            &prior_page_state,
3052                            &format!("{error}; MVCC conflict-only state restore failed"),
3053                        )?;
3054                        tracing::warn!(
3055                            txn_id,
3056                            commit_seq = conflicting_commit_seq.get(),
3057                            snapshot_high,
3058                            page_id = page_no.get(),
3059                            visibility_decision = "conflict_only_post_acquire_snapshot_stale",
3060                            conflict_reason = "fcw_base_drift",
3061                            operation,
3062                            "mvcc conflict-only page rejected after lock acquisition"
3063                        );
3064                        return Err(error);
3065                    }
3066                    Err(error) => {
3067                        SharedTxnPageIo::restore_concurrent_page_state(
3068                            ctx,
3069                            &prior_page_state,
3070                            &format!("{error}; MVCC conflict-only state restore failed"),
3071                        )?;
3072                        return Err(error);
3073                    }
3074                }
3075                add_vdbe_counter_if(
3076                    metrics_enabled,
3077                    &FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL,
3078                    1,
3079                );
3080                if let Some(track_started) = track_started {
3081                    add_vdbe_duration_if(
3082                        metrics_enabled,
3083                        &FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL,
3084                        track_started,
3085                    );
3086                }
3087                tracing::debug!(
3088                    txn_id,
3089                    commit_seq = snapshot_high,
3090                    snapshot_high,
3091                    page_id = page_no.get(),
3092                    visibility_decision = "conflict_only_tracked",
3093                    conflict_reason = "none",
3094                    operation,
3095                    "mvcc conflict-only page tracked"
3096                );
3097                return Ok(());
3098            }
3099            Err(MvccError::Busy) => {
3100                let remaining = deadline.saturating_sub(started.elapsed());
3101                if !wait_for_page_lock_holder_change(cx, ctx, page_no, remaining)? {
3102                    tracing::warn!(
3103                        txn_id,
3104                        commit_seq = snapshot_high,
3105                        snapshot_high,
3106                        page_id = page_no.get(),
3107                        visibility_decision = "conflict_only_busy_timeout",
3108                        conflict_reason = "page_lock_busy",
3109                        operation,
3110                        retry_policy = "park_wake",
3111                        "mvcc conflict-only page exceeded busy timeout"
3112                    );
3113                    return Err(FrankenError::Busy);
3114                }
3115
3116                tracing::warn!(
3117                    txn_id,
3118                    commit_seq = snapshot_high,
3119                    snapshot_high,
3120                    page_id = page_no.get(),
3121                    visibility_decision = "conflict_only_retry",
3122                    conflict_reason = "page_lock_busy",
3123                    operation,
3124                    retry_policy = "park_wake",
3125                    retry_wait_ms = remaining.as_millis(),
3126                    "mvcc conflict-only page contention detected"
3127                );
3128            }
3129            Err(e) => {
3130                tracing::warn!(
3131                    txn_id,
3132                    commit_seq = snapshot_high,
3133                    snapshot_high,
3134                    page_id = page_no.get(),
3135                    visibility_decision = "conflict_only_abort",
3136                    conflict_reason = %e,
3137                    operation,
3138                    "mvcc conflict-only page tracking failed"
3139                );
3140                return Err(FrankenError::Internal(format!(
3141                    "MVCC conflict-only page tracking failed: {e}"
3142                )));
3143            }
3144        }
3145    }
3146}
3147
3148impl PageReader for SharedTxnPageIo {
3149    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
3150    // await in this function's body. The engine executes strictly
3151    // sequentially per connection, but the reachable-reentrancy audit and
3152    // borrow-scope repair belong to the Phase-C reconstruction.
3153    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3154    async fn read_page(&self, cx: &Cx, page_no: PageNumber) -> Result<Vec<u8>> {
3155        if let Some(ctx) = self.concurrent_context() {
3156            // Read-own-writes visibility: if this txn already wrote the page,
3157            // return the pager transaction's authoritative staged image first
3158            // and still record the read for SSI.
3159            let txn_id = ctx.txn_id;
3160            let snapshot_high = ctx.snapshot_high.get();
3161            let (handle, has_staged_write) = {
3162                let mut handle = ctx.handle.lock();
3163                // Read-only transactions never stage page state, so skip the
3164                // compound probe entirely on the empty-map fast path.
3165                let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3166                    concurrent_page_read_status(&handle, page_no)
3167                } else {
3168                    (false, false)
3169                };
3170                if is_freed {
3171                    return Err(FrankenError::DatabaseCorrupt {
3172                        detail: format!(
3173                            "page {} was freed earlier in concurrent transaction {}",
3174                            page_no.get(),
3175                            txn_id
3176                        ),
3177                    });
3178                }
3179                handle.record_read(page_no);
3180                (handle, has_staged_write)
3181            };
3182            drop(handle);
3183
3184            if has_staged_write {
3185                let page = self.txn.borrow().get_page(cx, page_no).await?;
3186                tracing::debug!(
3187                    txn_id,
3188                    commit_seq = snapshot_high,
3189                    snapshot_high,
3190                    page_id = page_no.get(),
3191                    visibility_decision = "write_set_hit",
3192                    conflict_reason = "none",
3193                    "mvcc visibility decision"
3194                );
3195                return Ok(page.into_vec());
3196            }
3197
3198            tracing::debug!(
3199                txn_id,
3200                commit_seq = snapshot_high,
3201                snapshot_high,
3202                page_id = page_no.get(),
3203                visibility_decision = "snapshot_pager_read",
3204                conflict_reason = "none",
3205                "mvcc visibility decision"
3206            );
3207        }
3208
3209        let page = self.txn.borrow().get_page(cx, page_no).await?.into_vec();
3210        Ok(page)
3211    }
3212
3213    // bd-perf: Override to avoid Vec<u8> round-trip (read_page returns Vec,
3214    // default read_page_data wraps in PageData — wasteful 4KB alloc+copy).
3215    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
3216    // await in this function's body. The engine executes strictly
3217    // sequentially per connection, but the reachable-reentrancy audit and
3218    // borrow-scope repair belong to the Phase-C reconstruction.
3219    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3220    async fn read_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3221        if let Some(ctx) = self.concurrent_context() {
3222            let has_staged_write = {
3223                let mut handle = ctx.handle.lock();
3224                let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3225                    concurrent_page_read_status(&handle, page_no)
3226                } else {
3227                    (false, false)
3228                };
3229                if is_freed {
3230                    return Err(FrankenError::DatabaseCorrupt {
3231                        detail: format!(
3232                            "page {} was freed earlier in concurrent transaction {}",
3233                            page_no.get(),
3234                            ctx.txn_id
3235                        ),
3236                    });
3237                }
3238                handle.record_read(page_no);
3239                has_staged_write
3240            };
3241
3242            if has_staged_write {
3243                return self.txn.borrow().get_page(cx, page_no).await;
3244            }
3245        }
3246        self.txn.borrow().get_page(cx, page_no).await
3247    }
3248
3249    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
3250    // await in this function's body. The engine executes strictly
3251    // sequentially per connection, but the reachable-reentrancy audit and
3252    // borrow-scope repair belong to the Phase-C reconstruction.
3253    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3254    async fn read_btree_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3255        if let Some(ctx) = self.concurrent_context() {
3256            let has_staged_write = {
3257                let handle = ctx.handle.lock();
3258                let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3259                    concurrent_page_read_status(&handle, page_no)
3260                } else {
3261                    (false, false)
3262                };
3263                if is_freed {
3264                    return Err(FrankenError::DatabaseCorrupt {
3265                        detail: format!(
3266                            "page {} was freed earlier in concurrent transaction {}",
3267                            page_no.get(),
3268                            ctx.txn_id
3269                        ),
3270                    });
3271                }
3272                has_staged_write
3273            };
3274
3275            if has_staged_write {
3276                return self.txn.borrow().get_page(cx, page_no).await;
3277            }
3278        }
3279        self.txn.borrow().get_page(cx, page_no).await
3280    }
3281
3282    fn record_read_witness(&self, _cx: &Cx, key: WitnessKey) {
3283        if let Some(ctx) = self.concurrent_context() {
3284            ctx.handle.lock().record_read_witness(key);
3285        }
3286    }
3287
3288    fn is_dirty(&self, page_no: PageNumber) -> bool {
3289        if let Some(ctx) = self.concurrent_context() {
3290            return ctx.handle.lock().tracks_write_conflict_page(page_no);
3291        }
3292        false
3293    }
3294}
3295
3296impl PageWriter for SharedTxnPageIo {
3297    async fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()> {
3298        let page_size = self.txn.borrow().page_size().as_usize();
3299        let page_data = normalize_owned_page_data(page_size, data)?;
3300        self.write_page_internal(cx, page_no, page_data).await
3301    }
3302
3303    async fn write_page_data(
3304        &mut self,
3305        cx: &Cx,
3306        page_no: PageNumber,
3307        data: PageData,
3308    ) -> Result<()> {
3309        let page_size = self.txn.borrow().page_size().as_usize();
3310        let page_data = normalize_page_data_to_size(page_size, data)?;
3311        self.write_page_internal(cx, page_no, page_data).await
3312    }
3313
3314    fn try_mutate_staged_page_data(
3315        &mut self,
3316        page_no: PageNumber,
3317        f: &mut dyn FnMut(&mut PageData),
3318    ) -> bool {
3319        if let Some(ctx) = self.concurrent_context() {
3320            let has_staged_write = {
3321                let handle = ctx.handle.lock();
3322                let (is_freed, has_staged_write) = concurrent_page_read_status(&handle, page_no);
3323                !is_freed && has_staged_write
3324            };
3325            if !has_staged_write {
3326                return false;
3327            }
3328        }
3329        self.txn
3330            .borrow_mut()
3331            .try_mutate_staged_page_data(page_no, f)
3332    }
3333
3334    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
3335    // await in this function's body. The engine executes strictly
3336    // sequentially per connection, but the reachable-reentrancy audit and
3337    // borrow-scope repair belong to the Phase-C reconstruction.
3338    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3339    async fn allocate_page(&mut self, cx: &Cx) -> Result<PageNumber> {
3340        let concurrent = self.concurrent_context();
3341        let page_one_tracking_required = self
3342            .concurrent_context()
3343            .as_ref()
3344            .map(|_| {
3345                self.txn
3346                    .borrow()
3347                    .allocate_page_requires_page_one_conflict_tracking()
3348            })
3349            .transpose()?
3350            .unwrap_or(false);
3351        let page_one_state = concurrent.as_ref().and_then(|ctx| {
3352            let handle = ctx.handle.lock();
3353            if page_one_tracking_required {
3354                if !handle.tracks_write_conflict_page(PageNumber::ONE) {
3355                    Some(concurrent_page_state(&handle, PageNumber::ONE))
3356                } else {
3357                    None
3358                }
3359            } else {
3360                None
3361            }
3362        });
3363        if let Some(ctx) = concurrent.as_ref()
3364            && page_one_state.is_some()
3365        {
3366            track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "allocate_page")?;
3367        }
3368        let allocate_result = self.txn.borrow_mut().allocate_page(cx).await;
3369        if let Err(allocate_error) = &allocate_result {
3370            if let (Some(ctx), Some(page_one_state)) =
3371                (concurrent.as_ref(), page_one_state.as_ref())
3372            {
3373                let mut handle = ctx.handle.lock();
3374                if let Err(restore_error) = concurrent_restore_page_state(
3375                    &mut handle,
3376                    &ctx.lock_table,
3377                    ctx.session_id,
3378                    page_one_state,
3379                ) {
3380                    return Err(FrankenError::Internal(format!(
3381                        "pager allocate_page failed: {allocate_error}; MVCC state restore failed: {restore_error}"
3382                    )));
3383                }
3384            }
3385        }
3386        let page_no = allocate_result?;
3387        self.clear_stale_synthetic_pending_commit_surface(cx, "allocate_page")?;
3388        Ok(page_no)
3389    }
3390
3391    // bd-h9o9r: a RefCell borrow (or sync mutex guard) is held across an
3392    // await in this function's body. The engine executes strictly
3393    // sequentially per connection, but the reachable-reentrancy audit and
3394    // borrow-scope repair belong to the Phase-C reconstruction.
3395    #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3396    async fn free_page(&mut self, cx: &Cx, page_no: PageNumber) -> Result<()> {
3397        let concurrent = self.concurrent_context();
3398        let page_one_tracking_required = self
3399            .concurrent_context()
3400            .as_ref()
3401            .map(|_| {
3402                self.txn
3403                    .borrow()
3404                    .free_page_requires_page_one_conflict_tracking(page_no)
3405            })
3406            .transpose()?
3407            .unwrap_or(false);
3408        let (prior_page_state, page_one_state) = if let Some(ctx) = concurrent.as_ref() {
3409            let handle = ctx.handle.lock();
3410            let prior_page_state = Some(concurrent_page_state(&handle, page_no));
3411            let page_one_state = if page_one_tracking_required {
3412                if !handle.tracks_write_conflict_page(PageNumber::ONE) {
3413                    Some(concurrent_page_state(&handle, PageNumber::ONE))
3414                } else {
3415                    None
3416                }
3417            } else {
3418                None
3419            };
3420            (prior_page_state, page_one_state)
3421        } else {
3422            (None, None)
3423        };
3424        if let Some(ctx) = concurrent.as_ref() {
3425            if page_one_state.is_some() {
3426                track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "free_page")?;
3427            }
3428            let concurrent_free_result = (|| -> Result<()> {
3429                let started = Instant::now();
3430                let deadline = Duration::from_millis(ctx.busy_timeout_ms);
3431
3432                loop {
3433                    observe_execution_cancellation(cx)?;
3434                    let (free_result, conflicting_commit_seq) = {
3435                        let mut handle = ctx.handle.lock();
3436                        let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
3437                            .then(|| ctx.commit_index.latest(page_no))
3438                            .flatten()
3439                            .filter(|seq| *seq > ctx.snapshot_high);
3440                        let free_result = conflicting_commit_seq.is_none().then(|| {
3441                            fire_concurrent_page_lock_window_hook();
3442                            concurrent_free_page(
3443                                &mut handle,
3444                                &ctx.lock_table,
3445                                ctx.session_id,
3446                                page_no,
3447                            )
3448                        });
3449                        (free_result, conflicting_commit_seq)
3450                    };
3451                    let txn_id = ctx.txn_id;
3452                    let snapshot_high = ctx.snapshot_high.get();
3453
3454                    if let Some(conflicting_commit_seq) = conflicting_commit_seq {
3455                        tracing::warn!(
3456                            txn_id,
3457                            commit_seq = conflicting_commit_seq.get(),
3458                            snapshot_high,
3459                            page_id = page_no.get(),
3460                            visibility_decision = "free_snapshot_stale",
3461                            conflict_reason = "fcw_base_drift",
3462                            "mvcc free rejected due to stale snapshot"
3463                        );
3464                        return Err(FrankenError::BusySnapshot {
3465                            conflicting_pages: page_no.get().to_string(),
3466                        });
3467                    }
3468
3469                    match free_result.ok_or_else(|| {
3470                        FrankenError::Internal(
3471                            "free result must exist when snapshot is valid".to_owned(),
3472                        )
3473                    })? {
3474                        Ok(()) => {
3475                            if let Some(conflicting_commit_seq) =
3476                                Self::post_acquire_fcw_conflict(ctx, page_no)?
3477                            {
3478                                tracing::warn!(
3479                                    txn_id,
3480                                    commit_seq = conflicting_commit_seq.get(),
3481                                    snapshot_high,
3482                                    page_id = page_no.get(),
3483                                    visibility_decision = "free_post_acquire_snapshot_stale",
3484                                    conflict_reason = "fcw_base_drift",
3485                                    "mvcc free rejected after page-lock acquisition"
3486                                );
3487                                return Err(FrankenError::BusySnapshot {
3488                                    conflicting_pages: page_no.get().to_string(),
3489                                });
3490                            }
3491                            tracing::debug!(
3492                                txn_id,
3493                                commit_seq = snapshot_high,
3494                                snapshot_high,
3495                                page_id = page_no.get(),
3496                                visibility_decision = "free_set_recorded",
3497                                conflict_reason = "none",
3498                                "mvcc free visibility decision"
3499                            );
3500                            return Ok(());
3501                        }
3502                        Err(MvccError::Busy) => {
3503                            let remaining = deadline.saturating_sub(started.elapsed());
3504                            if !wait_for_page_lock_holder_change(cx, ctx, page_no, remaining)? {
3505                                tracing::warn!(
3506                                    txn_id,
3507                                    commit_seq = snapshot_high,
3508                                    snapshot_high,
3509                                    page_id = page_no.get(),
3510                                    visibility_decision = "free_busy_timeout",
3511                                    conflict_reason = "page_lock_busy",
3512                                    retry_policy = "park_wake",
3513                                    "mvcc free conflict exceeded busy timeout"
3514                                );
3515                                return Err(FrankenError::Busy);
3516                            }
3517
3518                            tracing::warn!(
3519                                txn_id,
3520                                commit_seq = snapshot_high,
3521                                snapshot_high,
3522                                page_id = page_no.get(),
3523                                visibility_decision = "free_retry",
3524                                conflict_reason = "page_lock_busy",
3525                                retry_policy = "park_wake",
3526                                retry_wait_ms = remaining.as_millis(),
3527                                "mvcc free conflict detected"
3528                            );
3529                        }
3530                        Err(e) => {
3531                            tracing::warn!(
3532                                txn_id,
3533                                commit_seq = snapshot_high,
3534                                snapshot_high,
3535                                page_id = page_no.get(),
3536                                visibility_decision = "free_abort",
3537                                conflict_reason = %e,
3538                                "mvcc free failed"
3539                            );
3540                            return Err(FrankenError::Internal(format!(
3541                                "MVCC free_page failed: {e}"
3542                            )));
3543                        }
3544                    }
3545                }
3546            })();
3547            if let Err(error) = concurrent_free_result {
3548                let mut handle = ctx.handle.lock();
3549                if let Some(prior_page_state) = prior_page_state.as_ref()
3550                    && let Err(restore_error) = concurrent_restore_page_state(
3551                        &mut handle,
3552                        &ctx.lock_table,
3553                        ctx.session_id,
3554                        prior_page_state,
3555                    )
3556                {
3557                    return Err(FrankenError::Internal(format!(
3558                        "{error}; MVCC page restore failed: {restore_error}"
3559                    )));
3560                }
3561                if let Some(page_one_state) = page_one_state.as_ref()
3562                    && let Err(restore_error) = concurrent_restore_page_state(
3563                        &mut handle,
3564                        &ctx.lock_table,
3565                        ctx.session_id,
3566                        page_one_state,
3567                    )
3568                {
3569                    return Err(FrankenError::Internal(format!(
3570                        "{error}; MVCC page1 restore failed: {restore_error}"
3571                    )));
3572                }
3573                return Err(error);
3574            }
3575        }
3576        let free_result = self.txn.borrow_mut().free_page(cx, page_no).await;
3577        if let Err(free_error) = free_result {
3578            if let (Some(ctx), Some(prior_page_state)) =
3579                (concurrent.as_ref(), prior_page_state.as_ref())
3580            {
3581                let mut handle = ctx.handle.lock();
3582                if let Err(restore_error) = concurrent_restore_page_state(
3583                    &mut handle,
3584                    &ctx.lock_table,
3585                    ctx.session_id,
3586                    prior_page_state,
3587                ) {
3588                    return Err(FrankenError::Internal(format!(
3589                        "pager free_page failed: {free_error}; MVCC state restore failed: {restore_error}"
3590                    )));
3591                }
3592                if let Some(page_one_state) = page_one_state.as_ref() {
3593                    if let Err(restore_error) = concurrent_restore_page_state(
3594                        &mut handle,
3595                        &ctx.lock_table,
3596                        ctx.session_id,
3597                        page_one_state,
3598                    ) {
3599                        return Err(FrankenError::Internal(format!(
3600                            "pager free_page failed: {free_error}; MVCC page1 restore failed: {restore_error}"
3601                        )));
3602                    }
3603                }
3604            }
3605            return Err(free_error);
3606        }
3607        self.clear_stale_synthetic_pending_commit_surface(cx, "free_page")?;
3608        Ok(())
3609    }
3610
3611    fn record_write_witness(&mut self, cx: &Cx, key: WitnessKey) {
3612        if let Some(ctx) = self.concurrent_context() {
3613            ctx.handle.lock().record_write_witness(key);
3614            return;
3615        }
3616        self.txn.borrow_mut().record_write_witness(cx, key);
3617    }
3618}
3619
3620// ── Time-Travel Page I/O ──────────────────────────────────────────────
3621//
3622// Wraps a `SharedTxnPageIo` and a `TimeTravelSnapshot` + `VersionStore`
3623// to intercept page reads and return historical page versions when
3624// available. Falls back to the underlying transaction for pages not
3625// present in the version store (i.e., pages unchanged since the
3626// time-travel target).
3627
3628/// Read-only page I/O that serves historical page versions for
3629/// time-travel queries (`FOR SYSTEM_TIME AS OF ...`).
3630///
3631/// On `read_page`, the wrapper first resolves the page through the MVCC
3632/// `VersionStore` at the snapshot's commit sequence. If a historical
3633/// version is found, its `PageData` is returned directly. Otherwise the
3634/// read falls through to the underlying transaction (the page has not
3635/// changed since the target commit, so the current version is correct).
3636///
3637/// Write operations are unconditionally rejected — time-travel cursors
3638/// are strictly read-only.
3639#[derive(Clone)]
3640struct TimeTravelPageIo {
3641    /// Underlying transaction page I/O for fall-through reads.
3642    inner: SharedTxnPageIo,
3643    /// MVCC version store for historical page resolution.
3644    version_store: Arc<VersionStore>,
3645    /// The pinned time-travel snapshot.
3646    snapshot: TimeTravelSnapshot,
3647}
3648
3649impl std::fmt::Debug for TimeTravelPageIo {
3650    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3651        f.debug_struct("TimeTravelPageIo")
3652            .field("inner", &self.inner)
3653            .field("version_store", &"<Arc<VersionStore>>")
3654            .field(
3655                "target_commit_seq",
3656                &self.snapshot.target_commit_seq().get(),
3657            )
3658            .finish()
3659    }
3660}
3661
3662impl PageReader for TimeTravelPageIo {
3663    async fn read_page(&self, cx: &Cx, page_no: PageNumber) -> Result<Vec<u8>> {
3664        // Try to resolve the page at the historical snapshot first.
3665        let vs = &self.version_store;
3666        if let Some(idx) = self.snapshot.resolve_page(vs, page_no) {
3667            if let Some(version) = vs.get_version(idx) {
3668                tracing::trace!(
3669                    page_id = page_no.get(),
3670                    commit_seq = self.snapshot.target_commit_seq().get(),
3671                    "time-travel: serving historical page from version store"
3672                );
3673                return Ok(version.data.into_vec());
3674            }
3675        }
3676
3677        // The VersionStore has no version for this page at this snapshot.
3678        //
3679        // Fallthrough to the current transaction is only correct when the
3680        // VersionStore is actively tracking page versions (i.e., Native mode
3681        // with a populated commit stream). In that case, absence from the
3682        // store means the page has not changed since the target commit, so
3683        // the current on-disk version is the correct historical version.
3684        //
3685        // However, if the VersionStore is empty (page_count == 0), the MVCC
3686        // subsystem is not tracking any historical state. Falling through
3687        // would silently return current data, which is incorrect and
3688        // violates the time-travel query contract. In that case, we must
3689        // fail explicitly.
3690        let store_has_versions = vs.page_count() > 0;
3691
3692        if !store_has_versions {
3693            tracing::warn!(
3694                page_id = page_no.get(),
3695                commit_seq = self.snapshot.target_commit_seq().get(),
3696                "time-travel: VersionStore is empty — cannot serve historical \
3697                 page; historical data not available for this commit"
3698            );
3699            return Err(FrankenError::Internal(format!(
3700                "time-travel query failed: historical data not available for \
3701                 commit_seq={} (MVCC version store has no historical page \
3702                 versions; the database may be running in compatibility mode \
3703                 where time-travel is not yet supported)",
3704                self.snapshot.target_commit_seq().get(),
3705            )));
3706        }
3707
3708        // VersionStore is populated but this specific page is absent —
3709        // the page has not changed since the target commit, so the
3710        // current transaction's version is correct.
3711        tracing::trace!(
3712            page_id = page_no.get(),
3713            commit_seq = self.snapshot.target_commit_seq().get(),
3714            "time-travel: page not in version store (unchanged since target \
3715             commit), falling through to txn"
3716        );
3717        self.inner.read_page(cx, page_no).await
3718    }
3719
3720    async fn read_btree_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3721        let vs = &self.version_store;
3722        if let Some(idx) = self.snapshot.resolve_page(vs, page_no)
3723            && let Some(version) = vs.get_version(idx)
3724        {
3725            tracing::trace!(
3726                page_id = page_no.get(),
3727                commit_seq = self.snapshot.target_commit_seq().get(),
3728                "time-travel: serving historical B-tree page from version store"
3729            );
3730            return Ok(version.data);
3731        }
3732
3733        if vs.page_count() == 0 {
3734            tracing::warn!(
3735                page_id = page_no.get(),
3736                commit_seq = self.snapshot.target_commit_seq().get(),
3737                "time-travel: VersionStore is empty — cannot serve historical \
3738                 B-tree page; historical data not available for this commit"
3739            );
3740            return Err(FrankenError::Internal(format!(
3741                "time-travel query failed: historical data not available for \
3742                 commit_seq={} (MVCC version store has no historical page \
3743                 versions; the database may be running in compatibility mode \
3744                 where time-travel is not yet supported)",
3745                self.snapshot.target_commit_seq().get(),
3746            )));
3747        }
3748
3749        self.inner.read_btree_page_data(cx, page_no).await
3750    }
3751}
3752
3753// Desugared RPITIT form: `clippy::unused_async` ignores allow attributes on
3754// async-trait impl methods, and these read-only stubs answer synchronously.
3755impl PageWriter for TimeTravelPageIo {
3756    fn write_page(
3757        &mut self,
3758        _cx: &Cx,
3759        _page_no: PageNumber,
3760        _data: &[u8],
3761    ) -> impl std::future::Future<Output = Result<()>> {
3762        std::future::ready(Err(FrankenError::Internal(
3763            "time-travel cursors are read-only: write_page not permitted".to_owned(),
3764        )))
3765    }
3766
3767    fn allocate_page(&mut self, _cx: &Cx) -> impl std::future::Future<Output = Result<PageNumber>> {
3768        std::future::ready(Err(FrankenError::Internal(
3769            "time-travel cursors are read-only: allocate_page not permitted".to_owned(),
3770        )))
3771    }
3772
3773    fn free_page(
3774        &mut self,
3775        _cx: &Cx,
3776        _page_no: PageNumber,
3777    ) -> impl std::future::Future<Output = Result<()>> {
3778        std::future::ready(Err(FrankenError::Internal(
3779            "time-travel cursors are read-only: free_page not permitted".to_owned(),
3780        )))
3781    }
3782
3783    fn record_write_witness(&mut self, _cx: &Cx, _key: WitnessKey) {}
3784}
3785
3786// ── Cursor Backend Enum ────────────────────────────────────────────────
3787//
3788// Allows StorageCursor to work in three modes:
3789// - `Mem`: backed by MemPageStore (Phase 4 / tests)
3790// - `Txn`: backed by SharedTxnPageIo (Phase 5 production path)
3791// - `TimeTravel`: backed by TimeTravelPageIo (historical snapshot reads)
3792
3793/// Backend for a storage cursor, dispatching between in-memory,
3794/// transaction-backed, and time-travel page I/O.
3795enum CursorBackend {
3796    /// In-memory page store (used by tests and Phase 4 fallback).
3797    Mem(BtCursor<MemPageStore>),
3798    /// Real pager transaction (Phase 5 production path, bd-2a3y).
3799    Txn(BtCursor<SharedTxnPageIo>),
3800    /// Time-travel snapshot (historical reads via MVCC version store).
3801    TimeTravel(BtCursor<TimeTravelPageIo>),
3802}
3803
3804impl std::fmt::Debug for CursorBackend {
3805    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3806        match self {
3807            Self::Mem(c) => f.debug_tuple("Mem").field(c).finish(),
3808            Self::Txn(c) => f.debug_tuple("Txn").field(c).finish(),
3809            Self::TimeTravel(c) => f.debug_tuple("TimeTravel").field(c).finish(),
3810        }
3811    }
3812}
3813
3814impl CursorBackend {
3815    /// Returns `true` if this cursor is backed by the in-memory page store.
3816    #[must_use]
3817    fn is_mem(&self) -> bool {
3818        matches!(self, Self::Mem(_))
3819    }
3820
3821    /// Returns `true` if this cursor is backed by the real pager transaction.
3822    #[must_use]
3823    fn is_txn(&self) -> bool {
3824        matches!(self, Self::Txn(_))
3825    }
3826
3827    /// Returns `true` if this cursor is a time-travel cursor.
3828    #[must_use]
3829    #[allow(dead_code)]
3830    fn is_time_travel(&self) -> bool {
3831        matches!(self, Self::TimeTravel(_))
3832    }
3833
3834    /// Returns a string identifying the backend kind for diagnostics.
3835    #[must_use]
3836    #[allow(dead_code)]
3837    fn kind_str(&self) -> &'static str {
3838        match self {
3839            Self::Mem(_) => "mem",
3840            Self::Txn(_) => "txn",
3841            Self::TimeTravel(_) => "time_travel",
3842        }
3843    }
3844
3845    /// Whether the underlying B-tree cursor is for a table (intkey) B-tree.
3846    #[must_use]
3847    fn is_table_btree(&self) -> bool {
3848        match self {
3849            Self::Mem(c) => c.is_table(),
3850            Self::Txn(c) => c.is_table(),
3851            Self::TimeTravel(c) => c.is_table(),
3852        }
3853    }
3854
3855    #[must_use]
3856    fn usable_size(&self) -> u32 {
3857        match self {
3858            Self::Mem(c) => c.usable_size(),
3859            Self::Txn(c) => c.usable_size(),
3860            Self::TimeTravel(c) => c.usable_size(),
3861        }
3862    }
3863
3864    #[must_use]
3865    fn page_size(&self) -> u32 {
3866        match self {
3867            Self::Mem(c) => c.page_size(),
3868            Self::Txn(c) => c.page_size(),
3869            Self::TimeTravel(c) => c.page_size(),
3870        }
3871    }
3872
3873    #[must_use]
3874    fn index_desc_flags(&self) -> &[bool] {
3875        match self {
3876            Self::Mem(c) => c.index_desc_flags(),
3877            Self::Txn(c) => c.index_desc_flags(),
3878            Self::TimeTravel(c) => c.index_desc_flags(),
3879        }
3880    }
3881
3882    #[must_use]
3883    fn index_collations(&self) -> &[Option<String>] {
3884        match self {
3885            Self::Mem(c) => c.index_collations(),
3886            Self::Txn(c) => c.index_collations(),
3887            Self::TimeTravel(c) => c.index_collations(),
3888        }
3889    }
3890
3891    #[must_use]
3892    fn collation_registry(&self) -> Arc<Mutex<CollationRegistry>> {
3893        match self {
3894            Self::Mem(c) => c.collation_registry(),
3895            Self::Txn(c) => c.collation_registry(),
3896            Self::TimeTravel(c) => c.collation_registry(),
3897        }
3898    }
3899}
3900
3901/// Dispatch B-tree cursor operations across all backends.
3902impl CursorBackend {
3903    async fn first(&mut self, cx: &Cx) -> Result<bool> {
3904        match self {
3905            Self::Mem(c) => c.first(cx).await,
3906            Self::Txn(c) => c.first(cx).await,
3907            Self::TimeTravel(c) => c.first(cx).await,
3908        }
3909    }
3910
3911    async fn last(&mut self, cx: &Cx) -> Result<bool> {
3912        match self {
3913            Self::Mem(c) => c.last(cx).await,
3914            Self::Txn(c) => c.last(cx).await,
3915            Self::TimeTravel(c) => c.last(cx).await,
3916        }
3917    }
3918
3919    async fn next(&mut self, cx: &Cx) -> Result<bool> {
3920        match self {
3921            Self::Mem(c) => c.next(cx).await,
3922            Self::Txn(c) => c.next(cx).await,
3923            Self::TimeTravel(c) => c.next(cx).await,
3924        }
3925    }
3926
3927    /// bd-wwqen.1: Count all rows by walking leaf page headers without
3928    /// decoding cell payloads. Much cheaper than first()/while next().
3929    async fn count_all_rows(&mut self, cx: &Cx) -> Result<i64> {
3930        match self {
3931            Self::Mem(c) => c.count_all_rows(cx).await,
3932            Self::Txn(c) => c.count_all_rows(cx).await,
3933            Self::TimeTravel(c) => c.count_all_rows(cx).await,
3934        }
3935    }
3936
3937    async fn prev(&mut self, cx: &Cx) -> Result<bool> {
3938        match self {
3939            Self::Mem(c) => c.prev(cx).await,
3940            Self::Txn(c) => c.prev(cx).await,
3941            Self::TimeTravel(c) => c.prev(cx).await,
3942        }
3943    }
3944
3945    fn eof(&self) -> bool {
3946        match self {
3947            Self::Mem(c) => c.eof(),
3948            Self::Txn(c) => c.eof(),
3949            Self::TimeTravel(c) => c.eof(),
3950        }
3951    }
3952
3953    async fn rowid(&self, cx: &Cx) -> Result<i64> {
3954        match self {
3955            Self::Mem(c) => c.rowid(cx).await,
3956            Self::Txn(c) => c.rowid(cx).await,
3957            Self::TimeTravel(c) => c.rowid(cx).await,
3958        }
3959    }
3960
3961    async fn payload(&self, cx: &Cx) -> Result<Vec<u8>> {
3962        match self {
3963            Self::Mem(c) => c.payload(cx).await,
3964            Self::Txn(c) => c.payload(cx).await,
3965            Self::TimeTravel(c) => c.payload(cx).await,
3966        }
3967    }
3968
3969    async fn payload_into(&self, cx: &Cx, buf: &mut Vec<u8>) -> Result<()> {
3970        match self {
3971            Self::Mem(c) => c.payload_into(cx, buf).await,
3972            Self::Txn(c) => c.payload_into(cx, buf).await,
3973            Self::TimeTravel(c) => c.payload_into(cx, buf).await,
3974        }
3975    }
3976
3977    async fn payload_prefix_into(
3978        &self,
3979        cx: &Cx,
3980        max_prefix_bytes: usize,
3981        buf: &mut Vec<u8>,
3982    ) -> Result<()> {
3983        match self {
3984            Self::Mem(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3985            Self::Txn(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3986            Self::TimeTravel(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3987        }
3988    }
3989
3990    fn try_probe_current_first_index_key_integer_local(
3991        &self,
3992        probe_value: i64,
3993    ) -> Result<Option<(bool, i64)>> {
3994        match self {
3995            Self::Mem(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3996            Self::Txn(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3997            Self::TimeTravel(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3998        }
3999    }
4000
4001    async fn count_equal_first_index_key_run_integer_local_segment(
4002        &mut self,
4003        cx: &Cx,
4004        probe_value: i64,
4005    ) -> Result<FirstIndexKeyIntegerLocalRunSegment> {
4006        match self {
4007            Self::Mem(c) => {
4008                c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4009                    .await
4010            }
4011            Self::Txn(c) => {
4012                c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4013                    .await
4014            }
4015            Self::TimeTravel(c) => {
4016                c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4017                    .await
4018            }
4019        }
4020    }
4021
4022    async fn table_move_to(&mut self, cx: &Cx, rowid: i64) -> Result<SeekResult> {
4023        match self {
4024            Self::Mem(c) => c.table_move_to(cx, rowid).await,
4025            Self::Txn(c) => c.table_move_to(cx, rowid).await,
4026            Self::TimeTravel(c) => c.table_move_to(cx, rowid).await,
4027        }
4028    }
4029
4030    async fn table_advance_to(&mut self, cx: &Cx, rowid: i64) -> Result<SeekResult> {
4031        match self {
4032            Self::Mem(c) => c.advance_to(cx, rowid).await,
4033            Self::Txn(c) => c.advance_to(cx, rowid).await,
4034            Self::TimeTravel(c) => c.advance_to(cx, rowid).await,
4035        }
4036    }
4037
4038    async fn table_insert(&mut self, cx: &Cx, rowid: i64, data: &[u8]) -> Result<()> {
4039        match self {
4040            Self::Mem(c) => c.table_insert(cx, rowid, data).await,
4041            Self::Txn(c) => c.table_insert(cx, rowid, data).await,
4042            Self::TimeTravel(_) => Err(FrankenError::Internal(
4043                "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4044            )),
4045        }
4046    }
4047
4048    async fn table_insert_prechecked_absent(
4049        &mut self,
4050        cx: &Cx,
4051        rowid: i64,
4052        data: &[u8],
4053    ) -> Result<()> {
4054        match self {
4055            Self::Mem(c) => c.table_insert_prechecked_absent(cx, rowid, data).await,
4056            Self::Txn(c) => c.table_insert_prechecked_absent(cx, rowid, data).await,
4057            Self::TimeTravel(_) => Err(FrankenError::Internal(
4058                "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4059            )),
4060        }
4061    }
4062
4063    async fn table_refresh_rightmost_leaf_cache_after_insert(
4064        &mut self,
4065        cx: &Cx,
4066        rowid: i64,
4067    ) -> Result<()> {
4068        match self {
4069            Self::Mem(c) => {
4070                c.table_refresh_rightmost_leaf_cache_after_insert(cx, rowid)
4071                    .await
4072            }
4073            Self::Txn(c) => {
4074                c.table_refresh_rightmost_leaf_cache_after_insert(cx, rowid)
4075                    .await
4076            }
4077            Self::TimeTravel(_) => Err(FrankenError::Internal(
4078                "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4079            )),
4080        }
4081    }
4082
4083    async fn table_append_after_last_position(
4084        &mut self,
4085        cx: &Cx,
4086        rowid: i64,
4087        data: &[u8],
4088    ) -> Result<()> {
4089        match self {
4090            Self::Mem(c) => c.table_append_after_last_position(cx, rowid, data).await,
4091            Self::Txn(c) => c.table_append_after_last_position(cx, rowid, data).await,
4092            Self::TimeTravel(_) => Err(FrankenError::Internal(
4093                "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4094            )),
4095        }
4096    }
4097
4098    async fn table_append_after_last_position_with_writer<W>(
4099        &mut self,
4100        cx: &Cx,
4101        rowid: i64,
4102        payload_len: usize,
4103        writer: W,
4104    ) -> Result<bool>
4105    where
4106        W: FnOnce(&mut [u8]) -> Result<()>,
4107    {
4108        match self {
4109            Self::Mem(c) => {
4110                c.table_append_after_last_position_with_writer(cx, rowid, payload_len, writer)
4111                    .await
4112            }
4113            Self::Txn(c) => {
4114                c.table_append_after_last_position_with_writer(cx, rowid, payload_len, writer)
4115                    .await
4116            }
4117            Self::TimeTravel(_) => Err(FrankenError::Internal(
4118                "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4119            )),
4120        }
4121    }
4122
4123    async fn delete(&mut self, cx: &Cx) -> Result<()> {
4124        match self {
4125            Self::Mem(c) => c.delete(cx).await,
4126            Self::Txn(c) => c.delete(cx).await,
4127            Self::TimeTravel(_) => Err(FrankenError::Internal(
4128                "time-travel cursors are read-only: delete not permitted".to_owned(),
4129            )),
4130        }
4131    }
4132
4133    /// Position the cursor at the given key in an index B-tree.
4134    async fn index_move_to(&mut self, cx: &Cx, key: &[u8]) -> Result<SeekResult> {
4135        match self {
4136            Self::Mem(c) => c.index_move_to(cx, key).await,
4137            Self::Txn(c) => c.index_move_to(cx, key).await,
4138            Self::TimeTravel(c) => c.index_move_to(cx, key).await,
4139        }
4140    }
4141
4142    async fn index_move_to_upper_bound(&mut self, cx: &Cx, key: &[u8]) -> Result<()> {
4143        match self {
4144            Self::Mem(c) => c.index_move_to_upper_bound(cx, key).await,
4145            Self::Txn(c) => c.index_move_to_upper_bound(cx, key).await,
4146            Self::TimeTravel(c) => c.index_move_to_upper_bound(cx, key).await,
4147        }
4148    }
4149
4150    /// Insert a key into an index B-tree.
4151    async fn index_insert(&mut self, cx: &Cx, key: &[u8]) -> Result<()> {
4152        match self {
4153            Self::Mem(c) => c.index_insert(cx, key).await,
4154            Self::Txn(c) => c.index_insert(cx, key).await,
4155            Self::TimeTravel(_) => Err(FrankenError::Internal(
4156                "time-travel cursors are read-only: index_insert not permitted".to_owned(),
4157            )),
4158        }
4159    }
4160
4161    /// Insert a key into a UNIQUE index B-tree and report whether the key
4162    /// landed after the previously-existing rightmost key.
4163    async fn index_insert_unique_with_rightmost_report(
4164        &mut self,
4165        cx: &Cx,
4166        key: &[u8],
4167        n_unique_cols: usize,
4168        columns_label: &str,
4169    ) -> Result<bool> {
4170        match self {
4171            Self::Mem(c) => {
4172                c.index_insert_unique_with_rightmost_report(cx, key, n_unique_cols, columns_label)
4173                    .await
4174            }
4175            Self::Txn(c) => {
4176                c.index_insert_unique_with_rightmost_report(cx, key, n_unique_cols, columns_label)
4177                    .await
4178            }
4179            Self::TimeTravel(_) => Err(FrankenError::Internal(
4180                "time-travel cursors are read-only: index_insert_unique not permitted".to_owned(),
4181            )),
4182        }
4183    }
4184
4185    /// Append an index key from the current rightmost cursor position.
4186    async fn index_append_after_current_rightmost_position(
4187        &mut self,
4188        cx: &Cx,
4189        key: &[u8],
4190    ) -> Result<bool> {
4191        match self {
4192            Self::Mem(c) => {
4193                c.index_append_after_current_rightmost_position(cx, key)
4194                    .await
4195            }
4196            Self::Txn(c) => {
4197                c.index_append_after_current_rightmost_position(cx, key)
4198                    .await
4199            }
4200            Self::TimeTravel(_) => Err(FrankenError::Internal(
4201                "time-travel cursors are read-only: index_insert not permitted".to_owned(),
4202            )),
4203        }
4204    }
4205
4206    /// Force the cursor into EOF state so subsequent reads return NULL.
4207    ///
4208    /// Used by `OP_NullRow` to satisfy the SQLite contract that Column/Rowid
4209    /// after NullRow must return NULL.
4210    fn clear_position(&mut self) {
4211        match self {
4212            Self::Mem(c) => c.invalidate(),
4213            Self::Txn(c) => c.invalidate(),
4214            Self::TimeTravel(c) => c.invalidate(),
4215        }
4216    }
4217
4218    #[must_use]
4219    fn position_stamp(&self) -> Option<CursorPositionStamp> {
4220        match self {
4221            Self::Mem(c) => c.position_stamp(),
4222            Self::Txn(c) => c.position_stamp(),
4223            Self::TimeTravel(c) => c.position_stamp(),
4224        }
4225    }
4226}
4227
4228/// Storage-backed table cursor used by `OpenRead` and `OpenWrite`.
4229///
4230/// In Phase 5, `cursor` may be backed by either an in-memory [`MemPageStore`]
4231/// (for tests / Phase 4 fallback) or a real pager transaction via
4232/// [`SharedTxnPageIo`] (production path, bd-2a3y).
4233///
4234/// Decode/materialization scratch is owned by the cursor and reused only while
4235/// the cursor remains positioned on the same physical row image. Position
4236/// changes or write-path mutations invalidate it explicitly; nothing is shared
4237/// across cursors, statements, or transactions.
4238type RowDecodeScratch = fsqlite_types::record::RecordDecodeScratch;
4239
4240#[derive(Debug)]
4241struct StorageCursor {
4242    cursor: CursorBackend,
4243    cx: Cx,
4244    /// Whether this cursor was opened for writing (`OpenWrite`).
4245    writable: bool,
4246    /// Stable root page associated with this cursor.
4247    root_page: i32,
4248    /// Rowid allocation mode for this root page.
4249    rowid_mode: RowIdMode,
4250    /// AUTOINCREMENT lower bound loaded once per execution for this cursor.
4251    autoincrement_high_water: i64,
4252    /// Highest rowid allocated by `NewRowid` on this cursor (bd-1yi8).
4253    /// Ensures consecutive allocations return unique values even when
4254    /// no Insert has been issued between them.
4255    last_alloc_rowid: i64,
4256    /// Pre-allocated buffer to read payloads into without allocating.
4257    payload_buf: Vec<u8>,
4258    /// Scratch buffer for parsing target index keys.
4259    target_vals_buf: Vec<SqliteValue>,
4260    /// Scratch buffer for parsing current index keys.
4261    cur_vals_buf: Vec<SqliteValue>,
4262    /// Cursor-owned decode/materialization scratch for the current row image.
4263    row_decode: RowDecodeScratch,
4264    /// Cache the cursor's physical position to avoid redundant payload reads.
4265    last_position_stamp: Option<CursorPositionStamp>,
4266    /// Last rowid known to have been inserted on the right edge via this cursor.
4267    ///
4268    /// This is intentionally stricter than "last inserted rowid". We only keep
4269    /// it when the caller proved the insert landed at EOF/rightmost, because
4270    /// only then can a strictly larger next rowid safely reuse the append path.
4271    last_successful_insert_rowid: Option<i64>,
4272    /// Last UNIQUE index prefix known to have been inserted at the right edge.
4273    ///
4274    /// For monotonic composite UNIQUE streams, this lets `IdxInsert` skip the
4275    /// duplicate-probe seek and append from the current right-edge position.
4276    last_rightmost_unique_index_prefix: Option<Vec<SqliteValue>>,
4277    /// Cursor position that produced `last_rightmost_unique_index_prefix`.
4278    last_rightmost_unique_index_position: Option<CursorPositionStamp>,
4279    /// Cached rowid for the current cursor position (avoids repeated B-tree lookups).
4280    cached_rowid: Option<i64>,
4281    /// Cached result of payload_includes_rowid_alias check for the current row.
4282    payload_includes_rowid_alias: Option<bool>,
4283    /// Schema-derived IPK column index for this root page (avoids per-Column HashMap probes).
4284    ipk_col_idx: Option<usize>,
4285    /// Schema-derived column count for this root page.
4286    table_column_count: Option<usize>,
4287    /// Schema-derived first NOT NULL non-IPK column for this root page.
4288    first_not_null_non_ipk_col: Option<usize>,
4289}
4290
4291/// Lightweight version token for `MemDatabase` undo/rollback (bd-g6eo).
4292///
4293/// This is the MVCC-style snapshot identity for the in-memory store.
4294/// Returned by [`MemDatabase::undo_version`] and consumed by
4295/// [`MemDatabase::rollback_to`] to identify undo save-points.
4296/// The token is just the undo-log length — O(1) to capture, no cloning.
4297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4298#[must_use]
4299pub struct MemDbVersionToken(usize);
4300
4301#[derive(Debug, Clone)]
4302#[allow(dead_code)] // Variants constructed by MemDatabase methods not yet wired to VDBE opcodes.
4303enum MemDbUndoOp {
4304    CreateTable {
4305        root_page: i32,
4306        prev_next_root_page: i32,
4307    },
4308    DestroyTable {
4309        root_page: i32,
4310        table: MemTable,
4311    },
4312    ClearTable {
4313        root_page: i32,
4314        table: MemTable,
4315    },
4316    BumpRowid {
4317        root_page: i32,
4318        prev_next_rowid: i64,
4319    },
4320    UpsertRow {
4321        root_page: i32,
4322        rowid: i64,
4323        prev_next_rowid: i64,
4324        old_values: Option<MemRowValues>,
4325    },
4326    DeleteRow {
4327        root_page: i32,
4328        row: MemRow,
4329        prev_next_rowid: i64,
4330    },
4331}
4332
4333impl MemDbUndoOp {
4334    fn undo(self, db: &mut MemDatabase) {
4335        match self {
4336            Self::CreateTable {
4337                root_page,
4338                prev_next_root_page,
4339            } => {
4340                db.tables.remove(&root_page);
4341                db.next_root_page = prev_next_root_page;
4342            }
4343            Self::DestroyTable { root_page, table } | Self::ClearTable { root_page, table } => {
4344                db.tables.insert(root_page, table);
4345            }
4346            Self::BumpRowid {
4347                root_page,
4348                prev_next_rowid,
4349            } => {
4350                if let Some(table) = db.tables.get_mut(&root_page) {
4351                    table.next_rowid = prev_next_rowid;
4352                }
4353            }
4354            Self::UpsertRow {
4355                root_page,
4356                rowid,
4357                prev_next_rowid,
4358                old_values,
4359            } => {
4360                if let Some(table) = db.tables.get_mut(&root_page) {
4361                    match old_values {
4362                        Some(values) => {
4363                            table.insert(rowid, values);
4364                        }
4365                        None => {
4366                            table.delete_by_rowid(rowid);
4367                        }
4368                    }
4369                    table.next_rowid = prev_next_rowid;
4370                }
4371            }
4372            Self::DeleteRow {
4373                root_page,
4374                row,
4375                prev_next_rowid,
4376            } => {
4377                if let Some(table) = db.tables.get_mut(&root_page) {
4378                    table.insert(row.rowid, row.values);
4379                    table.next_rowid = prev_next_rowid;
4380                }
4381            }
4382        }
4383    }
4384}
4385
4386/// Shared in-memory database backing the VDBE engine's cursor operations.
4387///
4388/// Maps root page numbers to in-memory tables. The Connection layer
4389/// populates this when processing CREATE TABLE and passes it to the engine.
4390#[derive(Debug, Clone)]
4391pub struct MemDatabase {
4392    /// Tables indexed by root page number.
4393    pub tables: SwissIndex<i32, MemTable>,
4394    /// Next available root page number.
4395    next_root_page: i32,
4396    /// Whether undo logging is enabled for transaction/savepoint rollback.
4397    undo_enabled: bool,
4398    /// Undo log. A version token is the log length at the snapshot point.
4399    undo_log: Vec<MemDbUndoOp>,
4400}
4401
4402impl MemDatabase {
4403    /// Create a new empty in-memory database.
4404    pub fn new() -> Self {
4405        Self {
4406            tables: SwissIndex::new(),
4407            next_root_page: 2, // Page 1 is reserved for sqlite_master.
4408            undo_enabled: false,
4409            undo_log: Vec::new(),
4410        }
4411    }
4412
4413    /// Propagate a shared collation registry to all tracked MemTables.
4414    pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
4415        for (_, table) in self.tables.iter_mut() {
4416            table.set_collation_registry(Arc::clone(&registry));
4417        }
4418    }
4419
4420    /// Returns the number of tables in the database.
4421    #[must_use]
4422    pub fn table_count(&self) -> i32 {
4423        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
4424        let count = self.tables.len() as i32;
4425        count
4426    }
4427
4428    /// Return the next root page that would be assigned by `create_table`.
4429    #[must_use]
4430    pub fn next_root_page(&self) -> i32 {
4431        self.next_root_page
4432    }
4433
4434    /// Advance the root page counter so future allocations start at `val`.
4435    ///
4436    /// Used to prevent MemDatabase root pages from colliding with pager-
4437    /// allocated pages (e.g. when materializing sqlite_master temp tables).
4438    pub fn set_next_root_page(&mut self, val: i32) {
4439        self.next_root_page = val;
4440    }
4441
4442    /// Allocate and return the next root page number without creating a table.
4443    ///
4444    /// Used by `OpenAutoindex` which needs a unique page number for a
4445    /// `MemPageStore`-backed index cursor but must NOT pollute `self.tables`
4446    /// (a spurious `MemTable` entry would cause `get_table()` to return
4447    /// `Some`, misleading `open_storage_cursor` into treating the page as a
4448    /// table B-tree).
4449    pub fn allocate_root_page(&mut self) -> i32 {
4450        let root_page = self.next_root_page;
4451        self.next_root_page += 1;
4452        root_page
4453    }
4454
4455    /// Create a table and return its root page number.
4456    pub fn create_table(&mut self, num_columns: usize) -> i32 {
4457        let prev_next_root_page = self.next_root_page;
4458        let root_page = prev_next_root_page;
4459        self.next_root_page += 1;
4460        self.tables.insert(root_page, MemTable::new(num_columns));
4461        self.push_undo(MemDbUndoOp::CreateTable {
4462            root_page,
4463            prev_next_root_page,
4464        });
4465        root_page
4466    }
4467
4468    /// Create a table at a specific root page number.
4469    ///
4470    /// Used by the storage layer (5A.3) when the root page is allocated
4471    /// from the pager rather than auto-assigned.  Advances
4472    /// `next_root_page` past `root_page` if necessary so that future
4473    /// `create_table()` calls do not collide.
4474    pub fn create_table_at(&mut self, root_page: i32, num_columns: usize) {
4475        let prev_next_root_page = self.next_root_page;
4476        if root_page >= self.next_root_page {
4477            self.next_root_page = root_page + 1;
4478        }
4479        self.tables.insert(root_page, MemTable::new(num_columns));
4480        self.push_undo(MemDbUndoOp::CreateTable {
4481            root_page,
4482            prev_next_root_page,
4483        });
4484    }
4485
4486    /// Get a reference to a table by root page.
4487    pub fn get_table(&self, root_page: i32) -> Option<&MemTable> {
4488        self.tables.get(&root_page)
4489    }
4490
4491    /// Get a mutable reference to a table by root page.
4492    pub fn get_table_mut(&mut self, root_page: i32) -> Option<&mut MemTable> {
4493        self.tables.get_mut(&root_page)
4494    }
4495
4496    fn push_undo(&mut self, op: MemDbUndoOp) {
4497        if self.undo_enabled {
4498            self.undo_log.push(op);
4499        }
4500    }
4501
4502    /// Return the current undo-version token.
4503    ///
4504    /// This is the identity captured in snapshots for savepoints/transactions.
4505    pub fn undo_version(&self) -> MemDbVersionToken {
4506        MemDbVersionToken(self.undo_log.len())
4507    }
4508
4509    /// Begin a new undo region (transaction start).
4510    pub fn begin_undo(&mut self) {
4511        self.undo_enabled = true;
4512        self.undo_log.clear();
4513    }
4514
4515    /// End the undo region (transaction committed/finished).
4516    pub fn commit_undo(&mut self) {
4517        self.undo_enabled = false;
4518        self.undo_log.clear();
4519    }
4520
4521    /// Restore the database to a previously captured undo-version token.
4522    pub fn rollback_to(&mut self, token: MemDbVersionToken) {
4523        while self.undo_log.len() > token.0 {
4524            if let Some(op) = self.undo_log.pop() {
4525                op.undo(self);
4526            }
4527        }
4528    }
4529
4530    /// Drop a table by root page and record undo information.
4531    pub fn destroy_table(&mut self, root_page: i32) {
4532        if let Some(table) = self.tables.remove(&root_page) {
4533            self.push_undo(MemDbUndoOp::DestroyTable { root_page, table });
4534        }
4535    }
4536
4537    fn clear_table(&mut self, root_page: i32) {
4538        let prev = self.tables.get(&root_page).cloned();
4539        if let Some(table) = prev {
4540            self.push_undo(MemDbUndoOp::ClearTable { root_page, table });
4541        }
4542        if let Some(table) = self.tables.get_mut(&root_page) {
4543            table.clear();
4544        }
4545    }
4546
4547    fn alloc_rowid(&mut self, root_page: i32) -> i64 {
4548        if let Some(table) = self.tables.get_mut(&root_page) {
4549            let prev_next_rowid = table.next_rowid;
4550            let rowid = table.alloc_rowid();
4551            self.push_undo(MemDbUndoOp::BumpRowid {
4552                root_page,
4553                prev_next_rowid,
4554            });
4555            rowid
4556        } else {
4557            1
4558        }
4559    }
4560
4561    /// Allocate a rowid for concurrent mode (`OP_NewRowid` with `p3 != 0`).
4562    ///
4563    /// Unlike the serialized path (counter only), this path derives the next
4564    /// candidate strictly from the visible table contents (`max(rowid) + 1`).
4565    /// This avoids relying on potentially stale local counter state.
4566    fn alloc_rowid_concurrent(&mut self, root_page: i32) -> i64 {
4567        if let Some(table) = self.tables.get_mut(&root_page) {
4568            let prev_next_rowid = table.next_rowid;
4569            let max_visible = table.max_visible_rowid().unwrap_or(0);
4570            let rowid = max_visible.saturating_add(1);
4571            table.next_rowid = rowid.saturating_add(1);
4572            self.push_undo(MemDbUndoOp::BumpRowid {
4573                root_page,
4574                prev_next_rowid,
4575            });
4576            rowid
4577        } else {
4578            1
4579        }
4580    }
4581
4582    pub fn upsert_row<V>(&mut self, root_page: i32, rowid: i64, values: V)
4583    where
4584        V: Into<MemRowValues>,
4585    {
4586        let values = values.into();
4587        if let Some(table) = self.tables.get_mut(&root_page) {
4588            let prev_next_rowid = table.next_rowid;
4589            // Use binary search (O(log n)) instead of linear scan (O(n))
4590            // since rows are maintained in rowid-sorted order.
4591            let old_values = table
4592                .rows
4593                .binary_search_by_key(&rowid, |r| r.rowid)
4594                .ok()
4595                .map(|idx| table.rows[idx].values.clone());
4596            table.insert(rowid, values);
4597            self.push_undo(MemDbUndoOp::UpsertRow {
4598                root_page,
4599                rowid,
4600                prev_next_rowid,
4601                old_values,
4602            });
4603        }
4604    }
4605
4606    /// Remove a column slot from every row in a TEMP table.
4607    ///
4608    /// The pre-rewrite table image is recorded as one undo entry, so an
4609    /// enclosing statement savepoint restores row widths, UNIQUE metadata,
4610    /// and the column count together.
4611    pub fn remove_column_from_rows(&mut self, root_page: i32, removed_slot: usize) -> bool {
4612        let Some(previous) = self.tables.get(&root_page).cloned() else {
4613            return false;
4614        };
4615        if previous
4616            .unique_constraints
4617            .iter()
4618            .any(|constraint| constraint.columns.contains(&removed_slot))
4619        {
4620            return false;
4621        }
4622        self.push_undo(MemDbUndoOp::ClearTable {
4623            root_page,
4624            table: previous,
4625        });
4626        if let Some(table) = self.tables.get_mut(&root_page) {
4627            table.remove_column_from_rows(removed_slot);
4628        }
4629        true
4630    }
4631
4632    /// Allocate an implicit rowid and insert the row, recording one undo entry
4633    /// for both the rowid counter advance and the row mutation. If
4634    /// `rowid_value_column` is present, that column is patched to the allocated
4635    /// rowid before insertion.
4636    #[must_use]
4637    pub fn insert_auto_row<V>(
4638        &mut self,
4639        root_page: i32,
4640        values: V,
4641        rowid_value_column: Option<usize>,
4642    ) -> Option<i64>
4643    where
4644        V: Into<MemRowValues>,
4645    {
4646        let mut values = values.into();
4647        if let Some(table) = self.tables.get_mut(&root_page) {
4648            let prev_next_rowid = table.next_rowid;
4649            let rowid = table.alloc_rowid();
4650            if let Some(column) = rowid_value_column
4651                && let Some(value) = values.get_mut(column)
4652            {
4653                *value = SqliteValue::Integer(rowid);
4654            }
4655            let old_values = table
4656                .rows
4657                .binary_search_by_key(&rowid, |r| r.rowid)
4658                .ok()
4659                .map(|idx| table.rows[idx].values.clone());
4660            table.insert(rowid, values);
4661            self.push_undo(MemDbUndoOp::UpsertRow {
4662                root_page,
4663                rowid,
4664                prev_next_rowid,
4665                old_values,
4666            });
4667            Some(rowid)
4668        } else {
4669            None
4670        }
4671    }
4672
4673    /// Delete a row by rowid, recording undo information for rollback.
4674    pub fn delete_rowid(&mut self, root_page: i32, rowid: i64) -> bool {
4675        if let Some(table) = self.tables.get_mut(&root_page)
4676            && let Ok(index) = table.rows.binary_search_by_key(&rowid, |r| r.rowid)
4677        {
4678            let prev_next_rowid = table.next_rowid;
4679            let row = table.rows.remove(index);
4680            table.remove_unique_entries(row.rowid, &row.values);
4681            self.push_undo(MemDbUndoOp::DeleteRow {
4682                root_page,
4683                row,
4684                prev_next_rowid,
4685            });
4686            true
4687        } else {
4688            false
4689        }
4690    }
4691
4692    #[allow(dead_code)]
4693    fn delete_at(&mut self, root_page: i32, index: usize) {
4694        if let Some(table) = self.tables.get_mut(&root_page) {
4695            if index < table.rows.len() {
4696                let prev_next_rowid = table.next_rowid;
4697                let row = table.rows.remove(index);
4698                table.remove_unique_entries(row.rowid, &row.values);
4699                self.push_undo(MemDbUndoOp::DeleteRow {
4700                    root_page,
4701                    row,
4702                    prev_next_rowid,
4703                });
4704            }
4705        }
4706    }
4707}
4708
4709impl Default for MemDatabase {
4710    fn default() -> Self {
4711        Self::new()
4712    }
4713}
4714
4715// NOTE: MemDatabase intentionally does NOT implement Clone.
4716// Snapshot reads use the lightweight `MemDbVersionToken` (undo-log index)
4717// rather than cloning the entire table state.  See bd-g6eo.
4718
4719const VDBE_TRACE_ENV: &str = "FSQLITE_VDBE_TRACE_OPCODES";
4720const VDBE_TRACE_LOGGING_STANDARD: &str = "bd-1fpm";
4721
4722/// Slow query threshold for INFO-level logging (100ms).
4723const SLOW_QUERY_THRESHOLD_MS: u128 = 100;
4724
4725// ── VDBE execution metrics (bd-1rw.1) ──────────────────────────────────────
4726
4727/// Total number of VDBE opcodes executed across all statements.
4728static FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL: AtomicU64 = AtomicU64::new(0);
4729/// Total number of VDBE statements executed.
4730static FSQLITE_VDBE_STATEMENTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4731/// Cumulative statement duration in microseconds (for histogram approximation).
4732static FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL: AtomicU64 = AtomicU64::new(0);
4733/// Dynamic execution counts for each opcode, indexed by raw opcode byte.
4734static FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS: LazyLock<Box<[AtomicU64]>> = LazyLock::new(|| {
4735    (0..=Opcode::COUNT)
4736        .map(|_| AtomicU64::new(0))
4737        .collect::<Vec<_>>()
4738        .into_boxed_slice()
4739});
4740/// Total number of type-coercion attempts in Cast/Affinity opcodes.
4741static FSQLITE_VDBE_TYPE_COERCIONS_TOTAL: AtomicU64 = AtomicU64::new(0);
4742/// Total number of coercions that changed a value's storage class.
4743static FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL: AtomicU64 = AtomicU64::new(0);
4744/// Total number of storage cursor column reads.
4745static FSQLITE_VDBE_COLUMN_READS_TOTAL: AtomicU64 = AtomicU64::new(0);
4746/// Total number of record decode calls that materialized a full row vector.
4747static FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4748/// Total number of decode-cache hits across storage, sorter, and pseudo-row paths.
4749static FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL: AtomicU64 = AtomicU64::new(0);
4750/// Total number of decode-cache misses across storage, sorter, and pseudo-row paths.
4751static FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL: AtomicU64 = AtomicU64::new(0);
4752/// Total number of decode-cache invalidations caused by row-position changes.
4753static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL: AtomicU64 = AtomicU64::new(0);
4754/// Total number of decode-cache invalidations caused by write-path mutations.
4755static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL: AtomicU64 = AtomicU64::new(0);
4756/// Total number of decode-cache invalidations caused by pseudo-row image changes.
4757static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL: AtomicU64 = AtomicU64::new(0);
4758/// Total number of values materialized while decoding records/columns.
4759static FSQLITE_VDBE_DECODED_VALUES_TOTAL: AtomicU64 = AtomicU64::new(0);
4760/// Estimated heap bytes materialized while decoding records/columns.
4761static FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4762/// Total number of result rows emitted by the interpreter.
4763static FSQLITE_VDBE_RESULT_ROWS_TOTAL: AtomicU64 = AtomicU64::new(0);
4764/// Total number of result values materialized for emitted rows.
4765static FSQLITE_VDBE_RESULT_VALUES_TOTAL: AtomicU64 = AtomicU64::new(0);
4766/// Estimated heap bytes materialized for emitted result rows.
4767static FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4768/// Cumulative nanoseconds spent materializing emitted result rows.
4769static FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
4770/// Total number of MakeRecord calls.
4771static FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4772/// Total bytes produced by MakeRecord blobs.
4773static FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4774#[cfg(test)]
4775thread_local! {
4776    /// Test-only per-thread count of sideband MakeRecord values that had to become Arc-backed blobs.
4777    static FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL: Cell<u64> = const { Cell::new(0) };
4778}
4779/// bd-7vkes: Count times the BTREE_APPEND fast path was taken (skip seek).
4780static FSQLITE_VDBE_INSERT_APPEND_COUNT: AtomicU64 = AtomicU64::new(0);
4781/// bd-7vkes: Count times a full B-tree seek was needed for INSERT.
4782static FSQLITE_VDBE_INSERT_SEEK_COUNT: AtomicU64 = AtomicU64::new(0);
4783/// bd-qayid: Count times a cached append hint was cleared conservatively.
4784static FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT: AtomicU64 = AtomicU64::new(0);
4785/// Decoded NULL values.
4786static FSQLITE_VDBE_DECODED_NULLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4787/// Decoded INTEGER values.
4788static FSQLITE_VDBE_DECODED_INTEGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4789/// Decoded REAL values.
4790static FSQLITE_VDBE_DECODED_REALS_TOTAL: AtomicU64 = AtomicU64::new(0);
4791/// Decoded TEXT values.
4792static FSQLITE_VDBE_DECODED_TEXTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4793/// Decoded BLOB values.
4794static FSQLITE_VDBE_DECODED_BLOBS_TOTAL: AtomicU64 = AtomicU64::new(0);
4795/// Heap bytes for decoded TEXT values.
4796static FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4797/// Heap bytes for decoded BLOB values.
4798static FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4799/// Result-row NULL values.
4800static FSQLITE_VDBE_RESULT_NULLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4801/// Result-row INTEGER values.
4802static FSQLITE_VDBE_RESULT_INTEGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4803/// Result-row REAL values.
4804static FSQLITE_VDBE_RESULT_REALS_TOTAL: AtomicU64 = AtomicU64::new(0);
4805/// Result-row TEXT values.
4806static FSQLITE_VDBE_RESULT_TEXTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4807/// Result-row BLOB values.
4808static FSQLITE_VDBE_RESULT_BLOBS_TOTAL: AtomicU64 = AtomicU64::new(0);
4809/// Heap bytes for result-row TEXT values.
4810static FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4811/// Heap bytes for result-row BLOB values.
4812static FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4813/// Whether VDBE execution metrics should be collected on the hot path.
4814///
4815/// These counters are used only for diagnostics/tests today, so leave them
4816/// disabled by default to keep shared-state bookkeeping off ordinary execute().
4817static FSQLITE_VDBE_METRICS_ENABLED: AtomicBool = AtomicBool::new(false);
4818/// Monotonic program ID counter for tracing correlation.
4819static VDBE_PROGRAM_ID_SEQ: AtomicU64 = AtomicU64::new(1);
4820
4821// ── JIT scaffolding metrics/state (bd-1rw.3) ───────────────────────────────
4822
4823/// Total number of JIT compilation attempts that succeeded.
4824static FSQLITE_JIT_COMPILATIONS_TOTAL: AtomicU64 = AtomicU64::new(0);
4825/// Total number of JIT compilation attempts that failed and fell back.
4826static FSQLITE_JIT_COMPILE_FAILURES_TOTAL: AtomicU64 = AtomicU64::new(0);
4827/// Total number of hot-query trigger events.
4828static FSQLITE_JIT_TRIGGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4829/// Total number of JIT code cache hits.
4830static FSQLITE_JIT_CACHE_HITS_TOTAL: AtomicU64 = AtomicU64::new(0);
4831/// Total number of JIT code cache misses.
4832static FSQLITE_JIT_CACHE_MISSES_TOTAL: AtomicU64 = AtomicU64::new(0);
4833
4834/// Global JIT enable flag.
4835///
4836/// The current scaffold only tracks hot-query metadata and always falls back to
4837/// the interpreter, so leaving it on by default adds synchronization and hashing
4838/// overhead on every statement with no execution-speed upside. Keep it opt-in
4839/// until a real compiled fast path exists.
4840static FSQLITE_JIT_ENABLED: std::sync::atomic::AtomicBool =
4841    std::sync::atomic::AtomicBool::new(false);
4842/// Hot-query threshold (`N` executions before JIT trigger).
4843static FSQLITE_JIT_HOT_THRESHOLD: AtomicU64 = AtomicU64::new(8);
4844/// Maximum cached JIT plan stubs.
4845static FSQLITE_JIT_CACHE_CAPACITY: AtomicU64 = AtomicU64::new(128);
4846
4847/// In-memory JIT code-cache entry (scaffold).
4848#[derive(Debug, Clone)]
4849struct JitCacheEntry {
4850    code_size_bytes: u64,
4851    compiled_program: Arc<CompiledProgram>,
4852}
4853
4854#[derive(Debug, Default)]
4855struct JitRuntimeState {
4856    executions_by_plan: HashMap<u64, u64>,
4857    cache: HashMap<u64, JitCacheEntry>,
4858    lru: VecDeque<u64>,
4859    unsupported_plans: HashSet<u64>,
4860    unsupported_lru: VecDeque<u64>,
4861}
4862
4863impl JitRuntimeState {
4864    fn touch_lru(&mut self, plan_hash: u64) {
4865        self.lru.retain(|candidate| *candidate != plan_hash);
4866        self.lru.push_back(plan_hash);
4867    }
4868
4869    fn insert_cache(
4870        &mut self,
4871        plan_hash: u64,
4872        entry: JitCacheEntry,
4873        cache_capacity: usize,
4874    ) -> Option<u64> {
4875        if cache_capacity == 0 {
4876            return None;
4877        }
4878        let mut evicted = None;
4879        if let hashbrown::hash_map::Entry::Occupied(mut occupied) = self.cache.entry(plan_hash) {
4880            occupied.insert(entry);
4881            self.touch_lru(plan_hash);
4882            return None;
4883        }
4884        if self.cache.len() >= cache_capacity
4885            && let Some(oldest) = self.lru.pop_front()
4886        {
4887            self.cache.remove(&oldest);
4888            evicted = Some(oldest);
4889        }
4890        self.cache.insert(plan_hash, entry);
4891        self.touch_lru(plan_hash);
4892        evicted
4893    }
4894
4895    fn apply_capacity(&mut self, cache_capacity: usize) {
4896        if cache_capacity == 0 {
4897            self.cache.clear();
4898            self.lru.clear();
4899            self.unsupported_plans.clear();
4900            self.unsupported_lru.clear();
4901            return;
4902        }
4903        while self.cache.len() > cache_capacity {
4904            if let Some(oldest) = self.lru.pop_front() {
4905                self.cache.remove(&oldest);
4906            } else {
4907                break;
4908            }
4909        }
4910        while self.unsupported_plans.len() > cache_capacity {
4911            if let Some(oldest) = self.unsupported_lru.pop_front() {
4912                self.unsupported_plans.remove(&oldest);
4913            } else {
4914                break;
4915            }
4916        }
4917    }
4918
4919    fn mark_unsupported_plan(&mut self, plan_hash: u64, cache_capacity: usize) {
4920        if cache_capacity == 0 {
4921            return;
4922        }
4923        if !self.unsupported_plans.insert(plan_hash) {
4924            self.unsupported_lru
4925                .retain(|candidate| *candidate != plan_hash);
4926            self.unsupported_lru.push_back(plan_hash);
4927            return;
4928        }
4929        if self.unsupported_plans.len() > cache_capacity
4930            && let Some(oldest) = self.unsupported_lru.pop_front()
4931        {
4932            self.unsupported_plans.remove(&oldest);
4933        }
4934        self.unsupported_lru.push_back(plan_hash);
4935    }
4936
4937    fn is_unsupported_plan(&self, plan_hash: u64) -> bool {
4938        self.unsupported_plans.contains(&plan_hash)
4939    }
4940}
4941
4942static VDBE_JIT_RUNTIME: std::sync::OnceLock<Mutex<JitRuntimeState>> = std::sync::OnceLock::new();
4943
4944fn jit_runtime() -> &'static Mutex<JitRuntimeState> {
4945    VDBE_JIT_RUNTIME.get_or_init(|| Mutex::new(JitRuntimeState::default()))
4946}
4947
4948fn lock_jit_runtime() -> std::sync::MutexGuard<'static, JitRuntimeState> {
4949    jit_runtime()
4950        .lock()
4951        .unwrap_or_else(std::sync::PoisonError::into_inner)
4952}
4953
4954/// Snapshot of JIT scaffold metrics/configuration.
4955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4956pub struct VdbeJitMetricsSnapshot {
4957    /// Whether JIT triggering is enabled.
4958    pub enabled: bool,
4959    /// Hot-query threshold (`N` executions before a JIT trigger).
4960    pub hot_threshold: u64,
4961    /// Maximum number of cached JIT plan stubs.
4962    pub cache_capacity: usize,
4963    /// Current number of cached JIT plan stubs.
4964    pub cache_entries: usize,
4965    /// Total successful compilation attempts.
4966    pub jit_compilations_total: u64,
4967    /// Total failed compilation attempts.
4968    pub jit_compile_failures_total: u64,
4969    /// Total hot-query trigger events.
4970    pub jit_triggers_total: u64,
4971    /// Total cache hits.
4972    pub jit_cache_hits_total: u64,
4973    /// Total cache misses.
4974    pub jit_cache_misses_total: u64,
4975    /// Integer cache hit ratio (percent).
4976    pub jit_cache_hit_ratio_percent: u64,
4977}
4978
4979/// Read a point-in-time snapshot of JIT scaffold metrics/configuration.
4980#[must_use]
4981pub fn vdbe_jit_metrics_snapshot() -> VdbeJitMetricsSnapshot {
4982    let runtime = lock_jit_runtime();
4983    let hits = FSQLITE_JIT_CACHE_HITS_TOTAL.load(AtomicOrdering::Relaxed);
4984    let misses = FSQLITE_JIT_CACHE_MISSES_TOTAL.load(AtomicOrdering::Relaxed);
4985    let denom = hits.saturating_add(misses);
4986    let ratio_percent = if denom == 0 {
4987        0
4988    } else {
4989        hits.saturating_mul(100).saturating_add(denom / 2) / denom
4990    };
4991    VdbeJitMetricsSnapshot {
4992        enabled: FSQLITE_JIT_ENABLED.load(AtomicOrdering::Relaxed),
4993        hot_threshold: FSQLITE_JIT_HOT_THRESHOLD.load(AtomicOrdering::Relaxed),
4994        cache_capacity: usize::try_from(FSQLITE_JIT_CACHE_CAPACITY.load(AtomicOrdering::Relaxed))
4995            .unwrap_or(usize::MAX),
4996        cache_entries: runtime.cache.len(),
4997        jit_compilations_total: FSQLITE_JIT_COMPILATIONS_TOTAL.load(AtomicOrdering::Relaxed),
4998        jit_compile_failures_total: FSQLITE_JIT_COMPILE_FAILURES_TOTAL
4999            .load(AtomicOrdering::Relaxed),
5000        jit_triggers_total: FSQLITE_JIT_TRIGGERS_TOTAL.load(AtomicOrdering::Relaxed),
5001        jit_cache_hits_total: hits,
5002        jit_cache_misses_total: misses,
5003        jit_cache_hit_ratio_percent: ratio_percent,
5004    }
5005}
5006
5007/// Enable/disable JIT triggering.
5008pub fn set_vdbe_jit_enabled(enabled: bool) {
5009    FSQLITE_JIT_ENABLED.store(enabled, AtomicOrdering::Relaxed);
5010}
5011
5012/// Current JIT trigger enable flag.
5013#[must_use]
5014pub fn vdbe_jit_enabled() -> bool {
5015    FSQLITE_JIT_ENABLED.load(AtomicOrdering::Relaxed)
5016}
5017
5018/// Set hot-query threshold (`N` executions before a JIT trigger).
5019///
5020/// Values below 1 are clamped to 1.
5021#[must_use]
5022pub fn set_vdbe_jit_hot_threshold(threshold: u64) -> u64 {
5023    let clamped = threshold.max(1);
5024    FSQLITE_JIT_HOT_THRESHOLD.store(clamped, AtomicOrdering::Relaxed);
5025    clamped
5026}
5027
5028/// Current hot-query threshold.
5029#[must_use]
5030pub fn vdbe_jit_hot_threshold() -> u64 {
5031    FSQLITE_JIT_HOT_THRESHOLD.load(AtomicOrdering::Relaxed)
5032}
5033
5034/// Set JIT code cache capacity (number of plans).
5035///
5036/// Shrinks current cache immediately if needed.
5037#[must_use]
5038pub fn set_vdbe_jit_cache_capacity(capacity: usize) -> usize {
5039    let value_u64 = u64::try_from(capacity).unwrap_or(u64::MAX);
5040    FSQLITE_JIT_CACHE_CAPACITY.store(value_u64, AtomicOrdering::Relaxed);
5041    let mut runtime = lock_jit_runtime();
5042    runtime.apply_capacity(capacity);
5043    capacity
5044}
5045
5046/// Current JIT code-cache capacity.
5047#[must_use]
5048pub fn vdbe_jit_cache_capacity() -> usize {
5049    usize::try_from(FSQLITE_JIT_CACHE_CAPACITY.load(AtomicOrdering::Relaxed)).unwrap_or(usize::MAX)
5050}
5051
5052/// Reset JIT scaffold metrics and in-memory state.
5053pub fn reset_vdbe_jit_metrics() {
5054    FSQLITE_JIT_COMPILATIONS_TOTAL.store(0, AtomicOrdering::Relaxed);
5055    FSQLITE_JIT_COMPILE_FAILURES_TOTAL.store(0, AtomicOrdering::Relaxed);
5056    FSQLITE_JIT_TRIGGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5057    FSQLITE_JIT_CACHE_HITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5058    FSQLITE_JIT_CACHE_MISSES_TOTAL.store(0, AtomicOrdering::Relaxed);
5059    let mut runtime = lock_jit_runtime();
5060    runtime.executions_by_plan.clear();
5061    runtime.cache.clear();
5062    runtime.lru.clear();
5063    runtime.unsupported_plans.clear();
5064    runtime.unsupported_lru.clear();
5065}
5066
5067// ── Sort metrics (bd-1rw.4) ─────────────────────────────────────────────────
5068
5069/// Total rows sorted across all sorter invocations.
5070static FSQLITE_SORT_ROWS_TOTAL: AtomicU64 = AtomicU64::new(0);
5071/// Total pages spilled to disk by sorters.
5072static FSQLITE_SORT_SPILL_PAGES_TOTAL: AtomicU64 = AtomicU64::new(0);
5073/// Total MVCC write-path executions that reused an already-owned page lock.
5074static FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5075/// Total MVCC write-path executions that acquired a page lock on first touch.
5076static FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5077/// Total MVCC write-path executions that crossed the commit-surface/page-one lane.
5078static FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5079/// Total page-lock wait episodes observed on the MVCC path.
5080static FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL: AtomicU64 = AtomicU64::new(0);
5081/// Cumulative nanoseconds spent waiting for page-lock ownership changes.
5082static FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5083/// Total BUSY retries on MVCC write paths after waiting for a page lock.
5084static FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5085/// Total BUSY timeouts on MVCC write paths after exhausting the wait budget.
5086static FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL: AtomicU64 = AtomicU64::new(0);
5087/// Total MVCC write rejections caused by stale snapshots.
5088static FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL: AtomicU64 = AtomicU64::new(0);
5089/// Total conflict-only page-one tracking acquisitions.
5090static FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL: AtomicU64 = AtomicU64::new(0);
5091/// Cumulative nanoseconds spent recording conflict-only page-one tracking.
5092static FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5093/// Total stale synthetic pending-commit-surface clears.
5094static FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL: AtomicU64 = AtomicU64::new(0);
5095/// Cumulative nanoseconds spent clearing stale synthetic pending-commit surface state.
5096static FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5097/// Total borrowed `write_page(&[u8])` normalization calls.
5098static FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
5099/// Total borrowed `write_page(&[u8])` calls that still copied a full page even though input was exact-size.
5100static FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5101/// Total owned `write_page_data(PageData)` normalization calls.
5102static FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
5103/// Total owned `PageData` writes that passed through without resizing/copying.
5104static FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL: AtomicU64 = AtomicU64::new(0);
5105/// Total owned short-page writes that zero-extended the existing buffer in place.
5106static FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL: AtomicU64 = AtomicU64::new(0);
5107/// Total owned `PageData` writes that required allocating a resized page image.
5108static FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5109/// Total payload bytes copied while normalizing page data before writes.
5110static FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
5111/// Total zero-fill bytes synthesized while normalizing short page writes.
5112static FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
5113
5114/// Point-in-time breakdown of materialized value storage classes.
5115#[derive(Debug, Clone, PartialEq, Eq, Default)]
5116pub struct ValueTypeMetricsSnapshot {
5117    /// Total values observed in this lane.
5118    pub total_values: u64,
5119    /// NULL values observed.
5120    pub nulls: u64,
5121    /// INTEGER values observed.
5122    pub integers: u64,
5123    /// REAL values observed.
5124    pub reals: u64,
5125    /// TEXT values observed.
5126    pub texts: u64,
5127    /// BLOB values observed.
5128    pub blobs: u64,
5129    /// Heap bytes carried by TEXT values.
5130    pub text_bytes_total: u64,
5131    /// Heap bytes carried by BLOB values.
5132    pub blob_bytes_total: u64,
5133}
5134
5135/// Point-in-time dynamic opcode execution total.
5136#[derive(Debug, Clone, PartialEq, Eq)]
5137pub struct OpcodeExecutionCount {
5138    /// Stable opcode name.
5139    pub opcode: String,
5140    /// Total dynamic executions observed.
5141    pub total: u64,
5142}
5143
5144/// Snapshot of MVCC write-path counters captured inside the VDBE write helpers.
5145#[derive(Debug, Clone, PartialEq, Eq, Default)]
5146pub struct MvccWritePathMetricsSnapshot {
5147    /// Total tier-0 writes that reused an already-owned page lock.
5148    pub tier0_already_owned_writes_total: u64,
5149    /// Total tier-1 writes that acquired a page lock on first touch.
5150    pub tier1_first_touch_writes_total: u64,
5151    /// Total tier-2 writes that crossed the commit-surface/page-one lane.
5152    pub tier2_commit_surface_writes_total: u64,
5153    /// Total wait episodes on page-lock handoff.
5154    pub page_lock_waits_total: u64,
5155    /// Cumulative nanoseconds spent waiting for page-lock handoff.
5156    pub page_lock_wait_time_ns_total: u64,
5157    /// Total BUSY retries after a completed page-lock wait.
5158    pub write_busy_retries_total: u64,
5159    /// Total BUSY timeouts after exhausting the page-lock wait budget.
5160    pub write_busy_timeouts_total: u64,
5161    /// Total stale-snapshot rejections on MVCC writes.
5162    pub stale_snapshot_rejects_total: u64,
5163    /// Total conflict-only page-one tracking operations.
5164    pub page_one_conflict_tracks_total: u64,
5165    /// Cumulative nanoseconds spent in conflict-only page-one tracking.
5166    pub page_one_conflict_track_time_ns_total: u64,
5167    /// Total stale synthetic pending-surface clears.
5168    pub pending_commit_surface_clears_total: u64,
5169    /// Cumulative nanoseconds spent clearing stale synthetic pending-surface state.
5170    pub pending_commit_surface_clear_time_ns_total: u64,
5171}
5172
5173/// Snapshot of page-data normalization and copy/motion counters on the write path.
5174#[derive(Debug, Clone, PartialEq, Eq, Default)]
5175pub struct PageDataMotionMetricsSnapshot {
5176    /// Total borrowed `write_page(&[u8])` normalization calls.
5177    pub borrowed_write_normalization_calls_total: u64,
5178    /// Total borrowed exact-size writes that still copied a full page image.
5179    pub borrowed_exact_size_copies_total: u64,
5180    /// Total owned `write_page_data(PageData)` normalization calls.
5181    pub owned_write_normalization_calls_total: u64,
5182    /// Total owned writes that passed through without resizing/copying.
5183    pub owned_passthrough_total: u64,
5184    /// Total owned short-page writes that zero-extended the existing buffer.
5185    pub owned_in_place_zero_extends_total: u64,
5186    /// Total owned writes that required allocating and copying into a new page image.
5187    pub owned_resized_copies_total: u64,
5188    /// Total payload bytes copied into normalized page images.
5189    pub normalized_payload_bytes_total: u64,
5190    /// Total zero-fill bytes synthesized while normalizing short writes.
5191    pub normalized_zero_fill_bytes_total: u64,
5192}
5193
5194/// Snapshot of VDBE execution metrics.
5195#[derive(Debug, Clone, PartialEq, Eq)]
5196pub struct VdbeMetricsSnapshot {
5197    /// Total opcodes executed across all statements.
5198    pub opcodes_executed_total: u64,
5199    /// Total statements executed.
5200    pub statements_total: u64,
5201    /// Cumulative statement duration in microseconds.
5202    pub statement_duration_us_total: u64,
5203    /// Total rows sorted across all sorter invocations.
5204    pub sort_rows_total: u64,
5205    /// Total pages spilled to disk by sorters.
5206    pub sort_spill_pages_total: u64,
5207    /// Dynamic opcode execution counts observed while metrics were enabled.
5208    pub opcode_execution_totals: Vec<OpcodeExecutionCount>,
5209    /// Total type-coercion attempts.
5210    pub type_coercions_total: u64,
5211    /// Total type-coercion attempts that changed storage class.
5212    pub type_coercion_changes_total: u64,
5213    /// Total storage cursor column reads.
5214    pub column_reads_total: u64,
5215    /// Total full-record decode calls.
5216    pub record_decode_calls_total: u64,
5217    /// Total decode-cache hits across storage, sorter, and pseudo-row paths.
5218    pub decode_cache_hits_total: u64,
5219    /// Total decode-cache misses across storage, sorter, and pseudo-row paths.
5220    pub decode_cache_misses_total: u64,
5221    /// Total decode-cache invalidations caused by row-position changes.
5222    pub decode_cache_invalidations_position_total: u64,
5223    /// Total decode-cache invalidations caused by write-path mutations.
5224    pub decode_cache_invalidations_write_total: u64,
5225    /// Total decode-cache invalidations caused by pseudo-row image changes.
5226    pub decode_cache_invalidations_pseudo_total: u64,
5227    /// Total values materialized from record/column decode.
5228    pub decoded_values_total: u64,
5229    /// Estimated heap bytes materialized from record/column decode.
5230    pub decoded_value_heap_bytes_total: u64,
5231    /// Total emitted result rows.
5232    pub result_rows_total: u64,
5233    /// Total values materialized in emitted result rows.
5234    pub result_values_total: u64,
5235    /// Estimated heap bytes materialized in emitted result rows.
5236    pub result_value_heap_bytes_total: u64,
5237    /// Cumulative nanoseconds spent materializing emitted result rows.
5238    pub result_row_materialization_time_ns_total: u64,
5239    /// Total MakeRecord calls.
5240    pub make_record_calls_total: u64,
5241    /// Total bytes produced by MakeRecord blobs.
5242    pub make_record_blob_bytes_total: u64,
5243    /// Total INSERT executions that reused the append-eligible no-seek decision.
5244    pub insert_append_count: u64,
5245    /// Total INSERT executions that had to perform an existence seek.
5246    pub insert_seek_count: u64,
5247    /// Total times the cached append hint had to be cleared conservatively.
5248    pub insert_append_hint_clear_count: u64,
5249    /// Storage-class breakdown of decoded values.
5250    pub decoded_value_types: ValueTypeMetricsSnapshot,
5251    /// Storage-class breakdown of emitted result values.
5252    pub result_value_types: ValueTypeMetricsSnapshot,
5253    /// MVCC write-path timing and retry counters captured inside the VDBE layer.
5254    pub mvcc_write_path: MvccWritePathMetricsSnapshot,
5255    /// Page-data normalization/copy counters captured on write entry.
5256    pub page_data_motion: PageDataMotionMetricsSnapshot,
5257}
5258
5259/// Enable/disable VDBE execution metrics collection.
5260pub fn set_vdbe_metrics_enabled(enabled: bool) {
5261    FSQLITE_VDBE_METRICS_ENABLED.store(enabled, AtomicOrdering::Relaxed);
5262}
5263
5264/// Current VDBE metrics collection flag.
5265#[must_use]
5266pub fn vdbe_metrics_enabled() -> bool {
5267    FSQLITE_VDBE_METRICS_ENABLED.load(AtomicOrdering::Relaxed)
5268}
5269
5270/// Read a point-in-time snapshot of VDBE execution metrics.
5271#[must_use]
5272pub fn vdbe_metrics_snapshot() -> VdbeMetricsSnapshot {
5273    let mut opcode_execution_totals: Vec<OpcodeExecutionCount> =
5274        FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS
5275            .iter()
5276            .enumerate()
5277            .skip(1)
5278            .filter_map(|(idx, counter)| {
5279                let total = counter.load(AtomicOrdering::Relaxed);
5280                if total == 0 {
5281                    return None;
5282                }
5283                let raw = u8::try_from(idx).ok()?;
5284                let opcode = Opcode::from_byte(raw)?;
5285                Some(OpcodeExecutionCount {
5286                    opcode: opcode.name().to_owned(),
5287                    total,
5288                })
5289            })
5290            .collect();
5291    opcode_execution_totals.sort_by(|lhs, rhs| {
5292        rhs.total
5293            .cmp(&lhs.total)
5294            .then_with(|| lhs.opcode.cmp(&rhs.opcode))
5295    });
5296    VdbeMetricsSnapshot {
5297        opcodes_executed_total: FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.load(AtomicOrdering::Relaxed),
5298        statements_total: FSQLITE_VDBE_STATEMENTS_TOTAL.load(AtomicOrdering::Relaxed),
5299        statement_duration_us_total: FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
5300            .load(AtomicOrdering::Relaxed),
5301        sort_rows_total: FSQLITE_SORT_ROWS_TOTAL.load(AtomicOrdering::Relaxed),
5302        sort_spill_pages_total: FSQLITE_SORT_SPILL_PAGES_TOTAL.load(AtomicOrdering::Relaxed),
5303        opcode_execution_totals,
5304        type_coercions_total: FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.load(AtomicOrdering::Relaxed),
5305        type_coercion_changes_total: FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL
5306            .load(AtomicOrdering::Relaxed),
5307        column_reads_total: FSQLITE_VDBE_COLUMN_READS_TOTAL.load(AtomicOrdering::Relaxed),
5308        record_decode_calls_total: FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
5309            .load(AtomicOrdering::Relaxed),
5310        decode_cache_hits_total: FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.load(AtomicOrdering::Relaxed),
5311        decode_cache_misses_total: FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL
5312            .load(AtomicOrdering::Relaxed),
5313        decode_cache_invalidations_position_total:
5314            FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL.load(AtomicOrdering::Relaxed),
5315        decode_cache_invalidations_write_total: FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL
5316            .load(AtomicOrdering::Relaxed),
5317        decode_cache_invalidations_pseudo_total:
5318            FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL.load(AtomicOrdering::Relaxed),
5319        decoded_values_total: FSQLITE_VDBE_DECODED_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5320        decoded_value_heap_bytes_total: FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL
5321            .load(AtomicOrdering::Relaxed),
5322        result_rows_total: FSQLITE_VDBE_RESULT_ROWS_TOTAL.load(AtomicOrdering::Relaxed),
5323        result_values_total: FSQLITE_VDBE_RESULT_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5324        result_value_heap_bytes_total: FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL
5325            .load(AtomicOrdering::Relaxed),
5326        result_row_materialization_time_ns_total:
5327            FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.load(AtomicOrdering::Relaxed),
5328        make_record_calls_total: FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.load(AtomicOrdering::Relaxed),
5329        make_record_blob_bytes_total: FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL
5330            .load(AtomicOrdering::Relaxed),
5331        insert_append_count: FSQLITE_VDBE_INSERT_APPEND_COUNT.load(AtomicOrdering::Relaxed),
5332        insert_seek_count: FSQLITE_VDBE_INSERT_SEEK_COUNT.load(AtomicOrdering::Relaxed),
5333        insert_append_hint_clear_count: FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
5334            .load(AtomicOrdering::Relaxed),
5335        decoded_value_types: ValueTypeMetricsSnapshot {
5336            total_values: FSQLITE_VDBE_DECODED_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5337            nulls: FSQLITE_VDBE_DECODED_NULLS_TOTAL.load(AtomicOrdering::Relaxed),
5338            integers: FSQLITE_VDBE_DECODED_INTEGERS_TOTAL.load(AtomicOrdering::Relaxed),
5339            reals: FSQLITE_VDBE_DECODED_REALS_TOTAL.load(AtomicOrdering::Relaxed),
5340            texts: FSQLITE_VDBE_DECODED_TEXTS_TOTAL.load(AtomicOrdering::Relaxed),
5341            blobs: FSQLITE_VDBE_DECODED_BLOBS_TOTAL.load(AtomicOrdering::Relaxed),
5342            text_bytes_total: FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5343            blob_bytes_total: FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5344        },
5345        result_value_types: ValueTypeMetricsSnapshot {
5346            total_values: FSQLITE_VDBE_RESULT_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5347            nulls: FSQLITE_VDBE_RESULT_NULLS_TOTAL.load(AtomicOrdering::Relaxed),
5348            integers: FSQLITE_VDBE_RESULT_INTEGERS_TOTAL.load(AtomicOrdering::Relaxed),
5349            reals: FSQLITE_VDBE_RESULT_REALS_TOTAL.load(AtomicOrdering::Relaxed),
5350            texts: FSQLITE_VDBE_RESULT_TEXTS_TOTAL.load(AtomicOrdering::Relaxed),
5351            blobs: FSQLITE_VDBE_RESULT_BLOBS_TOTAL.load(AtomicOrdering::Relaxed),
5352            text_bytes_total: FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5353            blob_bytes_total: FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5354        },
5355        mvcc_write_path: MvccWritePathMetricsSnapshot {
5356            tier0_already_owned_writes_total: FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL
5357                .load(AtomicOrdering::Relaxed),
5358            tier1_first_touch_writes_total: FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL
5359                .load(AtomicOrdering::Relaxed),
5360            tier2_commit_surface_writes_total: FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL
5361                .load(AtomicOrdering::Relaxed),
5362            page_lock_waits_total: FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL
5363                .load(AtomicOrdering::Relaxed),
5364            page_lock_wait_time_ns_total: FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL
5365                .load(AtomicOrdering::Relaxed),
5366            write_busy_retries_total: FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL
5367                .load(AtomicOrdering::Relaxed),
5368            write_busy_timeouts_total: FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL
5369                .load(AtomicOrdering::Relaxed),
5370            stale_snapshot_rejects_total: FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL
5371                .load(AtomicOrdering::Relaxed),
5372            page_one_conflict_tracks_total: FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL
5373                .load(AtomicOrdering::Relaxed),
5374            page_one_conflict_track_time_ns_total:
5375                FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL
5376                    .load(AtomicOrdering::Relaxed),
5377            pending_commit_surface_clears_total: FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL
5378                .load(AtomicOrdering::Relaxed),
5379            pending_commit_surface_clear_time_ns_total:
5380                FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL.load(AtomicOrdering::Relaxed),
5381        },
5382        page_data_motion: PageDataMotionMetricsSnapshot {
5383            borrowed_write_normalization_calls_total:
5384                FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL
5385                    .load(AtomicOrdering::Relaxed),
5386            borrowed_exact_size_copies_total:
5387                FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL
5388                    .load(AtomicOrdering::Relaxed),
5389            owned_write_normalization_calls_total:
5390                FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL.load(AtomicOrdering::Relaxed),
5391            owned_passthrough_total: FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL
5392                .load(AtomicOrdering::Relaxed),
5393            owned_in_place_zero_extends_total:
5394                FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL
5395                    .load(AtomicOrdering::Relaxed),
5396            owned_resized_copies_total: FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL
5397                .load(AtomicOrdering::Relaxed),
5398            normalized_payload_bytes_total: FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL
5399                .load(AtomicOrdering::Relaxed),
5400            normalized_zero_fill_bytes_total:
5401                FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL
5402                    .load(AtomicOrdering::Relaxed),
5403        },
5404    }
5405}
5406
5407/// Reset VDBE metrics to zero (tests/diagnostics).
5408pub fn reset_vdbe_metrics() {
5409    FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.store(0, AtomicOrdering::Relaxed);
5410    FSQLITE_VDBE_STATEMENTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5411    FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL.store(0, AtomicOrdering::Relaxed);
5412    for counter in FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS.iter() {
5413        counter.store(0, AtomicOrdering::Relaxed);
5414    }
5415    FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.store(0, AtomicOrdering::Relaxed);
5416    FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL.store(0, AtomicOrdering::Relaxed);
5417    FSQLITE_VDBE_COLUMN_READS_TOTAL.store(0, AtomicOrdering::Relaxed);
5418    FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5419    FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5420    FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL.store(0, AtomicOrdering::Relaxed);
5421    FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL.store(0, AtomicOrdering::Relaxed);
5422    FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL.store(0, AtomicOrdering::Relaxed);
5423    FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL.store(0, AtomicOrdering::Relaxed);
5424    FSQLITE_VDBE_DECODED_VALUES_TOTAL.store(0, AtomicOrdering::Relaxed);
5425    FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5426    FSQLITE_VDBE_RESULT_ROWS_TOTAL.store(0, AtomicOrdering::Relaxed);
5427    FSQLITE_VDBE_RESULT_VALUES_TOTAL.store(0, AtomicOrdering::Relaxed);
5428    FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5429    FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5430    FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5431    FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5432    FSQLITE_VDBE_INSERT_APPEND_COUNT.store(0, AtomicOrdering::Relaxed);
5433    FSQLITE_VDBE_INSERT_SEEK_COUNT.store(0, AtomicOrdering::Relaxed);
5434    FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT.store(0, AtomicOrdering::Relaxed);
5435    FSQLITE_VDBE_DECODED_NULLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5436    FSQLITE_VDBE_DECODED_INTEGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5437    FSQLITE_VDBE_DECODED_REALS_TOTAL.store(0, AtomicOrdering::Relaxed);
5438    FSQLITE_VDBE_DECODED_TEXTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5439    FSQLITE_VDBE_DECODED_BLOBS_TOTAL.store(0, AtomicOrdering::Relaxed);
5440    FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5441    FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5442    FSQLITE_VDBE_RESULT_NULLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5443    FSQLITE_VDBE_RESULT_INTEGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5444    FSQLITE_VDBE_RESULT_REALS_TOTAL.store(0, AtomicOrdering::Relaxed);
5445    FSQLITE_VDBE_RESULT_TEXTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5446    FSQLITE_VDBE_RESULT_BLOBS_TOTAL.store(0, AtomicOrdering::Relaxed);
5447    FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5448    FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5449    FSQLITE_SORT_ROWS_TOTAL.store(0, AtomicOrdering::Relaxed);
5450    FSQLITE_SORT_SPILL_PAGES_TOTAL.store(0, AtomicOrdering::Relaxed);
5451    FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5452    FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5453    FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5454    FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5455    FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5456    FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5457    FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5458    FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5459    FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL.store(0, AtomicOrdering::Relaxed);
5460    FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5461    FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL.store(0, AtomicOrdering::Relaxed);
5462    FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5463    FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5464    FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5465    FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5466    FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL.store(0, AtomicOrdering::Relaxed);
5467    FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL.store(0, AtomicOrdering::Relaxed);
5468    FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5469    FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5470    FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5471    reset_vdbe_jit_metrics();
5472}
5473
5474#[cfg(test)]
5475fn reset_vdbe_test_sideband_materialization_count() {
5476    FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(|counter| counter.set(0));
5477}
5478
5479#[cfg(test)]
5480fn vdbe_test_sideband_materialization_count_snapshot() -> u64 {
5481    FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(Cell::get)
5482}
5483
5484fn estimated_value_heap_bytes(value: &SqliteValue) -> u64 {
5485    match value {
5486        SqliteValue::Null => 0,
5487        SqliteValue::Integer(_) | SqliteValue::Float(_) => {
5488            u64::try_from(std::mem::size_of::<SqliteValue>()).unwrap_or(u64::MAX)
5489        }
5490        SqliteValue::Text(text) => {
5491            u64::try_from(std::mem::size_of::<SqliteValue>().saturating_add(text.len()))
5492                .unwrap_or(u64::MAX)
5493        }
5494        SqliteValue::Blob(blob) => {
5495            u64::try_from(std::mem::size_of::<SqliteValue>().saturating_add(blob.len()))
5496                .unwrap_or(u64::MAX)
5497        }
5498    }
5499}
5500
5501struct ValueTypeMetricCounters<'a> {
5502    total: &'a AtomicU64,
5503    nulls: &'a AtomicU64,
5504    integers: &'a AtomicU64,
5505    reals: &'a AtomicU64,
5506    texts: &'a AtomicU64,
5507    blobs: &'a AtomicU64,
5508    text_bytes: &'a AtomicU64,
5509    blob_bytes: &'a AtomicU64,
5510}
5511
5512fn record_value_type_metrics(value: &SqliteValue, counters: &ValueTypeMetricCounters<'_>) {
5513    counters.total.fetch_add(1, AtomicOrdering::Relaxed);
5514    match value {
5515        SqliteValue::Null => {
5516            counters.nulls.fetch_add(1, AtomicOrdering::Relaxed);
5517        }
5518        SqliteValue::Integer(_) => {
5519            counters.integers.fetch_add(1, AtomicOrdering::Relaxed);
5520        }
5521        SqliteValue::Float(_) => {
5522            counters.reals.fetch_add(1, AtomicOrdering::Relaxed);
5523        }
5524        SqliteValue::Text(text) => {
5525            counters.texts.fetch_add(1, AtomicOrdering::Relaxed);
5526            counters.text_bytes.fetch_add(
5527                u64::try_from(text.len()).unwrap_or(u64::MAX),
5528                AtomicOrdering::Relaxed,
5529            );
5530        }
5531        SqliteValue::Blob(blob) => {
5532            counters.blobs.fetch_add(1, AtomicOrdering::Relaxed);
5533            counters.blob_bytes.fetch_add(
5534                u64::try_from(blob.len()).unwrap_or(u64::MAX),
5535                AtomicOrdering::Relaxed,
5536            );
5537        }
5538    }
5539}
5540
5541fn record_decoded_value_metrics(value: &SqliteValue) {
5542    FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL
5543        .fetch_add(estimated_value_heap_bytes(value), AtomicOrdering::Relaxed);
5544    let counters = ValueTypeMetricCounters {
5545        total: &FSQLITE_VDBE_DECODED_VALUES_TOTAL,
5546        nulls: &FSQLITE_VDBE_DECODED_NULLS_TOTAL,
5547        integers: &FSQLITE_VDBE_DECODED_INTEGERS_TOTAL,
5548        reals: &FSQLITE_VDBE_DECODED_REALS_TOTAL,
5549        texts: &FSQLITE_VDBE_DECODED_TEXTS_TOTAL,
5550        blobs: &FSQLITE_VDBE_DECODED_BLOBS_TOTAL,
5551        text_bytes: &FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL,
5552        blob_bytes: &FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL,
5553    };
5554    record_value_type_metrics(value, &counters);
5555}
5556
5557fn note_decode_cache_hit(collect_vdbe_metrics: bool) {
5558    if collect_vdbe_metrics {
5559        FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5560    }
5561}
5562
5563fn note_decode_cache_miss(collect_vdbe_metrics: bool) {
5564    if collect_vdbe_metrics {
5565        FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5566    }
5567}
5568
5569fn note_decode_cache_invalidation(
5570    collect_vdbe_metrics: bool,
5571    reason: DecodeCacheInvalidationReason,
5572) {
5573    if !collect_vdbe_metrics {
5574        return;
5575    }
5576    match reason {
5577        DecodeCacheInvalidationReason::PositionChange => {
5578            FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL
5579                .fetch_add(1, AtomicOrdering::Relaxed);
5580        }
5581        DecodeCacheInvalidationReason::WriteMutation => {
5582            FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL
5583                .fetch_add(1, AtomicOrdering::Relaxed);
5584        }
5585        DecodeCacheInvalidationReason::PseudoRowChange => {
5586            FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL
5587                .fetch_add(1, AtomicOrdering::Relaxed);
5588        }
5589    }
5590}
5591
5592fn record_result_row_metrics(row: &[SqliteValue]) {
5593    FSQLITE_VDBE_RESULT_ROWS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5594    let row_heap_bytes = row.iter().fold(0_u64, |acc, value| {
5595        acc.saturating_add(estimated_value_heap_bytes(value))
5596    });
5597    FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL.fetch_add(row_heap_bytes, AtomicOrdering::Relaxed);
5598    let counters = ValueTypeMetricCounters {
5599        total: &FSQLITE_VDBE_RESULT_VALUES_TOTAL,
5600        nulls: &FSQLITE_VDBE_RESULT_NULLS_TOTAL,
5601        integers: &FSQLITE_VDBE_RESULT_INTEGERS_TOTAL,
5602        reals: &FSQLITE_VDBE_RESULT_REALS_TOTAL,
5603        texts: &FSQLITE_VDBE_RESULT_TEXTS_TOTAL,
5604        blobs: &FSQLITE_VDBE_RESULT_BLOBS_TOTAL,
5605        text_bytes: &FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL,
5606        blob_bytes: &FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL,
5607    };
5608    for value in row {
5609        record_value_type_metrics(value, &counters);
5610    }
5611}
5612
5613/// Record VDBE-style metrics for a result row emitted by a direct fast path
5614/// outside the opcode loop.
5615pub fn record_external_result_row_metrics(row: &[SqliteValue]) {
5616    if !vdbe_metrics_enabled() {
5617        return;
5618    }
5619
5620    let materialize_start = Instant::now();
5621    for value in row {
5622        record_decoded_value_metrics(value);
5623    }
5624    record_result_row_metrics(row);
5625    FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
5626        u64::try_from(materialize_start.elapsed().as_nanos())
5627            .unwrap_or(u64::MAX)
5628            .max(1),
5629        AtomicOrdering::Relaxed,
5630    );
5631}
5632
5633/// Record VDBE-style INSERT fast-path decision metrics for direct execution
5634/// paths outside the opcode loop.
5635pub fn record_external_insert_path_metric(append_fast_path: bool) {
5636    if !vdbe_metrics_enabled() {
5637        return;
5638    }
5639
5640    if append_fast_path {
5641        FSQLITE_VDBE_INSERT_APPEND_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5642    } else {
5643        FSQLITE_VDBE_INSERT_SEEK_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5644    }
5645}
5646
5647/// Record that a direct execution path had to clear a cached INSERT append hint.
5648pub fn record_external_insert_append_hint_clear_metric() {
5649    if !vdbe_metrics_enabled() {
5650        return;
5651    }
5652
5653    FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5654}
5655
5656fn record_type_coercion(before: &SqliteValue, after: &SqliteValue) {
5657    FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5658    if before.storage_class() != after.storage_class() {
5659        FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5660    }
5661}
5662
5663#[derive(Debug, Clone)]
5664enum JitDecision {
5665    Disabled,
5666    Warming {
5667        plan_hash: u64,
5668        execution_count: u64,
5669    },
5670    CacheHit {
5671        plan_hash: u64,
5672        code_size_bytes: u64,
5673        compiled_program: Arc<CompiledProgram>,
5674    },
5675    UnsupportedCached {
5676        plan_hash: u64,
5677    },
5678    Compiled {
5679        plan_hash: u64,
5680        compile_time_us: u64,
5681        code_size_bytes: u64,
5682        evicted_plan_hash: Option<u64>,
5683        compiled_program: Arc<CompiledProgram>,
5684    },
5685    CompileFailed {
5686        plan_hash: u64,
5687        compile_time_us: u64,
5688        reason: &'static str,
5689    },
5690}
5691
5692fn hash_program(program: &VdbeProgram) -> u64 {
5693    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
5694    const FNV_PRIME: u64 = 0x100000001b3;
5695
5696    fn mix(hash: &mut u64, bytes: &[u8]) {
5697        for byte in bytes {
5698            *hash ^= u64::from(*byte);
5699            *hash = hash.wrapping_mul(FNV_PRIME);
5700        }
5701    }
5702
5703    fn mix_len(hash: &mut u64, len: usize) {
5704        mix(hash, &u64::try_from(len).unwrap_or(u64::MAX).to_le_bytes());
5705    }
5706
5707    fn mix_p4(hash: &mut u64, p4: &P4) {
5708        match p4 {
5709            P4::None => mix(hash, &[0]),
5710            P4::Int(value) => {
5711                mix(hash, &[1]);
5712                mix(hash, &value.to_le_bytes());
5713            }
5714            P4::Int64(value) => {
5715                mix(hash, &[2]);
5716                mix(hash, &value.to_le_bytes());
5717            }
5718            P4::Real(value) => {
5719                mix(hash, &[3]);
5720                mix(hash, &value.to_bits().to_le_bytes());
5721            }
5722            P4::Str(value) => {
5723                mix(hash, &[4]);
5724                mix_len(hash, value.len());
5725                mix(hash, value.as_bytes());
5726            }
5727            P4::Blob(value) => {
5728                mix(hash, &[5]);
5729                mix_len(hash, value.len());
5730                mix(hash, value);
5731            }
5732            P4::Collation(value) => {
5733                mix(hash, &[6]);
5734                mix_len(hash, value.len());
5735                mix(hash, value.as_bytes());
5736            }
5737            P4::FuncName(value) => {
5738                mix(hash, &[7]);
5739                mix_len(hash, value.len());
5740                mix(hash, value.as_bytes());
5741            }
5742            P4::FuncNameCollated(value, coll) => {
5743                mix(hash, &[13]); // distinct from FuncName tag [7]
5744                mix_len(hash, value.len());
5745                mix(hash, value.as_bytes());
5746                mix_len(hash, coll.len());
5747                mix(hash, coll.as_bytes());
5748            }
5749            P4::Table(value) => {
5750                mix(hash, &[8]);
5751                mix_len(hash, value.len());
5752                mix(hash, value.as_bytes());
5753            }
5754            P4::Index(value) => {
5755                mix(hash, &[9]);
5756                mix_len(hash, value.len());
5757                mix(hash, value.as_bytes());
5758            }
5759            P4::Affinity(value) => {
5760                mix(hash, &[10]);
5761                mix_len(hash, value.len());
5762                mix(hash, value.as_bytes());
5763            }
5764            P4::TimeTravelCommitSeq(value) => {
5765                mix(hash, &[11]);
5766                mix(hash, &value.to_le_bytes());
5767            }
5768            P4::TimeTravelTimestamp(value) => {
5769                mix(hash, &[12]);
5770                mix_len(hash, value.len());
5771                mix(hash, value.as_bytes());
5772            }
5773            P4::PrecomputedHeader(value) => {
5774                mix(hash, &[14]);
5775                mix_len(hash, value.template.len());
5776                mix(hash, &value.template);
5777                mix_len(hash, value.slots.len());
5778                for slot in &value.slots {
5779                    let kind = match slot.kind {
5780                        PrecomputedSerialTypeKind::NullPlaceholder => 0_u8,
5781                        PrecomputedSerialTypeKind::AnyOneByteVarintOrNull => 1_u8,
5782                        PrecomputedSerialTypeKind::IntegerOrNull => 2_u8,
5783                        PrecomputedSerialTypeKind::RealOrNull => 3_u8,
5784                    };
5785                    mix(hash, &[kind]);
5786                    mix_len(hash, slot.header_offset);
5787                }
5788            }
5789        }
5790    }
5791
5792    let mut hash = FNV_OFFSET;
5793    mix(&mut hash, &program.register_count().to_le_bytes());
5794    for op in program.ops() {
5795        mix(&mut hash, &[op.opcode as u8]);
5796        mix(&mut hash, &op.p1.to_le_bytes());
5797        mix(&mut hash, &op.p2.to_le_bytes());
5798        mix(&mut hash, &op.p3.to_le_bytes());
5799        mix_p4(&mut hash, &op.p4);
5800        mix(&mut hash, &op.p5.to_le_bytes());
5801    }
5802    hash
5803}
5804
5805fn estimate_compiled_program_size(compiled_program: &CompiledProgram) -> u64 {
5806    match compiled_program {
5807        CompiledProgram::ConstantResultRow(template) => {
5808            let value_count = u64::try_from(template.values.len()).unwrap_or(u64::MAX);
5809            value_count.saturating_mul(24).saturating_add(64)
5810        }
5811        CompiledProgram::SimpleInsert(template) => {
5812            let value_count = u64::try_from(template.value_sources.len()).unwrap_or(u64::MAX);
5813            let builder_bytes = match &template.record_builder {
5814                CompiledRecordBuilder::Generic => 0,
5815                CompiledRecordBuilder::PrecomputedHeader(header) => {
5816                    u64::try_from(header.template.len() + header.slots.len() * 8)
5817                        .unwrap_or(u64::MAX)
5818                }
5819            };
5820            value_count
5821                .saturating_mul(32)
5822                .saturating_add(96)
5823                .saturating_add(builder_bytes)
5824        }
5825        CompiledProgram::RowidLookupSelect(template) => {
5826            let col_count = u64::try_from(template.column_indices.len()).unwrap_or(u64::MAX);
5827            col_count.saturating_mul(24).saturating_add(80)
5828        }
5829        CompiledProgram::FullScanSelect(template) => {
5830            let col_count = u64::try_from(template.column_indices.len()).unwrap_or(u64::MAX);
5831            col_count.saturating_mul(24).saturating_add(64)
5832        }
5833    }
5834}
5835
5836fn compile_jit_program(
5837    program: &VdbeProgram,
5838) -> std::result::Result<(CompiledProgram, u64), &'static str> {
5839    let compiled_program =
5840        try_compile_program(program.ops()).ok_or("unsupported opcode in JIT scaffold compiler")?;
5841    let code_size_bytes = estimate_compiled_program_size(&compiled_program);
5842    Ok((compiled_program, code_size_bytes))
5843}
5844
5845fn maybe_trigger_jit(program: &VdbeProgram) -> JitDecision {
5846    if !vdbe_jit_enabled() {
5847        return JitDecision::Disabled;
5848    }
5849    let plan_hash = hash_program(program);
5850    let hot_threshold = vdbe_jit_hot_threshold();
5851    let cache_capacity = vdbe_jit_cache_capacity();
5852
5853    let mut runtime = lock_jit_runtime();
5854    let execution_count = {
5855        let count = runtime.executions_by_plan.entry(plan_hash).or_insert(0);
5856        *count = count.saturating_add(1);
5857        *count
5858    };
5859    if execution_count < hot_threshold {
5860        return JitDecision::Warming {
5861            plan_hash,
5862            execution_count,
5863        };
5864    }
5865
5866    FSQLITE_JIT_TRIGGERS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5867    if runtime.is_unsupported_plan(plan_hash) {
5868        return JitDecision::UnsupportedCached { plan_hash };
5869    }
5870    if let Some((code_size_bytes, compiled_program)) = runtime
5871        .cache
5872        .get(&plan_hash)
5873        .map(|entry| (entry.code_size_bytes, Arc::clone(&entry.compiled_program)))
5874    {
5875        FSQLITE_JIT_CACHE_HITS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5876        runtime.touch_lru(plan_hash);
5877        return JitDecision::CacheHit {
5878            plan_hash,
5879            code_size_bytes,
5880            compiled_program,
5881        };
5882    }
5883
5884    FSQLITE_JIT_CACHE_MISSES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5885    let compile_started = Instant::now();
5886    match compile_jit_program(program) {
5887        Ok((compiled_program, code_size_bytes)) => {
5888            FSQLITE_JIT_COMPILATIONS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5889            let compile_time_us =
5890                u64::try_from(compile_started.elapsed().as_micros()).unwrap_or(u64::MAX);
5891            let compiled_program = Arc::new(compiled_program);
5892            let evicted_plan_hash = runtime.insert_cache(
5893                plan_hash,
5894                JitCacheEntry {
5895                    code_size_bytes,
5896                    compiled_program: Arc::clone(&compiled_program),
5897                },
5898                cache_capacity,
5899            );
5900            JitDecision::Compiled {
5901                plan_hash,
5902                compile_time_us,
5903                code_size_bytes,
5904                evicted_plan_hash,
5905                compiled_program,
5906            }
5907        }
5908        Err(reason) => {
5909            FSQLITE_JIT_COMPILE_FAILURES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5910            let compile_time_us =
5911                u64::try_from(compile_started.elapsed().as_micros()).unwrap_or(u64::MAX);
5912            runtime.mark_unsupported_plan(plan_hash, cache_capacity.max(1));
5913            JitDecision::CompileFailed {
5914                plan_hash,
5915                compile_time_us,
5916                reason,
5917            }
5918        }
5919    }
5920}
5921
5922/// Outcome of a single engine execution.
5923#[derive(Debug, Clone, PartialEq, Eq)]
5924pub enum ExecOutcome {
5925    /// Program halted normally (Halt with p1=0).
5926    Done,
5927    /// Program halted with an error code and message.
5928    Error { code: i32, message: String },
5929}
5930
5931/// Exact logical row implicitly deleted by REPLACE conflict handling.
5932#[derive(Debug, Clone, PartialEq, Eq)]
5933pub struct ReplaceVictim {
5934    pub root_page: i32,
5935    pub values: Vec<SqliteValue>,
5936}
5937
5938/// Inline register storage for the 1-indexed VDBE register file.
5939///
5940/// The engine keeps `SqliteValue` cells directly in a `SmallVec` so the hot
5941/// opcode loop can borrow, materialize, or consume values without going
5942/// through an extra container abstraction.
5943type RegisterFile = smallvec::SmallVec<[SqliteValue; 32]>;
5944
5945/// Bound parameter storage for `?1`, `?2`, ... lookups during execution.
5946type BindingStorage = smallvec::SmallVec<[SqliteValue; 8]>;
5947
5948/// Buffered result-row storage retained by the engine when row collection is on.
5949type ResultRowStorage = Vec<smallvec::SmallVec<[SqliteValue; 16]>>;
5950
5951#[derive(Debug, Default)]
5952struct MakeRecordStatementLookaside {
5953    buf: Vec<u8>,
5954    sideband_reg: i32,
5955}
5956
5957impl MakeRecordStatementLookaside {
5958    fn prepare_for_statement(&mut self, estimated_capacity: usize) {
5959        if estimated_capacity > self.buf.capacity() {
5960            self.buf.reserve(estimated_capacity - self.buf.capacity());
5961        }
5962        self.reset();
5963    }
5964
5965    fn reset(&mut self) {
5966        self.buf.clear();
5967        self.sideband_reg = 0;
5968    }
5969
5970    fn clear_sideband_for_register(&mut self, register: i32) {
5971        if self.sideband_reg == register {
5972            self.reset();
5973        }
5974    }
5975
5976    #[must_use]
5977    fn sideband_is_armed_for(&self, register: i32) -> bool {
5978        self.sideband_reg == register && !self.buf.is_empty()
5979    }
5980
5981    #[must_use]
5982    fn armed_register(&self) -> Option<i32> {
5983        (self.sideband_reg != 0 && !self.buf.is_empty()).then_some(self.sideband_reg)
5984    }
5985
5986    fn disarm(&mut self) {
5987        self.sideband_reg = 0;
5988    }
5989
5990    fn replace_buf(&mut self, buf: Vec<u8>) {
5991        self.buf = buf;
5992    }
5993
5994    fn replace_cleared_buf(&mut self, mut buf: Vec<u8>) {
5995        buf.clear();
5996        self.buf = buf;
5997    }
5998
5999    fn arm_sideband(&mut self, register: i32) {
6000        self.sideband_reg = register;
6001    }
6002
6003    fn take_buf(&mut self) -> Vec<u8> {
6004        std::mem::take(&mut self.buf)
6005    }
6006
6007    #[cfg(test)]
6008    #[must_use]
6009    fn capacity(&self) -> usize {
6010        self.buf.capacity()
6011    }
6012
6013    #[cfg(test)]
6014    #[must_use]
6015    fn is_empty(&self) -> bool {
6016        self.buf.is_empty()
6017    }
6018
6019    #[must_use]
6020    fn as_slice(&self) -> &[u8] {
6021        self.buf.as_slice()
6022    }
6023}
6024
6025enum CompiledRecordWritePlan<'a> {
6026    PrecomputedHeader {
6027        values: &'a [SqliteValue],
6028        header: &'a fsqlite_types::record::PrecomputedRecordHeader,
6029        exact_size: usize,
6030    },
6031    Generic(Box<fsqlite_types::record::PlannedRecordSerialization<'a>>),
6032}
6033
6034impl CompiledRecordWritePlan<'_> {
6035    #[must_use]
6036    fn exact_size(&self) -> usize {
6037        match self {
6038            Self::PrecomputedHeader { exact_size, .. } => *exact_size,
6039            Self::Generic(plan) => plan.exact_size(),
6040        }
6041    }
6042
6043    #[allow(clippy::result_unit_err)]
6044    fn write_into_slice(self, dst: &mut [u8]) -> std::result::Result<(), ()> {
6045        match self {
6046            Self::PrecomputedHeader { values, header, .. } => {
6047                serialize_record_iter_with_precomputed_header_into_slice(values.iter(), header, dst)
6048            }
6049            Self::Generic(plan) => (*plan).write_into_slice(dst),
6050        }
6051    }
6052}
6053
6054fn build_compiled_record_write_plan<'a>(
6055    values: &'a [SqliteValue],
6056    record_builder: &'a CompiledRecordBuilder,
6057) -> CompiledRecordWritePlan<'a> {
6058    if let CompiledRecordBuilder::PrecomputedHeader(header) = record_builder
6059        && let Some(exact_size) =
6060            record_iter_with_precomputed_header_exact_size(values.iter(), header)
6061    {
6062        return CompiledRecordWritePlan::PrecomputedHeader {
6063            values,
6064            header,
6065            exact_size,
6066        };
6067    }
6068
6069    CompiledRecordWritePlan::Generic(Box::new(
6070        fsqlite_types::record::plan_record_iter_serialization(values.iter()),
6071    ))
6072}
6073
6074fn serialize_compiled_record_into_vec(
6075    values: &[SqliteValue],
6076    record_builder: &CompiledRecordBuilder,
6077    buf: &mut Vec<u8>,
6078) {
6079    if let CompiledRecordBuilder::PrecomputedHeader(header) = record_builder
6080        && serialize_record_iter_with_precomputed_header_into(values.iter(), header, buf)
6081    {
6082        return;
6083    }
6084
6085    fsqlite_types::record::serialize_record_iter_into(values.iter(), buf);
6086}
6087
6088/// The VDBE bytecode interpreter.
6089///
6090/// Executes a program produced by the code generator, maintaining a register
6091/// file and collecting result rows. In Phase 4, cursor operations use an
6092/// in-memory table store (`MemDatabase`) rather than the full B-tree stack.
6093#[allow(clippy::struct_excessive_bools)]
6094pub struct VdbeEngine {
6095    /// Register file (1-indexed; index 0 is unused/sentinel).
6096    registers: RegisterFile,
6097    /// Bound SQL parameter values (`?1`, `?2`, ...).
6098    bindings: BindingStorage,
6099    /// Root capability context for execution-owned cursor and virtual-table work.
6100    execution_cx: Cx,
6101    /// Page size for this database (bd-zjisk.2).
6102    page_size: PageSize,
6103    /// Whether opcode-level tracing is enabled, latched when the engine is constructed.
6104    trace_opcodes: bool,
6105    /// Execute-scoped metrics flag latched once per statement.
6106    collect_vdbe_metrics: bool,
6107    /// Result rows accumulated during execution.
6108    results: ResultRowStorage,
6109    /// Open cursors (keyed by cursor number, i.e. p1 of OpenRead/OpenWrite).
6110    /// bd-perf (V1.6): Flat array instead of HashMap — direct O(1) index.
6111    cursors: CursorSlots<MemCursor>,
6112    /// Open sorter cursors keyed by cursor number.
6113    sorters: CursorSlots<SorterCursor>,
6114    /// Open storage-backed cursors keyed by cursor number (read and write).
6115    storage_cursors: CursorSlots<StorageCursor>,
6116    /// Cursors that deleted the current row and should treat the next `Next`
6117    /// as a no-advance "consume successor" step.
6118    pending_next_after_delete: HashSet<i32>,
6119    /// Whether `OpenRead`/`OpenWrite` should route through storage-backed cursors.
6120    storage_cursors_enabled: bool,
6121    /// Whether `OP_Close` should keep storage cursors alive for reusable DML.
6122    ///
6123    /// Normal VDBE execution closes storage cursors immediately. The reusable
6124    /// DML lanes enable this so retained engines carry right-edge append hints
6125    /// across statement executions while `OpenWrite` still revalidates cursor
6126    /// id, root page, and backend kind before reuse.
6127    retain_storage_cursors_on_close: bool,
6128    /// Shared pager transaction for storage cursors (Phase 5, bd-2a3y).
6129    /// When set, `open_storage_cursor` routes through the real pager/WAL
6130    /// stack instead of building transient `MemPageStore` snapshots.
6131    txn_page_io: Option<SharedTxnPageIo>,
6132    /// When true, `open_storage_cursor` will reject the MemPageStore fallback
6133    /// path and return false instead of silently routing through in-memory
6134    /// storage. Used in parity-certification mode (bd-2ttd8.1) to verify all
6135    /// cursor operations flow through the real Pager+BtreeCursor stack.
6136    reject_mem_fallback: bool,
6137    /// In-memory database backing cursor operations (shared with Connection).
6138    db: Option<MemDatabase>,
6139    /// Whether the attached `MemDatabase` contains a fully hydrated row mirror
6140    /// for ordinary storage-backed table reads.
6141    memdb_rows_loaded: bool,
6142    /// Whether storage-cursor `Count` may trust the attached `MemDatabase`
6143    /// row mirror for exact cardinality. VDBE write-through DML can leave the
6144    /// MemDatabase rows stale until the connection reloads from pager.
6145    storage_cursor_memdb_count_shortcuts_safe: bool,
6146    /// Scalar/aggregate/window function registry for Function/PureFunc opcodes.
6147    func_registry: Option<Arc<FunctionRegistry>>,
6148    /// Resolved scalar functions keyed by opcode address.
6149    scalar_function_cache: HashMap<usize, ResolvedScalarFunction>,
6150    /// Resolved aggregate functions keyed by opcode address.
6151    aggregate_function_cache: HashMap<usize, Arc<ErasedAggregateFunction>>,
6152    /// Collation registry for compare, sort, DISTINCT, and grouping semantics.
6153    collation_registry: Arc<Mutex<CollationRegistry>>,
6154    /// Lazily allocated feature state kept off the hot interpreter footprint.
6155    cold_state: Option<Box<ColdVdbeState>>,
6156    /// Schema cookie value provided by the Connection (bd-3mmj).
6157    /// Used by `ReadCookie` (p3=1) and `SetCookie` opcodes, and
6158    /// by `Transaction` for stale-schema detection.
6159    schema_cookie: u32,
6160    /// Result of the last `Opcode::Compare` operation.
6161    last_compare_result: Option<Ordering>,
6162    /// Number of rows modified (inserted, deleted, or updated) during execution.
6163    changes: usize,
6164    /// Exact logical rows implicitly deleted by REPLACE during this execution.
6165    replace_victims: Vec<ReplaceVictim>,
6166    /// Rowid of the last INSERT operation (for `last_insert_rowid()` support).
6167    last_insert_rowid: i64,
6168    /// Whether this execution recorded a real last-insert rowid.
6169    last_insert_rowid_valid: bool,
6170    /// Cursor ID used by the last Insert opcode (for conflict resolution in
6171    /// `IdxInsert`: allows the index handler to undo or replace the table row).
6172    last_insert_cursor_id: Option<i32>,
6173    /// Foreign key constraint violation counter (deferred FK enforcement).
6174    fk_counter: i64,
6175    /// AUTOINCREMENT high-water marks keyed by root page number (bd-31j76).
6176    /// Populated from `sqlite_sequence` by the Connection before execution.
6177    autoincrement_seq_by_root_page: HashMap<i32, i64>,
6178    /// Shared allocator used for implicit rowid assignment in concurrent mode.
6179    concurrent_rowid_allocator: Option<Arc<ConcurrentRowIdAllocator>>,
6180    /// Schema epoch namespace for concurrent rowid reservations.
6181    concurrent_rowid_schema_epoch: SchemaEpoch,
6182    /// INTEGER PRIMARY KEY alias column positions keyed by root page number.
6183    /// Used to decode storage-cursor payload columns for rowid tables.
6184    rowid_alias_col_by_root_page: Arc<HashMap<i32, usize>>,
6185    /// Declared table column counts keyed by root page number.
6186    /// Used to distinguish canonical SQLite payloads from legacy short records.
6187    table_column_count_by_root_page: Arc<HashMap<i32, usize>>,
6188    /// Earliest non-IPK NOT NULL column keyed by root page number.
6189    first_not_null_non_ipk_col_by_root_page: Arc<HashMap<i32, usize>>,
6190    /// Column default values by root page number (for ALTER TABLE ADD COLUMN).
6191    /// When a row has fewer columns than the schema expects, defaults from this
6192    /// map are applied instead of returning NULL.
6193    column_defaults_by_root_page: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
6194    /// Per-index descending flags keyed by index root page number.
6195    index_desc_flags_by_root_page: Arc<HashMap<i32, Vec<bool>>>,
6196    /// Per-index collation sequences keyed by index root page number.
6197    index_collations_by_root_page: Arc<HashMap<i32, Vec<Option<String>>>>,
6198    /// Mapping from cursor_id to root_page for default value lookup.
6199    cursor_root_pages: HashMap<i32, i32>,
6200    /// Virtual table instances keyed by cursor number (for transaction ops).
6201    vtab_instances: SwissIndex<i32, Box<dyn VtabInstance>>,
6202    /// Cursors with time-travel snapshots (SQL:2011 temporal queries).
6203    /// Keyed by cursor ID; the integration layer uses this to resolve
6204    /// historical page versions and enforce read-only semantics.
6205    time_travel_cursors: HashMap<i32, TimeTravelMarker>,
6206    /// MVCC version store for time-travel page resolution.
6207    /// Set by the connection layer before execution when time-travel
6208    /// queries may be present.
6209    version_store: Option<Arc<VersionStore>>,
6210    /// Commit log for time-travel timestamp resolution and commit validation.
6211    time_travel_commit_log: Option<Arc<Mutex<CommitLog>>>,
6212    /// GC horizon for time-travel snapshot validation.
6213    time_travel_gc_horizon: Option<CommitSeq>,
6214    /// Metadata mapping table cursor IDs to their associated index cursors
6215    /// and column indices. Used by `native_replace_row` to clean up secondary
6216    /// index entries during REPLACE conflict resolution.
6217    table_index_meta: Arc<TableIndexMetaMap>,
6218    /// Statement-local lookaside for `MakeRecord` serialization sideband.
6219    make_record_lookaside: MakeRecordStatementLookaside,
6220    /// Whether `OP_ResultRow` should materialize and retain result rows.
6221    /// DML-only execution lanes can disable this to avoid row-buffer work.
6222    collect_result_rows: bool,
6223    /// Optional cap on how many `OP_ResultRow` payloads are retained.
6224    ///
6225    /// Execution still continues to completion; rows past the cap are simply
6226    /// discarded while preserving register-clearing semantics.
6227    max_collected_result_rows: Option<usize>,
6228    /// Whether per-statement execution state is already in a fresh baseline.
6229    ///
6230    /// `reset_for_reuse()` establishes this for cached engines. The common
6231    /// prepared-statement path can then skip a second round of identical clears
6232    /// at the top of `execute()`, while plain repeated `execute()` calls on the
6233    /// same engine still preserve their existing reset semantics.
6234    statement_state_clean: bool,
6235    /// Tracks which cold subsystems were actually touched by the last statement
6236    /// so common-case reuse can skip blanket clears of unused collections.
6237    statement_cold_state: StatementColdState,
6238    /// Root pages of tables modified by storage-cursor DML since last sync.
6239    /// Lazy tracking: instead of syncing MemDB on every INSERT, we mark the
6240    /// table dirty and bulk-sync only when a compatibility-path query needs
6241    /// MemDB data. This eliminates the 0.10 µs/row sync overhead for fast-path
6242    /// storage-cursor-backed workloads.
6243    dirty_root_pages: HashSet<i32>,
6244}
6245
6246type ResultRowCallback<'a> = dyn FnMut(smallvec::SmallVec<[SqliteValue; 16]>) -> Result<()> + 'a;
6247
6248#[derive(Debug, PartialEq, Eq)]
6249pub enum ExactResultRowOutcome {
6250    NoRows,
6251    Row(Box<smallvec::SmallVec<[SqliteValue; 16]>>),
6252    MultipleRows,
6253}
6254
6255/// Time-travel target marker stored on cursors opened with
6256/// `FOR SYSTEM_TIME AS OF ...`.
6257#[derive(Debug, Clone)]
6258pub enum TimeTravelMarker {
6259    /// Pinned to a specific commit sequence number.
6260    CommitSeq(u64),
6261    /// Resolved from an ISO-8601 timestamp string.
6262    Timestamp(String),
6263}
6264
6265/// Type-erased virtual table instance for transaction and lifecycle ops.
6266///
6267/// Because `VirtualTable` has an associated `Cursor` type, we need a
6268/// type-erased wrapper to store heterogeneous vtab instances.
6269#[allow(dead_code)]
6270trait VtabInstance: Send + Sync {
6271    fn begin(&mut self, cx: &Cx) -> Result<()>;
6272    fn commit(&mut self, cx: &Cx) -> Result<()>;
6273    fn rollback(&mut self, cx: &Cx) -> Result<()>;
6274    fn destroy(&mut self, cx: &Cx) -> Result<()>;
6275    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
6276    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
6277    fn vtab_update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
6278}
6279
6280/// A type-erased virtual table cursor.
6281///
6282/// Wraps a concrete `VirtualTableCursor` implementation behind dynamic
6283/// dispatch so the engine can store cursors from different vtab modules
6284/// in the same index.
6285pub trait ErasedVtabCursor: Send {
6286    fn filter(
6287        &mut self,
6288        cx: &Cx,
6289        idx_num: i32,
6290        idx_str: Option<&str>,
6291        args: &[SqliteValue],
6292    ) -> Result<()>;
6293    fn next(&mut self, cx: &Cx) -> Result<()>;
6294    fn eof(&self) -> bool;
6295    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
6296    fn rowid(&self) -> Result<i64>;
6297}
6298
6299/// Blanket implementation of `ErasedVtabCursor` for any concrete cursor type.
6300impl<C: fsqlite_func::vtab::VirtualTableCursor + 'static> ErasedVtabCursor for C {
6301    fn filter(
6302        &mut self,
6303        cx: &Cx,
6304        idx_num: i32,
6305        idx_str: Option<&str>,
6306        args: &[SqliteValue],
6307    ) -> Result<()> {
6308        fsqlite_func::vtab::VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
6309    }
6310    fn next(&mut self, cx: &Cx) -> Result<()> {
6311        fsqlite_func::vtab::VirtualTableCursor::next(self, cx)
6312    }
6313    fn eof(&self) -> bool {
6314        fsqlite_func::vtab::VirtualTableCursor::eof(self)
6315    }
6316    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
6317        fsqlite_func::vtab::VirtualTableCursor::column(self, ctx, col)
6318    }
6319    fn rowid(&self) -> Result<i64> {
6320        fsqlite_func::vtab::VirtualTableCursor::rowid(self)
6321    }
6322}
6323
6324/// Bridge from `fsqlite_func::vtab::ErasedVtabInstance` to engine's `VtabInstance`.
6325struct ErasedVtabBridge(Box<dyn fsqlite_func::vtab::ErasedVtabInstance>);
6326
6327impl VtabInstance for ErasedVtabBridge {
6328    fn begin(&mut self, cx: &Cx) -> Result<()> {
6329        self.0.begin(cx)
6330    }
6331    fn commit(&mut self, cx: &Cx) -> Result<()> {
6332        self.0.commit(cx)
6333    }
6334    fn rollback(&mut self, cx: &Cx) -> Result<()> {
6335        self.0.rollback(cx)
6336    }
6337    fn destroy(&mut self, cx: &Cx) -> Result<()> {
6338        self.0.destroy(cx)
6339    }
6340    fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
6341        self.0.rename(cx, new_name)
6342    }
6343    fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
6344        let func_cursor = self.0.open_cursor()?;
6345        Ok(Box::new(FuncVtabCursorBridge(func_cursor)))
6346    }
6347    fn vtab_update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
6348        self.0.update(cx, args)
6349    }
6350}
6351
6352/// Bridge from `fsqlite_func::vtab::ErasedVtabCursor` to engine's `ErasedVtabCursor`.
6353struct FuncVtabCursorBridge(Box<dyn fsqlite_func::vtab::ErasedVtabCursor>);
6354
6355impl ErasedVtabCursor for FuncVtabCursorBridge {
6356    fn filter(
6357        &mut self,
6358        cx: &Cx,
6359        idx_num: i32,
6360        idx_str: Option<&str>,
6361        args: &[SqliteValue],
6362    ) -> Result<()> {
6363        self.0.erased_filter(cx, idx_num, idx_str, args)
6364    }
6365    fn next(&mut self, cx: &Cx) -> Result<()> {
6366        self.0.erased_next(cx)
6367    }
6368    fn eof(&self) -> bool {
6369        self.0.erased_eof()
6370    }
6371    fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
6372        self.0.erased_column(ctx, col)
6373    }
6374    fn rowid(&self) -> Result<i64> {
6375        self.0.erased_rowid()
6376    }
6377}
6378
6379/// State for an open virtual table cursor in the VDBE engine.
6380struct VtabCursorState {
6381    /// The type-erased cursor.
6382    cursor: Box<dyn ErasedVtabCursor>,
6383    /// Synthetic NULL-row state forced by `OP_NullRow`.
6384    null_row: bool,
6385}
6386
6387/// A set of rowids for RowSetAdd/RowSetRead/RowSetTest opcodes.
6388///
6389/// Used by OR-optimized queries and IN subquery evaluation.
6390/// SQLite implements this as a sorted unique set of i64 rowids.
6391struct RowSet {
6392    /// Sorted, deduplicated set of rowids.
6393    entries: Vec<i64>,
6394    /// Current read position for `RowSetRead`.
6395    read_pos: usize,
6396}
6397
6398impl RowSet {
6399    fn new() -> Self {
6400        Self {
6401            entries: Vec::new(),
6402            read_pos: 0,
6403        }
6404    }
6405
6406    /// Add a rowid to the set (maintains sorted order, deduplicates).
6407    fn add(&mut self, rowid: i64) {
6408        match self.entries.binary_search(&rowid) {
6409            Ok(_) => {} // Already present
6410            Err(pos) => self.entries.insert(pos, rowid),
6411        }
6412    }
6413
6414    /// Read the next rowid. Returns `None` when exhausted.
6415    fn read_next(&mut self) -> Option<i64> {
6416        if self.read_pos < self.entries.len() {
6417            let val = self.entries[self.read_pos];
6418            self.read_pos += 1;
6419            Some(val)
6420        } else {
6421            None
6422        }
6423    }
6424
6425    /// Test if a rowid exists in the set.
6426    fn contains(&self, rowid: i64) -> bool {
6427        self.entries.binary_search(&rowid).is_ok()
6428    }
6429}
6430
6431/// Stack-backed byte buffer for distinct keys (avoids heap allocation for small keys).
6432type DistinctKeyBuf = smallvec::SmallVec<[u8; 64]>;
6433
6434struct AggregateContext {
6435    func: Arc<ErasedAggregateFunction>,
6436    state: Box<dyn Any + Send>,
6437    /// When DISTINCT is active, tracks seen argument byte-keys to skip duplicates.
6438    distinct_seen: Option<std::collections::HashSet<DistinctKeyBuf>>,
6439}
6440
6441struct AggStepCall<'a> {
6442    accum_reg: i32,
6443    is_distinct: bool,
6444    func: &'a Arc<ErasedAggregateFunction>,
6445    func_name: &'a str,
6446    agg_collation: Option<&'a str>,
6447    execution_cx: &'a Cx,
6448    args: &'a [SqliteValue],
6449}
6450
6451/// Original-row state captured for UPDATE's delete+insert rewrite so the old
6452/// row can be restored if the replacement later hits a conflict.
6453#[derive(Debug, Clone)]
6454enum PendingUpdateRestore {
6455    Storage {
6456        cursor_id: i32,
6457        rowid: i64,
6458        payload: Vec<u8>,
6459    },
6460    Mem {
6461        root_page: i32,
6462        rowid: i64,
6463        values: MemRowValues,
6464    },
6465}
6466
6467#[derive(Debug, Clone)]
6468struct PendingInsertRollback {
6469    cursor_id: i32,
6470    rowid: i64,
6471    previous_last_insert_rowid: i64,
6472    previous_last_insert_rowid_valid: bool,
6473    update_restore: Option<PendingUpdateRestore>,
6474}
6475
6476/// Per-accumulator window function context (for `AggInverse` / `AggValue`).
6477///
6478/// Bead bd-bldc5.4.1 (T20): Window functions are not yet emitted by VDBE
6479/// codegen. `AggInverse`/`AggValue` opcodes exist and dispatch correctly
6480/// but are never generated — window functions route through the connection
6481/// fallback path (which works correctly, per 100+ conformance tests).
6482///
6483/// To enable VDBE window codegen:
6484/// 1. Emit `SorterOpen` for PARTITION BY + ORDER BY sort in codegen.rs
6485/// 2. Emit `AggStep`/`AggValue` per window function per row
6486/// 3. For running aggregates (frame specs), emit `AggInverse` for shrinkage
6487/// 4. Handle multiple window specs per query (Fix #85 established the
6488///    runtime path for per-function sorted indices)
6489///
6490/// The split between `self.aggregates` and `self.window_contexts` must be
6491/// reconciled — a function used as both aggregate and window needs shared
6492/// state, not duplicated across the two maps.
6493struct WindowContext {
6494    func: Arc<ErasedWindowFunction>,
6495    state: Box<dyn Any + Send>,
6496}
6497
6498struct ColdVdbeState {
6499    /// Aggregate accumulators keyed by accumulator register.
6500    aggregates: SwissIndex<i32, AggregateContext>,
6501    /// Deleted-row state for UPDATE's delete+insert rewrite. When the
6502    /// replacement row later conflicts, we must restore the original row.
6503    pending_update_restore: Option<PendingUpdateRestore>,
6504    /// Provisional table insert metadata kept until later `IdxInsert`
6505    /// opcodes either succeed or roll the row back after a secondary-index
6506    /// conflict.
6507    pending_insert_rollback: Option<PendingInsertRollback>,
6508    /// When true, a UNIQUE conflict with IGNORE was detected during an
6509    /// `IdxInsert`, so remaining `IdxInsert` opcodes for this row should be
6510    /// skipped.
6511    conflict_skip_idx: bool,
6512    /// Index entries inserted for the current row (cursor_id, key_bytes).
6513    /// On secondary-index conflict rollback, these entries must be deleted to
6514    /// avoid phantom index entries blocking future inserts.
6515    pending_idx_entries: Vec<(i32, Vec<u8>)>,
6516    /// RowSet data structures for OR-optimized queries (keyed by register).
6517    rowsets: SwissIndex<i32, RowSet>,
6518    /// Per-cursor monotonic sequence counters for `Opcode::Sequence`.
6519    sequence_counters: HashMap<i32, i64>,
6520    /// Open virtual table cursors keyed by cursor number.
6521    vtab_cursors: SwissIndex<i32, VtabCursorState>,
6522    /// Window function accumulators keyed by accumulator register.
6523    window_contexts: SwissIndex<i32, WindowContext>,
6524    /// Register subtype tags (register index → subtype value).
6525    /// Used by JSON functions to distinguish JSON text (subtype 74/'J')
6526    /// from regular text. Cleared on each register write.
6527    register_subtypes: HashMap<i32, u32>,
6528    /// Fast check to avoid a hash probe on every register write.
6529    has_subtypes: bool,
6530    /// Bloom filters keyed by cursor/filter register.
6531    /// Each entry is a fixed-size bit array used for early rejection
6532    /// during index lookups.
6533    bloom_filters: HashMap<i32, Vec<u64>>,
6534}
6535
6536impl ColdVdbeState {
6537    fn new() -> Self {
6538        Self {
6539            aggregates: SwissIndex::new(),
6540            pending_update_restore: None,
6541            pending_insert_rollback: None,
6542            conflict_skip_idx: false,
6543            pending_idx_entries: Vec::new(),
6544            rowsets: SwissIndex::new(),
6545            sequence_counters: HashMap::new(),
6546            vtab_cursors: SwissIndex::new(),
6547            window_contexts: SwissIndex::new(),
6548            register_subtypes: HashMap::new(),
6549            has_subtypes: false,
6550            bloom_filters: HashMap::new(),
6551        }
6552    }
6553}
6554
6555/// Encode aggregate arguments into a canonical byte key for DISTINCT deduplication,
6556/// applying collation when specified.
6557///
6558/// Built-in text collations normalize into the same key space that the collation
6559/// compares over:
6560/// - `NOCASE` lowercases ASCII text so `'Alice'` and `'alice'` deduplicate
6561/// - `RTRIM` strips trailing ASCII spaces so `'abc'` and `'abc  '` deduplicate
6562fn distinct_key_collated(args: &[SqliteValue], collation: Option<&str>) -> DistinctKeyBuf {
6563    let is_nocase = collation.is_some_and(|c| c.eq_ignore_ascii_case("NOCASE"));
6564    let is_rtrim = collation.is_some_and(|c| c.eq_ignore_ascii_case("RTRIM"));
6565    let mut key = DistinctKeyBuf::new();
6566    for val in args {
6567        match val {
6568            SqliteValue::Null => key.push(0),
6569            SqliteValue::Integer(i) => {
6570                key.push(1);
6571                key.extend_from_slice(&i.to_le_bytes());
6572            }
6573            SqliteValue::Float(f) => {
6574                if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(f) {
6575                    #[allow(clippy::cast_possible_truncation)]
6576                    let i = *f as i64;
6577                    #[allow(clippy::cast_precision_loss)]
6578                    if (i as f64) == *f {
6579                        key.push(1);
6580                        key.extend_from_slice(&i.to_le_bytes());
6581                        continue;
6582                    }
6583                }
6584                key.push(2);
6585                key.extend_from_slice(&f.to_bits().to_le_bytes());
6586            }
6587            SqliteValue::Text(s) => {
6588                key.push(3);
6589                let text_bytes = s.as_bytes_direct();
6590                if is_nocase {
6591                    #[allow(clippy::cast_possible_truncation)]
6592                    key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
6593                    key.extend(text_bytes.iter().map(u8::to_ascii_lowercase));
6594                } else if is_rtrim {
6595                    let trimmed = trim_rtrim_collation_text(text_bytes);
6596                    #[allow(clippy::cast_possible_truncation)]
6597                    key.extend_from_slice(&(trimmed.len() as u64).to_le_bytes());
6598                    key.extend_from_slice(trimmed);
6599                } else {
6600                    #[allow(clippy::cast_possible_truncation)]
6601                    key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
6602                    key.extend_from_slice(text_bytes);
6603                }
6604            }
6605            SqliteValue::Blob(b) => {
6606                key.push(4);
6607                #[allow(clippy::cast_possible_truncation)]
6608                key.extend_from_slice(&(b.len() as u64).to_le_bytes());
6609                key.extend_from_slice(b);
6610            }
6611        }
6612    }
6613    key
6614}
6615
6616fn trim_rtrim_collation_text(text: &[u8]) -> &[u8] {
6617    let mut end = text.len();
6618    while end > 0 && text[end - 1] == b' ' {
6619        end -= 1;
6620    }
6621    &text[..end]
6622}
6623
6624fn sqlite_substr_prefix_value(value: &SqliteValue, prefix_len: usize) -> SqliteValue {
6625    match value {
6626        SqliteValue::Null => SqliteValue::Null,
6627        SqliteValue::Blob(bytes) => {
6628            let end = prefix_len.min(bytes.len());
6629            SqliteValue::Blob(Arc::from(&bytes[..end]))
6630        }
6631        SqliteValue::Text(text) => {
6632            sqlite_substr_prefix_text(Cow::Borrowed(text.as_str()), prefix_len)
6633        }
6634        SqliteValue::Integer(_) | SqliteValue::Float(_) => {
6635            sqlite_substr_prefix_text(Cow::Owned(value.to_text()), prefix_len)
6636        }
6637    }
6638}
6639
6640fn sqlite_octet_length_value(value: &SqliteValue) -> SqliteValue {
6641    if value.is_null() {
6642        return SqliteValue::Null;
6643    }
6644    let len = match value {
6645        SqliteValue::Text(text) => text.len(),
6646        SqliteValue::Blob(bytes) => bytes.len(),
6647        SqliteValue::Integer(_) | SqliteValue::Float(_) => value.to_text().len(),
6648        SqliteValue::Null => unreachable!("NULL returned above"),
6649    };
6650    SqliteValue::Integer(i64::try_from(len).unwrap_or(i64::MAX))
6651}
6652
6653fn sqlite_substr_prefix_text(text: Cow<'_, str>, prefix_len: usize) -> SqliteValue {
6654    let text = text.as_ref();
6655    let end = if text.is_ascii() {
6656        prefix_len.min(text.len())
6657    } else if prefix_len == 0 {
6658        0
6659    } else {
6660        text.char_indices()
6661            .nth(prefix_len)
6662            .map_or(text.len(), |(idx, _)| idx)
6663    };
6664    SqliteValue::Text(SmallText::new(&text[..end]))
6665}
6666
6667impl VdbeEngine {
6668    /// Create a new engine with enough registers for the given program.
6669    ///
6670    /// Test-only detached convenience constructor. Production execution paths
6671    /// should prefer [`Self::new_with_execution_cx`] so capability lineage is
6672    /// preserved end-to-end.
6673    #[cfg(test)]
6674    #[must_use]
6675    pub fn new(register_count: i32) -> Self {
6676        let detached_execution_cx = Cx::new();
6677        Self::new_with_execution_cx(register_count, &detached_execution_cx, PageSize::DEFAULT)
6678    }
6679
6680    /// Create a new engine rooted in the caller's execution context.
6681    #[must_use]
6682    #[allow(clippy::cast_sign_loss)]
6683    pub fn new_with_execution_cx(
6684        register_count: i32,
6685        execution_cx: &Cx,
6686        page_size: PageSize,
6687    ) -> Self {
6688        // +1 because registers are 1-indexed (register 0 unused).
6689        let count = register_count.max(0) as u32 + 1;
6690        Self {
6691            registers: smallvec::smallvec![SqliteValue::Null; count as usize],
6692            bindings: smallvec::SmallVec::new(),
6693            // The caller already supplies a per-execution context; cloning it
6694            // keeps cancellation/tracing lineage while avoiding another child
6695            // allocation on every statement execution.
6696            execution_cx: execution_cx.clone(),
6697            page_size,
6698            trace_opcodes: opcode_trace_enabled(),
6699            collect_vdbe_metrics: false,
6700            results: Vec::with_capacity(64),
6701            cursors: CursorSlots::new(),
6702            sorters: CursorSlots::new(),
6703            storage_cursors: CursorSlots::new(),
6704            pending_next_after_delete: HashSet::new(),
6705            storage_cursors_enabled: true,
6706            retain_storage_cursors_on_close: false,
6707            txn_page_io: None,
6708            // bd-zjisk.1: Default to parity-cert mode — reject MemPageStore fallback.
6709            reject_mem_fallback: true,
6710            db: None,
6711            memdb_rows_loaded: false,
6712            storage_cursor_memdb_count_shortcuts_safe: false,
6713            func_registry: None,
6714            scalar_function_cache: HashMap::new(),
6715            aggregate_function_cache: HashMap::new(),
6716            collation_registry: Arc::new(Mutex::new(CollationRegistry::new())),
6717            cold_state: None,
6718            schema_cookie: 0,
6719            last_compare_result: None,
6720            changes: 0,
6721            replace_victims: Vec::new(),
6722            last_insert_rowid: 0,
6723            last_insert_rowid_valid: false,
6724            last_insert_cursor_id: None,
6725            fk_counter: 0,
6726            autoincrement_seq_by_root_page: HashMap::new(),
6727            concurrent_rowid_allocator: None,
6728            concurrent_rowid_schema_epoch: SchemaEpoch::ZERO,
6729            rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
6730            table_column_count_by_root_page: Arc::new(HashMap::new()),
6731            first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
6732            column_defaults_by_root_page: Arc::new(HashMap::new()),
6733            index_desc_flags_by_root_page: Arc::new(HashMap::new()),
6734            index_collations_by_root_page: Arc::new(HashMap::new()),
6735            cursor_root_pages: HashMap::new(),
6736            vtab_instances: SwissIndex::new(),
6737            time_travel_cursors: HashMap::new(),
6738            version_store: None,
6739            time_travel_commit_log: None,
6740            time_travel_gc_horizon: None,
6741            table_index_meta: Arc::new(HashMap::new()),
6742            make_record_lookaside: MakeRecordStatementLookaside::default(),
6743            collect_result_rows: true,
6744            max_collected_result_rows: None,
6745            statement_state_clean: true,
6746            statement_cold_state: StatementColdState::empty(),
6747            dirty_root_pages: HashSet::new(),
6748        }
6749    }
6750
6751    #[inline]
6752    fn mark_statement_cold_state(&mut self, state: StatementColdState) {
6753        self.statement_cold_state.insert(state);
6754    }
6755
6756    #[inline]
6757    fn cold_state(&self) -> Option<&ColdVdbeState> {
6758        self.cold_state.as_deref()
6759    }
6760
6761    #[inline]
6762    fn cold_state_mut(&mut self) -> Option<&mut ColdVdbeState> {
6763        self.cold_state.as_deref_mut()
6764    }
6765
6766    #[inline]
6767    fn ensure_cold_state(&mut self) -> &mut ColdVdbeState {
6768        self.cold_state
6769            .get_or_insert_with(|| Box::new(ColdVdbeState::new()))
6770            .as_mut()
6771    }
6772
6773    #[inline]
6774    fn ensure_cold_state_for(&mut self, state: StatementColdState) -> &mut ColdVdbeState {
6775        self.mark_statement_cold_state(state);
6776        self.ensure_cold_state()
6777    }
6778
6779    #[cfg(test)]
6780    #[inline]
6781    fn has_cold_state_allocated(&self) -> bool {
6782        self.cold_state.is_some()
6783    }
6784
6785    #[inline]
6786    fn clear_register_subtype(&mut self, r: i32) {
6787        if let Some(cold_state) = self.cold_state_mut()
6788            && cold_state.has_subtypes
6789        {
6790            cold_state.register_subtypes.remove(&r);
6791            if cold_state.register_subtypes.is_empty() {
6792                cold_state.has_subtypes = false;
6793            }
6794        }
6795    }
6796
6797    #[inline]
6798    fn register_subtype(&self, r: i32) -> Option<u32> {
6799        self.cold_state()
6800            .and_then(|cold_state| cold_state.register_subtypes.get(&r).copied())
6801    }
6802
6803    #[inline]
6804    fn set_register_subtype(&mut self, r: i32, subtype: u32) {
6805        if subtype == 0 {
6806            self.clear_register_subtype(r);
6807            return;
6808        }
6809        let cold_state = self.ensure_cold_state_for(StatementColdState::REGISTER_SUBTYPES);
6810        cold_state.register_subtypes.insert(r, subtype);
6811        cold_state.has_subtypes = true;
6812    }
6813
6814    #[inline]
6815    fn take_pending_update_restore(&mut self) -> Option<PendingUpdateRestore> {
6816        self.cold_state_mut()
6817            .and_then(|cold_state| cold_state.pending_update_restore.take())
6818    }
6819
6820    #[inline]
6821    fn set_pending_update_restore(&mut self, restore: Option<PendingUpdateRestore>) {
6822        if let Some(restore) = restore {
6823            self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6824                .pending_update_restore = Some(restore);
6825        } else if let Some(cold_state) = self.cold_state_mut() {
6826            cold_state.pending_update_restore = None;
6827        }
6828    }
6829
6830    #[inline]
6831    fn take_pending_insert_rollback(&mut self) -> Option<PendingInsertRollback> {
6832        self.cold_state_mut()
6833            .and_then(|cold_state| cold_state.pending_insert_rollback.take())
6834    }
6835
6836    #[inline]
6837    fn set_pending_insert_rollback(&mut self, rollback: Option<PendingInsertRollback>) {
6838        if let Some(rollback) = rollback {
6839            self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6840                .pending_insert_rollback = Some(rollback);
6841        } else if let Some(cold_state) = self.cold_state_mut() {
6842            cold_state.pending_insert_rollback = None;
6843        }
6844    }
6845
6846    #[inline]
6847    fn take_pending_idx_entries(&mut self) -> Vec<(i32, Vec<u8>)> {
6848        self.cold_state_mut().map_or_else(Vec::new, |cold_state| {
6849            std::mem::take(&mut cold_state.pending_idx_entries)
6850        })
6851    }
6852
6853    #[inline]
6854    fn clear_pending_idx_entries(&mut self) {
6855        if let Some(cold_state) = self.cold_state_mut() {
6856            cold_state.pending_idx_entries.clear();
6857        }
6858    }
6859
6860    #[inline]
6861    fn push_pending_idx_entry(&mut self, cursor_id: i32, key_bytes: Vec<u8>) {
6862        self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6863            .pending_idx_entries
6864            .push((cursor_id, key_bytes));
6865    }
6866
6867    #[inline]
6868    fn conflict_skip_idx(&self) -> bool {
6869        self.cold_state()
6870            .is_some_and(|cold_state| cold_state.conflict_skip_idx)
6871    }
6872
6873    #[inline]
6874    fn set_conflict_skip_idx(&mut self, skip: bool) {
6875        if skip {
6876            self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6877                .conflict_skip_idx = true;
6878        } else if let Some(cold_state) = self.cold_state_mut() {
6879            cold_state.conflict_skip_idx = false;
6880        }
6881    }
6882
6883    fn clear_statement_cold_state(&mut self) {
6884        if self.statement_cold_state.is_empty() && self.cold_state.is_none() {
6885            return;
6886        }
6887        self.cold_state = None;
6888        self.statement_cold_state.clear();
6889    }
6890
6891    fn reset_for_reuse_impl(
6892        &mut self,
6893        register_count: i32,
6894        execution_cx: &Cx,
6895        page_size: PageSize,
6896        retain_cursors: bool,
6897        preserve_runtime_setup: bool,
6898    ) {
6899        let count = register_count.max(0) as u32 + 1;
6900        // Clear + resize registers to reuse the SmallVec's inline/heap buffer.
6901        self.registers.clear();
6902        #[allow(clippy::cast_possible_truncation)]
6903        self.registers.resize(count as usize, SqliteValue::Null);
6904        self.bindings.clear();
6905        self.execution_cx = execution_cx.clone();
6906        self.page_size = page_size;
6907        // Opcode tracing is engine configuration; reuse preserves the construction-time setting.
6908        self.collect_vdbe_metrics = false;
6909        self.results.clear();
6910        if retain_cursors {
6911            // Keep cursors + cursor_root_pages + storage_cursors alive.
6912            // OP_OpenWrite will detect an existing cursor on the same root
6913            // page and reuse it instead of creating a new one.
6914            // Clear only transient per-statement cursor state.
6915            self.clear_retained_storage_cursor_statement_state();
6916            self.sorters.clear();
6917            self.pending_next_after_delete.clear();
6918        } else {
6919            self.cursors.clear();
6920            self.sorters.clear();
6921            self.storage_cursors.clear();
6922            self.pending_next_after_delete.clear();
6923            self.storage_cursors_enabled = true;
6924            self.txn_page_io = None;
6925        }
6926        self.retain_storage_cursors_on_close = retain_cursors;
6927        if !retain_cursors {
6928            self.db = None;
6929            self.cursor_root_pages.clear();
6930            self.vtab_instances.clear();
6931            self.time_travel_cursors.clear();
6932            self.time_travel_commit_log = None;
6933            self.time_travel_gc_horizon = None;
6934        }
6935        if !preserve_runtime_setup {
6936            self.func_registry = None;
6937        }
6938        // Function caches are keyed by program counter (PC), so they are
6939        // only valid for a single program. Always clear them on reset
6940        // because the next execution may load a different program with
6941        // different functions at the same PCs. (The old code skipped this
6942        // when preserving runtime setup, but that caused SUM/AVG/TOTAL
6943        // cross-contamination when the cached engine ran different programs.)
6944        self.scalar_function_cache.clear();
6945        self.aggregate_function_cache.clear();
6946        // Keep the existing collation_registry Arc — don't allocate a new one.
6947        self.clear_statement_cold_state();
6948        if !preserve_runtime_setup {
6949            self.schema_cookie = 0;
6950        }
6951        self.last_compare_result = None;
6952        self.changes = 0;
6953        self.replace_victims.clear();
6954        self.last_insert_rowid = 0;
6955        self.last_insert_rowid_valid = false;
6956        self.last_insert_cursor_id = None;
6957        self.fk_counter = 0;
6958        if !preserve_runtime_setup {
6959            self.autoincrement_seq_by_root_page.clear();
6960        }
6961        if !retain_cursors {
6962            self.concurrent_rowid_allocator = None;
6963            self.concurrent_rowid_schema_epoch = SchemaEpoch::ZERO;
6964        }
6965        // table_index_meta: kept as-is — execute() overwrites it from the
6966        // program at the start of each run (line ~4903).
6967        self.make_record_lookaside.reset();
6968        if !preserve_runtime_setup {
6969            self.reject_mem_fallback = true;
6970            self.memdb_rows_loaded = false;
6971            self.storage_cursor_memdb_count_shortcuts_safe = false;
6972            self.version_store = None;
6973            self.collect_result_rows = true;
6974            self.max_collected_result_rows = None;
6975        }
6976        self.statement_state_clean = true;
6977    }
6978
6979    /// Reset the engine for reuse, clearing per-statement state but keeping
6980    /// allocated backing memory so subsequent executions avoid 21+ collection
6981    /// re-allocations.
6982    ///
6983    /// After `reset()` the engine is equivalent to a freshly constructed one
6984    /// with the same `register_count`, apart from construction-time engine
6985    /// configuration such as opcode tracing — but all
6986    /// `Vec`/`HashMap`/`SmallVec` retain their heap capacity.
6987    /// Reset engine state for reuse from the cached-engine pool.
6988    ///
6989    /// When `retain_cursors` is true, storage cursors and their root-page
6990    /// mapping are kept alive so that repeated DML on the same table can
6991    /// skip `OP_OpenWrite` cursor creation and `OP_Last` seek on subsequent
6992    /// rows.  The caller is responsible for ensuring the transaction handle
6993    /// (`txn_page_io`) stays valid across calls when cursors are retained.
6994    pub fn reset_for_reuse_ex(
6995        &mut self,
6996        register_count: i32,
6997        execution_cx: &Cx,
6998        page_size: PageSize,
6999        retain_cursors: bool,
7000    ) {
7001        self.reset_for_reuse_impl(
7002            register_count,
7003            execution_cx,
7004            page_size,
7005            retain_cursors,
7006            false,
7007        );
7008    }
7009
7010    /// Reset the engine while retaining runtime bindings that a caller may
7011    /// immediately revalidate and reuse on the next execution.
7012    ///
7013    /// This is used by prepared-statement reusable lanes so hot loops can skip
7014    /// rebinding identical function/schema/default metadata on every row.
7015    pub fn reset_for_reuse_preserving_runtime_setup(
7016        &mut self,
7017        register_count: i32,
7018        execution_cx: &Cx,
7019        page_size: PageSize,
7020        retain_cursors: bool,
7021    ) {
7022        self.reset_for_reuse_impl(
7023            register_count,
7024            execution_cx,
7025            page_size,
7026            retain_cursors,
7027            true,
7028        );
7029    }
7030
7031    /// Convenience wrapper: reset without retaining cursors (legacy behavior).
7032    pub fn reset_for_reuse(&mut self, register_count: i32, execution_cx: &Cx, page_size: PageSize) {
7033        self.reset_for_reuse_ex(register_count, execution_cx, page_size, false);
7034    }
7035
7036    /// Control whether `OP_Close` preserves storage cursors for reusable DML.
7037    pub fn set_retain_storage_cursors_on_close(&mut self, retain: bool) {
7038        self.retain_storage_cursors_on_close = retain;
7039    }
7040
7041    fn clear_storage_cursor_statement_state(sc: &mut StorageCursor) {
7042        // `last_alloc_rowid` exists only to keep multiple OP_NewRowid calls in a
7043        // single statement unique before any corresponding insert lands. If a
7044        // retained cursor carries it across statements, conflict-only INSERT /
7045        // UPSERT statements incorrectly burn normal rowids.
7046        sc.last_alloc_rowid = 0;
7047    }
7048
7049    fn clear_retained_storage_cursor_statement_state(&mut self) {
7050        for sc in self.storage_cursors.values_mut() {
7051            Self::clear_storage_cursor_statement_state(sc);
7052        }
7053    }
7054
7055    pub fn apply_reusable_table_execution_state(
7056        &mut self,
7057        state: ReusableTableExecutionState,
7058    ) -> ReusableTableExecutionStateOutcome {
7059        let ReusableTableExecutionState {
7060            func_registry,
7061            collation_registry,
7062            schema_cookie,
7063            autoincrement_seq_by_root_page,
7064            rowid_alias_col_by_root_page,
7065            table_column_count_by_root_page,
7066            first_not_null_non_ipk_col_by_root_page,
7067            column_defaults_by_root_page,
7068            index_desc_flags_by_root_page,
7069            index_collations_by_root_page,
7070            reject_mem_fallback,
7071            memdb_rows_loaded,
7072            storage_cursor_memdb_count_shortcuts_safe,
7073            version_store,
7074            collect_result_rows,
7075            max_collected_result_rows,
7076        } = state;
7077
7078        let mut outcome = ReusableTableExecutionStateOutcome::default();
7079        let mut note_rebind = |changed: bool| {
7080            if changed {
7081                outcome.metadata_rebind_count += 1;
7082            }
7083        };
7084
7085        let func_registry_changed = self
7086            .func_registry
7087            .as_ref()
7088            .is_none_or(|current| !Arc::ptr_eq(current, &func_registry));
7089        if func_registry_changed {
7090            self.func_registry = Some(func_registry);
7091            // W1: Clear function caches when func_registry changes, since cached
7092            // function lookups are keyed by program counter and would reference
7093            // stale entries from the old registry.
7094            self.scalar_function_cache.clear();
7095            self.aggregate_function_cache.clear();
7096            outcome.function_cache_cleared = true;
7097        }
7098        note_rebind(func_registry_changed);
7099
7100        let collation_registry_changed =
7101            !Arc::ptr_eq(&self.collation_registry, &collation_registry);
7102        if collation_registry_changed {
7103            self.set_collation_registry(collation_registry);
7104        }
7105        note_rebind(collation_registry_changed);
7106
7107        let schema_cookie_changed = self.schema_cookie != schema_cookie;
7108        if schema_cookie_changed {
7109            self.schema_cookie = schema_cookie;
7110        }
7111        note_rebind(schema_cookie_changed);
7112
7113        let autoincrement_seq_changed =
7114            self.autoincrement_seq_by_root_page != autoincrement_seq_by_root_page;
7115        if autoincrement_seq_changed {
7116            self.autoincrement_seq_by_root_page = autoincrement_seq_by_root_page;
7117        }
7118        note_rebind(autoincrement_seq_changed);
7119
7120        let rowid_alias_changed = !Arc::ptr_eq(
7121            &self.rowid_alias_col_by_root_page,
7122            &rowid_alias_col_by_root_page,
7123        );
7124        if rowid_alias_changed {
7125            self.rowid_alias_col_by_root_page = rowid_alias_col_by_root_page;
7126        }
7127        note_rebind(rowid_alias_changed);
7128
7129        let table_column_count_changed = !Arc::ptr_eq(
7130            &self.table_column_count_by_root_page,
7131            &table_column_count_by_root_page,
7132        );
7133        if table_column_count_changed {
7134            self.table_column_count_by_root_page = table_column_count_by_root_page;
7135        }
7136        note_rebind(table_column_count_changed);
7137
7138        let first_not_null_changed = !Arc::ptr_eq(
7139            &self.first_not_null_non_ipk_col_by_root_page,
7140            &first_not_null_non_ipk_col_by_root_page,
7141        );
7142        if first_not_null_changed {
7143            self.first_not_null_non_ipk_col_by_root_page = first_not_null_non_ipk_col_by_root_page;
7144        }
7145        note_rebind(first_not_null_changed);
7146
7147        let column_defaults_changed = !Arc::ptr_eq(
7148            &self.column_defaults_by_root_page,
7149            &column_defaults_by_root_page,
7150        );
7151        if column_defaults_changed {
7152            self.column_defaults_by_root_page = column_defaults_by_root_page;
7153        }
7154        note_rebind(column_defaults_changed);
7155
7156        let index_desc_flags_changed = !Arc::ptr_eq(
7157            &self.index_desc_flags_by_root_page,
7158            &index_desc_flags_by_root_page,
7159        );
7160        if index_desc_flags_changed {
7161            self.index_desc_flags_by_root_page = index_desc_flags_by_root_page;
7162        }
7163        note_rebind(index_desc_flags_changed);
7164
7165        let index_collations_changed = !Arc::ptr_eq(
7166            &self.index_collations_by_root_page,
7167            &index_collations_by_root_page,
7168        );
7169        if index_collations_changed {
7170            self.index_collations_by_root_page = index_collations_by_root_page;
7171        }
7172        note_rebind(index_collations_changed);
7173
7174        let reject_mem_fallback_changed = self.reject_mem_fallback != reject_mem_fallback;
7175        if reject_mem_fallback_changed {
7176            self.reject_mem_fallback = reject_mem_fallback;
7177        }
7178        note_rebind(reject_mem_fallback_changed);
7179
7180        let memdb_rows_loaded_changed = self.memdb_rows_loaded != memdb_rows_loaded;
7181        if memdb_rows_loaded_changed {
7182            self.memdb_rows_loaded = memdb_rows_loaded;
7183        }
7184        note_rebind(memdb_rows_loaded_changed);
7185
7186        let storage_shortcuts_changed = self.storage_cursor_memdb_count_shortcuts_safe
7187            != storage_cursor_memdb_count_shortcuts_safe;
7188        if storage_shortcuts_changed {
7189            self.storage_cursor_memdb_count_shortcuts_safe =
7190                storage_cursor_memdb_count_shortcuts_safe;
7191        }
7192        note_rebind(storage_shortcuts_changed);
7193
7194        let version_store_changed = match (&self.version_store, &version_store) {
7195            (Some(current), Some(next)) => !Arc::ptr_eq(current, next),
7196            (None, None) => false,
7197            _ => true,
7198        };
7199        if version_store_changed {
7200            self.version_store = version_store;
7201        }
7202        note_rebind(version_store_changed);
7203
7204        let collect_result_rows_changed = self.collect_result_rows != collect_result_rows;
7205        if collect_result_rows_changed {
7206            self.collect_result_rows = collect_result_rows;
7207        }
7208        note_rebind(collect_result_rows_changed);
7209
7210        let max_collected_result_rows_changed =
7211            self.max_collected_result_rows != max_collected_result_rows;
7212        if max_collected_result_rows_changed {
7213            self.max_collected_result_rows = max_collected_result_rows;
7214        }
7215        note_rebind(max_collected_result_rows_changed);
7216
7217        outcome
7218    }
7219
7220    /// Returns the number of rows modified (inserted, deleted, or updated).
7221    pub fn changes(&self) -> usize {
7222        self.changes
7223    }
7224
7225    /// Drain exact logical rows implicitly deleted by REPLACE before the
7226    /// engine is reset or reused.
7227    pub fn take_replace_victims(&mut self) -> Vec<ReplaceVictim> {
7228        std::mem::take(&mut self.replace_victims)
7229    }
7230
7231    /// Returns the rowid of the last INSERT operation, if this execution
7232    /// performed a real INSERT that updated `last_insert_rowid()`.
7233    pub fn last_insert_rowid(&self) -> Option<i64> {
7234        self.last_insert_rowid_valid
7235            .then_some(self.last_insert_rowid)
7236    }
7237
7238    /// Enable or disable `OP_ResultRow` materialization.
7239    pub fn set_collect_result_rows(&mut self, collect_result_rows: bool) {
7240        self.collect_result_rows = collect_result_rows;
7241    }
7242
7243    /// Cap how many `OP_ResultRow` payloads are retained in `self.results`.
7244    pub fn set_max_collected_result_rows(&mut self, max_collected_result_rows: Option<usize>) {
7245        self.max_collected_result_rows = max_collected_result_rows;
7246    }
7247
7248    /// Returns the time-travel marker for a cursor, if any.
7249    pub fn time_travel_marker(&self, cursor_id: i32) -> Option<&TimeTravelMarker> {
7250        self.time_travel_cursors.get(&cursor_id)
7251    }
7252
7253    /// Returns all time-travel cursor mappings.
7254    pub fn time_travel_cursors(&self) -> &HashMap<i32, TimeTravelMarker> {
7255        &self.time_travel_cursors
7256    }
7257
7258    /// Set the MVCC version store for time-travel page resolution.
7259    ///
7260    /// Must be called by the connection layer before executing programs that
7261    /// contain `SetSnapshot` opcodes so the engine can create
7262    /// `TimeTravelPageIo` cursors.
7263    pub fn set_version_store(&mut self, vs: Arc<VersionStore>) {
7264        self.version_store = Some(vs);
7265    }
7266
7267    /// Set the commit log used for time-travel resolution and validation.
7268    pub fn set_time_travel_commit_log(&mut self, log: Arc<Mutex<CommitLog>>) {
7269        self.time_travel_commit_log = Some(log);
7270    }
7271
7272    /// Set the GC horizon for time-travel snapshot validation.
7273    pub fn set_time_travel_gc_horizon(&mut self, horizon: CommitSeq) {
7274        self.time_travel_gc_horizon = Some(horizon);
7275    }
7276
7277    /// Attach the root capability context for this execution.
7278    pub fn set_execution_cx(&mut self, cx: Cx) {
7279        self.execution_cx = cx;
7280    }
7281
7282    fn derive_execution_cx(&self) -> Cx {
7283        self.execution_cx.clone()
7284    }
7285
7286    #[allow(clippy::too_many_arguments)]
7287    fn log_open_storage_cursor_fallback_decision(
7288        trace_id: u64,
7289        certifying_mode: bool,
7290        cursor_id: i32,
7291        root_page: i32,
7292        writable: bool,
7293        backend_kind: &'static str,
7294        decision_reason: &'static str,
7295        decision_outcome: &'static str,
7296        detail: &str,
7297    ) {
7298        let rejected = decision_outcome == "vdbe_mempage_fallback_rejected";
7299        let trace_enabled = if rejected {
7300            tracing::enabled!(target: "fsqlite.fallback_decision", tracing::Level::WARN)
7301        } else {
7302            tracing::enabled!(target: "fsqlite.fallback_decision", tracing::Level::DEBUG)
7303        };
7304        if !trace_enabled {
7305            return;
7306        }
7307
7308        let mode = if certifying_mode {
7309            "parity_cert"
7310        } else {
7311            "fallback_allowed"
7312        };
7313        let statement_fingerprint =
7314            format!("vdbe-open-storage-cursor:{cursor_id}:{root_page}:{decision_reason}");
7315        let statement_kind = "vdbe_open_cursor";
7316        let backend_identity = format!("{backend_kind}:{mode}");
7317        let fallback_boundary = format!("vdbe.open_cursor.{decision_reason}");
7318        let source_touchpoint = "VdbeEngine::open_storage_cursor";
7319        let first_failure_diag = format!(
7320            "statement_kind={statement_kind}; fallback_boundary={fallback_boundary}; \
7321             source_touchpoint={source_touchpoint}; decision_reason={decision_reason}; \
7322             cursor_id={cursor_id}; root_page={root_page}; writable={writable}; detail={detail}"
7323        );
7324        let run_id = "(none)";
7325        let scenario_id = "(none)";
7326        let strict_mode = false;
7327        if rejected {
7328            tracing::warn!(
7329                target: "fsqlite.fallback_decision",
7330                trace_id,
7331                run_id,
7332                scenario_id,
7333                statement_fingerprint = %statement_fingerprint,
7334                statement_kind,
7335                backend_identity = %backend_identity,
7336                fallback_boundary = %fallback_boundary,
7337                decision_reason,
7338                certifying_mode,
7339                strict_mode,
7340                decision_outcome,
7341                source_touchpoint,
7342                first_failure_diag = %first_failure_diag,
7343                "fallback decision telemetry"
7344            );
7345        } else {
7346            tracing::debug!(
7347                target: "fsqlite.fallback_decision",
7348                trace_id,
7349                run_id,
7350                scenario_id,
7351                statement_fingerprint = %statement_fingerprint,
7352                statement_kind,
7353                backend_identity = %backend_identity,
7354                fallback_boundary = %fallback_boundary,
7355                decision_reason,
7356                certifying_mode,
7357                strict_mode,
7358                decision_outcome,
7359                source_touchpoint,
7360                first_failure_diag = %first_failure_diag,
7361                "fallback decision telemetry"
7362            );
7363        }
7364    }
7365
7366    fn index_desc_flags_for_root(&self, root_page: i32) -> Vec<bool> {
7367        self.index_desc_flags_by_root_page
7368            .get(&root_page)
7369            .cloned()
7370            .unwrap_or_default()
7371    }
7372
7373    fn index_collations_for_root(&self, root_page: i32) -> Vec<Option<String>> {
7374        self.index_collations_by_root_page
7375            .get(&root_page)
7376            .cloned()
7377            .unwrap_or_default()
7378    }
7379
7380    fn index_desc_flags_for_cursor(&self, cursor_id: i32) -> Vec<bool> {
7381        if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
7382            return cursor.cursor.index_desc_flags().to_vec();
7383        }
7384        let root_page = self
7385            .cursor_root_pages
7386            .get(&cursor_id)
7387            .copied()
7388            .unwrap_or_default();
7389        self.index_desc_flags_for_root(root_page)
7390    }
7391
7392    fn index_collations_for_cursor(&self, cursor_id: i32) -> Vec<Option<String>> {
7393        if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
7394            return cursor.cursor.index_collations().to_vec();
7395        }
7396        let root_page = self
7397            .cursor_root_pages
7398            .get(&cursor_id)
7399            .copied()
7400            .unwrap_or_default();
7401        self.index_collations_for_root(root_page)
7402    }
7403
7404    async fn storage_cursor_find_exact_index_key(
7405        &mut self,
7406        cursor_id: i32,
7407        key_bytes: &[u8],
7408        missing_registry_detail: &'static str,
7409    ) -> Result<bool> {
7410        let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
7411        let index_collations = self.index_collations_for_cursor(cursor_id);
7412        let uses_collated_probe = index_collations.iter().any(|collation| {
7413            collation
7414                .as_deref()
7415                .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
7416        });
7417        let collated_probe_registry = uses_collated_probe.then(|| {
7418            self.collation_registry
7419                .lock()
7420                .unwrap_or_else(|err| err.into_inner())
7421                .clone()
7422        });
7423
7424        let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
7425            return Ok(false);
7426        };
7427
7428        if uses_collated_probe {
7429            let Some(collation_registry) = collated_probe_registry.as_ref() else {
7430                return Err(FrankenError::internal(missing_registry_detail));
7431            };
7432            storage_cursor_exact_match_collated(
7433                cursor_id,
7434                cursor,
7435                key_bytes,
7436                &index_desc_flags,
7437                &index_collations,
7438                collation_registry,
7439            )
7440            .await
7441        } else {
7442            cursor
7443                .cursor
7444                .index_move_to(&cursor.cx, key_bytes)
7445                .await
7446                .map(|seek| seek.is_found())
7447        }
7448    }
7449
7450    fn table_root_page_for_cursor(&self, cursor_id: i32) -> Option<i32> {
7451        self.cursor_root_pages.get(&cursor_id).copied().or_else(|| {
7452            self.storage_cursors
7453                .get(&cursor_id)
7454                .map(|cursor| cursor.root_page)
7455        })
7456    }
7457
7458    fn logical_table_payload_value(
7459        &self,
7460        table_root_page: Option<i32>,
7461        payload_values: &[SqliteValue],
7462        rowid: i64,
7463        column_index: usize,
7464    ) -> SqliteValue {
7465        let rowid_alias_col_idx = table_root_page
7466            .and_then(|root_page| self.rowid_alias_col_by_root_page.get(&root_page))
7467            .copied();
7468        if rowid_alias_col_idx == Some(column_index) {
7469            return SqliteValue::Integer(rowid);
7470        }
7471
7472        let payload_includes_rowid_alias = rowid_alias_col_idx.is_some_and(|ipk_idx| {
7473            payload_includes_rowid_alias(
7474                payload_values,
7475                rowid,
7476                ipk_idx,
7477                table_root_page.and_then(|root_page| {
7478                    self.table_column_count_by_root_page
7479                        .get(&root_page)
7480                        .copied()
7481                }),
7482                table_root_page.and_then(|root_page| {
7483                    self.first_not_null_non_ipk_col_by_root_page
7484                        .get(&root_page)
7485                        .copied()
7486                }),
7487            )
7488        });
7489
7490        let payload_index = if let Some(ipk_idx) = rowid_alias_col_idx
7491            && column_index > ipk_idx
7492            && !payload_includes_rowid_alias
7493        {
7494            column_index - 1
7495        } else {
7496            column_index
7497        };
7498
7499        payload_values
7500            .get(payload_index)
7501            .cloned()
7502            .unwrap_or_else(|| {
7503                table_root_page
7504                    .and_then(|root_page| self.column_defaults_by_root_page.get(&root_page))
7505                    .and_then(|defaults| defaults.get(column_index))
7506                    .and_then(|default| default.as_ref())
7507                    .cloned()
7508                    .unwrap_or(SqliteValue::Null)
7509            })
7510    }
7511
7512    fn index_key_values_from_table_payload(
7513        &self,
7514        table_root_page: Option<i32>,
7515        payload_values: &[SqliteValue],
7516        rowid: i64,
7517        column_indices: &[usize],
7518    ) -> Vec<SqliteValue> {
7519        let mut key_values = Vec::with_capacity(column_indices.len().saturating_add(1));
7520        for &col_idx in column_indices {
7521            key_values.push(self.logical_table_payload_value(
7522                table_root_page,
7523                payload_values,
7524                rowid,
7525                col_idx,
7526            ));
7527        }
7528        key_values.push(SqliteValue::Integer(rowid));
7529        key_values
7530    }
7531
7532    async fn delete_index_entry_for_rowid(&mut self, cursor_id: i32, rowid: i64) -> Result<()> {
7533        let Some(sc) = self.storage_cursors.get_mut(&cursor_id) else {
7534            return Ok(());
7535        };
7536        if !sc.writable || !sc.cursor.first(&sc.cx).await? {
7537            return Ok(());
7538        }
7539
7540        loop {
7541            let key = sc.cursor.payload(&sc.cx).await?;
7542            let values = parse_record(&key).ok_or_else(|| FrankenError::DatabaseCorrupt {
7543                detail: "REPLACE cleanup encountered a malformed secondary-index record".to_owned(),
7544            })?;
7545            let entry_rowid = values
7546                .last()
7547                .and_then(SqliteValue::as_integer)
7548                .ok_or_else(|| FrankenError::DatabaseCorrupt {
7549                    detail:
7550                        "REPLACE cleanup encountered a secondary-index record without a rowid suffix"
7551                            .to_owned(),
7552                })?;
7553            if entry_rowid == rowid {
7554                sc.cursor.delete(&sc.cx).await?;
7555                invalidate_storage_cursor_row_cache_with_reason(
7556                    sc,
7557                    self.collect_vdbe_metrics,
7558                    DecodeCacheInvalidationReason::WriteMutation,
7559                );
7560                return Ok(());
7561            }
7562            if !sc.cursor.next(&sc.cx).await? {
7563                return Ok(());
7564            }
7565        }
7566    }
7567
7568    fn logical_replace_victim(
7569        &self,
7570        root_page: i32,
7571        stored_values: &[SqliteValue],
7572        rowid: Option<i64>,
7573    ) -> ReplaceVictim {
7574        let column_count = self
7575            .table_column_count_by_root_page
7576            .get(&root_page)
7577            .copied()
7578            .unwrap_or(stored_values.len());
7579        let values = (0..column_count)
7580            .map(|column_index| {
7581                rowid.map_or_else(
7582                    || {
7583                        stored_values
7584                            .get(column_index)
7585                            .cloned()
7586                            .or_else(|| {
7587                                self.column_defaults_by_root_page
7588                                    .get(&root_page)
7589                                    .and_then(|defaults| defaults.get(column_index))
7590                                    .and_then(Clone::clone)
7591                            })
7592                            .unwrap_or(SqliteValue::Null)
7593                    },
7594                    |rowid| {
7595                        self.logical_table_payload_value(
7596                            Some(root_page),
7597                            stored_values,
7598                            rowid,
7599                            column_index,
7600                        )
7601                    },
7602                )
7603            })
7604            .collect();
7605        ReplaceVictim { root_page, values }
7606    }
7607
7608    /// Handles REPLACE conflict resolution natively (bd-2yqp6.x).
7609    /// Deletes the conflicting row from the table AND from all associated indexes.
7610    async fn native_replace_row(&mut self, tbl_cursor_id: i32, conflict_rowid: i64) -> Result<()> {
7611        let old_payload = if let Some(tsc) = self.storage_cursors.get_mut(&tbl_cursor_id) {
7612            if tsc
7613                .cursor
7614                .table_move_to(&tsc.cx, conflict_rowid)
7615                .await?
7616                .is_found()
7617            {
7618                Some(tsc.cursor.payload(&tsc.cx).await?)
7619            } else {
7620                None
7621            }
7622        } else {
7623            None
7624        };
7625
7626        let Some(payload) = old_payload else {
7627            return Ok(());
7628        };
7629
7630        // Parse old row to extract column values for index key construction.
7631        let old_row = parse_record(&payload).ok_or_else(|| {
7632            FrankenError::internal("delete_secondary_index_entries: malformed table record")
7633        })?;
7634        let table_root_page = self.table_root_page_for_cursor(tbl_cursor_id);
7635        let replace_victim = table_root_page.map(|root_page| {
7636            self.logical_replace_victim(root_page, &old_row, Some(conflict_rowid))
7637        });
7638
7639        // Delete secondary index entries for the old row using the metadata
7640        // registered by the codegen. For each index cursor, build the index
7641        // key from the old row's column values and delete it.
7642        let table_index_meta = Arc::clone(&self.table_index_meta);
7643        if let Some(index_metas) = table_index_meta.get(&tbl_cursor_id) {
7644            for meta in index_metas.iter() {
7645                if meta.column_indices.is_empty() {
7646                    // Partial and expression index keys cannot be rebuilt
7647                    // from plain column offsets alone. Every rowid-table
7648                    // secondary key ends with the table rowid, so scan for
7649                    // that exact victim suffix instead of leaving an orphan.
7650                    self.delete_index_entry_for_rowid(meta.cursor_id, conflict_rowid)
7651                        .await?;
7652                    continue;
7653                }
7654                let key_values = self.index_key_values_from_table_payload(
7655                    table_root_page,
7656                    &old_row,
7657                    conflict_rowid,
7658                    &meta.column_indices,
7659                );
7660                let key_bytes = encode_record(&key_values);
7661
7662                // Seek to the key in the index cursor and delete it.
7663                let found =
7664                    self.storage_cursor_find_exact_index_key(
7665                        meta.cursor_id,
7666                        &key_bytes,
7667                        "delete_secondary_index_entries: missing collation registry for collated exact probe",
7668                    )
7669                    .await?;
7670                if found
7671                    && let Some(sc) = self.storage_cursors.get_mut(&meta.cursor_id)
7672                    && sc.writable
7673                {
7674                    sc.cursor.delete(&sc.cx).await?;
7675                    invalidate_storage_cursor_row_cache_with_reason(
7676                        sc,
7677                        self.collect_vdbe_metrics,
7678                        DecodeCacheInvalidationReason::WriteMutation,
7679                    );
7680                }
7681            }
7682        }
7683
7684        // Delete the table row.
7685        if let Some(tsc) = self.storage_cursors.get_mut(&tbl_cursor_id) {
7686            tsc.cursor.table_move_to(&tsc.cx, conflict_rowid).await?;
7687            tsc.cursor.delete(&tsc.cx).await?;
7688            invalidate_storage_cursor_row_cache_with_reason(
7689                tsc,
7690                self.collect_vdbe_metrics,
7691                DecodeCacheInvalidationReason::WriteMutation,
7692            );
7693        }
7694        self.sync_storage_table_delete_into_memdb_mirror(tbl_cursor_id, conflict_rowid);
7695        if let Some(replace_victim) = replace_victim {
7696            self.replace_victims.push(replace_victim);
7697        }
7698
7699        Ok(())
7700    }
7701
7702    async fn rollback_pending_insert_after_index_conflict(
7703        &mut self,
7704        require_pending_table_insert: bool,
7705    ) -> Result<()> {
7706        let entries = self.take_pending_idx_entries();
7707        for (idx_cid, idx_key) in entries {
7708            if self.storage_cursor_find_exact_index_key(
7709                idx_cid,
7710                &idx_key,
7711                "rollback_pending_insert_after_index_conflict: missing collation registry for collated exact probe",
7712            )
7713            .await?
7714                && let Some(isc) = self.storage_cursors.get_mut(&idx_cid)
7715            {
7716                isc.cursor.delete(&isc.cx).await?;
7717                invalidate_storage_cursor_row_cache_with_reason(
7718                    isc,
7719                    self.collect_vdbe_metrics,
7720                    DecodeCacheInvalidationReason::WriteMutation,
7721                );
7722            }
7723        }
7724
7725        let Some(rollback) = self.take_pending_insert_rollback() else {
7726            if require_pending_table_insert {
7727                return Err(FrankenError::internal(
7728                    "secondary-index conflict without pending table insert",
7729                ));
7730            }
7731            return Ok(());
7732        };
7733        let tsc = self
7734            .storage_cursors
7735            .get_mut(&rollback.cursor_id)
7736            .ok_or_else(|| {
7737                FrankenError::internal("table cursor missing during secondary-index rollback")
7738            })?;
7739        if !tsc
7740            .cursor
7741            .table_move_to(&tsc.cx, rollback.rowid)
7742            .await?
7743            .is_found()
7744        {
7745            return Err(FrankenError::internal(
7746                "failed to locate provisional table row during secondary-index rollback",
7747            ));
7748        }
7749        tsc.cursor.delete(&tsc.cx).await?;
7750        invalidate_storage_cursor_row_cache_with_reason(
7751            tsc,
7752            self.collect_vdbe_metrics,
7753            DecodeCacheInvalidationReason::WriteMutation,
7754        );
7755        let rollback_cursor_id = rollback.cursor_id;
7756        let rollback_rowid = rollback.rowid;
7757        self.changes = self.changes.checked_sub(1).ok_or_else(|| {
7758            FrankenError::internal("secondary-index rollback underflowed change counter")
7759        })?;
7760        self.sync_storage_table_delete_into_memdb_mirror(rollback_cursor_id, rollback_rowid);
7761        if let Some(update_restore) = rollback.update_restore {
7762            self.restore_pending_update_after_conflict(update_restore)
7763                .await?;
7764        }
7765        self.last_insert_rowid = rollback.previous_last_insert_rowid;
7766        self.last_insert_rowid_valid = rollback.previous_last_insert_rowid_valid;
7767        self.last_insert_cursor_id = None;
7768        Ok(())
7769    }
7770
7771    async fn restore_pending_update_after_conflict(
7772        &mut self,
7773        restore: PendingUpdateRestore,
7774    ) -> Result<()> {
7775        match restore {
7776            PendingUpdateRestore::Storage {
7777                cursor_id,
7778                rowid,
7779                payload,
7780            } => {
7781                let tsc = self.storage_cursors.get_mut(&cursor_id).ok_or_else(|| {
7782                    FrankenError::internal("table cursor missing during UPDATE conflict restore")
7783                })?;
7784                tsc.cursor.table_insert(&tsc.cx, rowid, &payload).await?;
7785                invalidate_storage_cursor_row_cache_with_reason(
7786                    tsc,
7787                    self.collect_vdbe_metrics,
7788                    DecodeCacheInvalidationReason::WriteMutation,
7789                );
7790
7791                let table_index_meta = Arc::clone(&self.table_index_meta);
7792                let table_root_page = self.table_root_page_for_cursor(cursor_id);
7793                if let Some(index_metas) = table_index_meta.get(&cursor_id) {
7794                    let old_row = parse_record(&payload).ok_or_else(|| {
7795                        FrankenError::internal(
7796                            "UPDATE conflict restore could not decode original row payload",
7797                        )
7798                    })?;
7799                    for meta in index_metas.iter() {
7800                        // Empty column metadata denotes an expression or
7801                        // partial index. These indexes were not restorable by
7802                        // this path before they were registered for REPLACE
7803                        // cleanup, so do not synthesize an invalid `(rowid)`
7804                        // key here.
7805                        if meta.column_indices.is_empty() {
7806                            continue;
7807                        }
7808                        let key_values = self.index_key_values_from_table_payload(
7809                            table_root_page,
7810                            &old_row,
7811                            rowid,
7812                            &meta.column_indices,
7813                        );
7814                        let key_bytes = encode_record(&key_values);
7815                        if let Some(sc) = self.storage_cursors.get_mut(&meta.cursor_id)
7816                            && sc.writable
7817                        {
7818                            sc.cursor.index_insert(&sc.cx, &key_bytes).await?;
7819                            invalidate_storage_cursor_row_cache_with_reason(
7820                                sc,
7821                                self.collect_vdbe_metrics,
7822                                DecodeCacheInvalidationReason::WriteMutation,
7823                            );
7824                        }
7825                    }
7826                }
7827                self.sync_storage_table_restore_into_memdb_mirror(cursor_id, rowid, &payload);
7828            }
7829            PendingUpdateRestore::Mem {
7830                root_page,
7831                rowid,
7832                values,
7833            } => {
7834                let db = self.db.as_mut().ok_or_else(|| {
7835                    FrankenError::internal("MemDatabase missing during UPDATE conflict restore")
7836                })?;
7837                db.upsert_row(root_page, rowid, values);
7838            }
7839        }
7840        Ok(())
7841    }
7842
7843    /// Attach an in-memory database for cursor operations.
7844    pub fn set_database(&mut self, mut db: MemDatabase) {
7845        db.set_collation_registry(Arc::clone(&self.collation_registry));
7846        self.db = Some(db);
7847    }
7848
7849    /// Declare whether the attached `MemDatabase` is a complete row mirror for
7850    /// storage-backed table reads.
7851    pub fn set_memdb_rows_loaded(&mut self, loaded: bool) {
7852        self.memdb_rows_loaded = loaded;
7853    }
7854
7855    /// Declare whether storage-cursor `Count` and row-mirror scan fast paths
7856    /// may trust the attached `MemDatabase` row mirror.
7857    pub fn set_storage_cursor_memdb_count_shortcuts_safe(&mut self, safe: bool) {
7858        self.storage_cursor_memdb_count_shortcuts_safe = safe;
7859    }
7860
7861    /// Whether the attached `MemDatabase` row mirror still exactly matches
7862    /// storage-backed table contents for fast-path reads.
7863    pub fn storage_cursor_memdb_count_shortcuts_safe(&self) -> bool {
7864        self.storage_cursor_memdb_count_shortcuts_safe
7865    }
7866
7867    /// Mark a root page as dirty for lazy MemDB sync.
7868    /// Instead of syncing MemDB on every DML operation, we just mark the table
7869    /// dirty and bulk-sync only when a compatibility-path query needs MemDB data.
7870    #[inline]
7871    fn mark_root_page_dirty(&mut self, root_page: i32) {
7872        self.dirty_root_pages.insert(root_page);
7873    }
7874
7875    /// Mark a known storage-backed table root page dirty without another
7876    /// cursor-to-root lookup on the hot write path.
7877    #[inline]
7878    fn mark_storage_root_page_dirty(&mut self, root_page: i32) {
7879        self.mark_root_page_dirty(root_page);
7880        // Signal to Connection that MemDB is stale — queries should fall
7881        // back to pager-backed reads until a sync or reload occurs.
7882        self.storage_cursor_memdb_count_shortcuts_safe = false;
7883    }
7884
7885    /// Mark the table identified by cursor_id as dirty for lazy MemDB sync
7886    /// when the caller does not already know the table root page.
7887    /// Also sets `storage_cursor_memdb_count_shortcuts_safe` to false so the
7888    /// Connection layer knows MemDB is stale and will fall back to pager reads.
7889    fn mark_storage_table_dirty(&mut self, cursor_id: i32) {
7890        if let Some(root_page) = self.cursor_root_pages.get(&cursor_id).copied() {
7891            self.mark_storage_root_page_dirty(root_page);
7892        }
7893    }
7894
7895    /// Returns true if any tables have been modified and need MemDB sync.
7896    pub fn has_dirty_root_pages(&self) -> bool {
7897        !self.dirty_root_pages.is_empty()
7898    }
7899
7900    /// Returns the set of dirty root pages for bulk sync.
7901    pub fn dirty_root_pages(&self) -> &HashSet<i32> {
7902        &self.dirty_root_pages
7903    }
7904
7905    /// Clear dirty root pages after bulk sync completes.
7906    pub fn clear_dirty_root_pages(&mut self) {
7907        self.dirty_root_pages.clear();
7908    }
7909
7910    fn sync_storage_table_delete_into_memdb_mirror(&mut self, cursor_id: i32, _rowid: i64) {
7911        // bd-g595c: Lazy MemDB dirty tracking — mark dirty instead of immediate sync.
7912        self.mark_storage_table_dirty(cursor_id);
7913    }
7914
7915    fn sync_storage_table_restore_into_memdb_mirror(
7916        &mut self,
7917        cursor_id: i32,
7918        _rowid: i64,
7919        _payload: &[u8],
7920    ) {
7921        // bd-g595c: Lazy MemDB dirty tracking — mark dirty instead of immediate sync.
7922        self.mark_storage_table_dirty(cursor_id);
7923    }
7924
7925    /// Take ownership of the in-memory database back from the engine.
7926    pub fn take_database(&mut self) -> Option<MemDatabase> {
7927        self.db.take()
7928    }
7929
7930    /// Register a type-erased virtual table cursor for opcode execution.
7931    pub fn register_vtab_cursor(&mut self, cursor_id: i32, cursor: Box<dyn ErasedVtabCursor>) {
7932        self.ensure_cold_state_for(StatementColdState::VTAB_CURSORS)
7933            .vtab_cursors
7934            .insert(
7935                cursor_id,
7936                VtabCursorState {
7937                    cursor,
7938                    null_row: false,
7939                },
7940            );
7941    }
7942
7943    /// Register a virtual table instance for lifecycle and cursor operations.
7944    pub fn register_vtab_instance(
7945        &mut self,
7946        cursor_id: i32,
7947        instance: Box<dyn fsqlite_func::vtab::ErasedVtabInstance>,
7948    ) {
7949        self.vtab_instances
7950            .insert(cursor_id, Box::new(ErasedVtabBridge(instance)));
7951    }
7952
7953    /// Enable/disable storage-backed cursor execution for `OpenRead`/`OpenWrite`.
7954    pub fn enable_storage_cursors(&mut self, enabled: bool) {
7955        self.storage_cursors_enabled = enabled;
7956    }
7957
7958    /// Backwards-compatible alias for [`Self::enable_storage_cursors`].
7959    pub fn enable_storage_read_cursors(&mut self, enabled: bool) {
7960        self.enable_storage_cursors(enabled);
7961    }
7962
7963    /// Enable parity-certification mode (bd-2ttd8.1).
7964    ///
7965    /// When enabled, `open_storage_cursor` will refuse to fall back to the
7966    /// in-memory `MemPageStore` path and instead return an error. This
7967    /// verifies that all cursor operations route through the real
7968    /// Pager+BtreeCursor stack (`txn_page_io`).
7969    pub fn set_reject_mem_fallback(&mut self, reject: bool) {
7970        self.reject_mem_fallback = reject;
7971    }
7972
7973    /// Returns `true` if all open storage cursors use the real pager backend
7974    /// (`CursorBackend::Txn`). Returns `true` vacuously when no cursors are open.
7975    ///
7976    /// Used by parity-certification (bd-2ttd8.4) to verify that no cursor
7977    /// accidentally routed through MemPageStore.
7978    #[must_use]
7979    pub fn all_cursors_are_txn_backed(&self) -> bool {
7980        self.storage_cursors.values().all(|sc| sc.cursor.is_txn())
7981    }
7982
7983    /// Returns `true` if any open storage cursor uses the in-memory backend.
7984    #[must_use]
7985    pub fn has_mem_cursor(&self) -> bool {
7986        self.storage_cursors.values().any(|sc| sc.cursor.is_mem())
7987    }
7988
7989    /// Validate the parity-certification invariant: if `reject_mem_fallback`
7990    /// is enabled, no storage cursor should be backed by MemPageStore.
7991    ///
7992    /// Returns `Ok(())` if the invariant holds, or `Err` with a diagnostic
7993    /// message listing the offending cursor IDs.
7994    pub fn validate_parity_cert_invariant(&self) -> std::result::Result<(), String> {
7995        if !self.reject_mem_fallback {
7996            return Ok(());
7997        }
7998        let mem_cursors: Vec<i32> = self
7999            .storage_cursors
8000            .iter()
8001            .filter(|(_, sc)| sc.cursor.is_mem())
8002            .map(|(id, _)| id)
8003            .collect();
8004        if mem_cursors.is_empty() {
8005            Ok(())
8006        } else {
8007            Err(format!(
8008                "bd-2ttd8.4: parity-cert violation — {} cursor(s) routed through MemPageStore: {:?}",
8009                mem_cursors.len(),
8010                mem_cursors
8011            ))
8012        }
8013    }
8014
8015    fn attach_transaction_state(
8016        &mut self,
8017        txn: TransactionKind,
8018        concurrent: Option<ConcurrentContext>,
8019    ) {
8020        if let Some(txn_page_io) = self.txn_page_io.as_ref() {
8021            txn_page_io.refill(txn, concurrent);
8022        } else {
8023            self.txn_page_io = Some(SharedTxnPageIo::from_parts(txn, concurrent));
8024        }
8025        self.storage_cursors_enabled = true;
8026    }
8027
8028    /// Lend a pager transaction to the engine for storage cursor I/O.
8029    ///
8030    /// When set, `open_storage_cursor` routes through the real pager/WAL
8031    /// stack (`SharedTxnPageIo`) instead of building transient `MemPageStore`
8032    /// snapshots. Also enables storage cursors automatically.
8033    pub fn set_transaction(&mut self, txn: impl Into<TransactionKind>) {
8034        self.attach_transaction_state(txn.into(), None);
8035    }
8036
8037    /// Lend a pager transaction with MVCC concurrent context (bd-kivg / 5E.2).
8038    ///
8039    /// Like [`set_transaction`](Self::set_transaction), but also enables
8040    /// MVCC page-level locking for concurrent writers. When the concurrent
8041    /// context is present:
8042    /// - Write operations acquire page-level locks via [`fsqlite_mvcc::concurrent_write_page`]
8043    /// - Written pages are recorded in the write set for FCW validation at commit
8044    pub fn set_transaction_concurrent(
8045        &mut self,
8046        txn: impl Into<TransactionKind>,
8047        session_id: u64,
8048        handle: SharedConcurrentHandle,
8049        lock_table: Arc<InProcessPageLockTable>,
8050        commit_index: Arc<CommitIndex>,
8051        busy_timeout_ms: u64,
8052    ) {
8053        self.attach_transaction_state(
8054            txn.into(),
8055            Some(ConcurrentContext::new(
8056                session_id,
8057                handle,
8058                lock_table,
8059                commit_index,
8060                busy_timeout_ms,
8061            )),
8062        );
8063    }
8064
8065    /// Provide the shared concurrent rowid allocator for implicit inserts.
8066    pub fn set_concurrent_rowid_allocator(
8067        &mut self,
8068        allocator: Arc<ConcurrentRowIdAllocator>,
8069        schema_epoch: SchemaEpoch,
8070    ) {
8071        self.concurrent_rowid_allocator = Some(allocator);
8072        self.concurrent_rowid_schema_epoch = schema_epoch;
8073    }
8074
8075    fn storage_cursor_runtime_meta(&self, root_page: i32) -> (RowIdMode, i64) {
8076        match self.autoincrement_seq_by_root_page.get(&root_page).copied() {
8077            Some(high_water) => (RowIdMode::AutoIncrement, high_water),
8078            None => (RowIdMode::Normal, 0),
8079        }
8080    }
8081
8082    async fn storage_cursor_visible_max_rowid(sc: &mut StorageCursor) -> Result<i64> {
8083        if sc.cursor.last(&sc.cx).await? {
8084            sc.cursor.rowid(&sc.cx).await
8085        } else {
8086            Ok(0)
8087        }
8088    }
8089
8090    fn concurrent_rowid_key(schema_epoch: SchemaEpoch, root_page: i32) -> Result<AllocatorKey> {
8091        let root_page_u32 = u32::try_from(root_page).map_err(|_| {
8092            FrankenError::internal(format!(
8093                "invalid concurrent rowid root page {root_page} for allocator"
8094            ))
8095        })?;
8096        Ok(AllocatorKey {
8097            schema_epoch,
8098            table_id: TableId::new(root_page_u32),
8099        })
8100    }
8101
8102    fn map_rowid_allocator_error(
8103        err: impl std::fmt::Display,
8104        detail: &'static str,
8105    ) -> FrankenError {
8106        FrankenError::VdbeExecutionError {
8107            detail: format!("{detail}: {err}"),
8108        }
8109    }
8110
8111    async fn allocate_serialized_storage_rowid(
8112        sc: &mut StorageCursor,
8113        autoinc_max: i64,
8114        overflow_detail: &'static str,
8115    ) -> Result<i64> {
8116        let base = if sc.last_alloc_rowid > 0 {
8117            sc.last_alloc_rowid.max(autoinc_max)
8118        } else {
8119            Self::storage_cursor_visible_max_rowid(sc)
8120                .await?
8121                .max(autoinc_max)
8122        };
8123        let rowid = base
8124            .checked_add(1)
8125            .ok_or_else(|| FrankenError::VdbeExecutionError {
8126                detail: overflow_detail.into(),
8127            })?;
8128        sc.last_alloc_rowid = rowid;
8129        Ok(rowid)
8130    }
8131
8132    async fn allocate_concurrent_storage_rowid(
8133        allocator: &ConcurrentRowIdAllocator,
8134        schema_epoch: SchemaEpoch,
8135        root_page: i32,
8136        mode: RowIdMode,
8137        autoinc_max: i64,
8138        sc: &mut StorageCursor,
8139        overflow_detail: &'static str,
8140    ) -> Result<i64> {
8141        let visible_max = if sc.last_alloc_rowid > 0 {
8142            sc.last_alloc_rowid.max(autoinc_max)
8143        } else {
8144            Self::storage_cursor_visible_max_rowid(sc)
8145                .await?
8146                .max(autoinc_max)
8147        };
8148        let key = Self::concurrent_rowid_key(schema_epoch, root_page)?;
8149        allocator.ensure_table_floor(
8150            key,
8151            (visible_max > 0).then(|| RowId::new(visible_max)),
8152            autoinc_max,
8153            mode,
8154        );
8155        let rowid = allocator
8156            .allocate_one(key)
8157            .map_err(|err| Self::map_rowid_allocator_error(err, overflow_detail))?
8158            .get();
8159        sc.last_alloc_rowid = rowid;
8160        Ok(rowid)
8161    }
8162
8163    async fn bump_concurrent_storage_rowid_floor(
8164        allocator: &ConcurrentRowIdAllocator,
8165        schema_epoch: SchemaEpoch,
8166        root_page: i32,
8167        mode: RowIdMode,
8168        autoinc_max: i64,
8169        sc: &mut StorageCursor,
8170        rowid: i64,
8171    ) -> Result<()> {
8172        let visible_max = Self::storage_cursor_visible_max_rowid(sc)
8173            .await?
8174            .max(autoinc_max)
8175            .max(rowid);
8176        let key = Self::concurrent_rowid_key(schema_epoch, root_page)?;
8177        allocator.ensure_table_floor(
8178            key,
8179            (visible_max > 0).then(|| RowId::new(visible_max)),
8180            autoinc_max,
8181            mode,
8182        );
8183        allocator
8184            .bump_explicit(key, RowId::new(rowid))
8185            .map_err(|err| Self::map_rowid_allocator_error(err, "explicit rowid bump failed"))?;
8186        Ok(())
8187    }
8188
8189    /// Take back the pager transaction after execution.
8190    ///
8191    /// All storage cursors must be dropped first (cleared during execution
8192    /// cleanup).
8193    pub fn take_transaction(&mut self) -> Result<Option<TransactionKind>> {
8194        // Drop all storage cursors first to release Rc references.
8195        self.storage_cursors.clear();
8196        match self.txn_page_io.take() {
8197            Some(txn_page_io) => Ok(Some(txn_page_io.into_inner()?)),
8198            None => Ok(None),
8199        }
8200    }
8201
8202    /// bd-perf: Swap the inner transaction without dropping the Rc.
8203    /// Retained StorageCursors hold Rc clones that remain valid.
8204    /// Returns the old transaction.
8205    pub fn refill_transaction(
8206        &mut self,
8207        txn: impl Into<TransactionKind>,
8208    ) -> Option<TransactionKind> {
8209        self.txn_page_io
8210            .as_ref()
8211            .map(|io| io.refill(txn.into(), None))
8212    }
8213
8214    /// bd-perf: Extract the transaction while keeping the Rc alive for
8215    /// retained cursors. The Rc inner becomes TransactionKind::Drained
8216    /// until the next refill_transaction call.
8217    pub fn drain_transaction(&mut self) -> Option<TransactionKind> {
8218        self.txn_page_io.as_ref().map(|io| io.drain())
8219    }
8220
8221    /// bd-perf: Check if a txn_page_io is present (for cursor reuse decisions).
8222    pub fn has_txn_page_io(&self) -> bool {
8223        self.txn_page_io.is_some()
8224    }
8225
8226    /// bd-perf: Check if storage cursors are empty (for drain vs take decision).
8227    pub fn storage_cursors_empty(&self) -> bool {
8228        self.storage_cursors.is_empty()
8229    }
8230
8231    /// Attach a function registry for `Function`/`PureFunc` opcode dispatch.
8232    pub fn set_function_registry(&mut self, registry: Arc<FunctionRegistry>) {
8233        self.func_registry = Some(registry);
8234    }
8235
8236    /// Attach a shared collation registry for compare and sorting opcodes.
8237    pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
8238        self.collation_registry = Arc::clone(&registry);
8239        if let Some(db) = self.db.as_mut() {
8240            db.set_collation_registry(registry);
8241        }
8242    }
8243
8244    /// Acquire a read-lock on the collation registry.  Callers should hold
8245    /// the guard for the duration of a comparison batch (e.g. an entire
8246    /// opcode or sort run) rather than re-acquiring per comparison.
8247    #[inline]
8248    fn lock_collation(&self) -> std::sync::MutexGuard<'_, CollationRegistry> {
8249        self.collation_registry
8250            .lock()
8251            .unwrap_or_else(|e| e.into_inner())
8252    }
8253
8254    /// Replace the current set of bound SQL parameters.
8255    ///
8256    /// Values are 1-indexed at execution time (`?1` maps to `bindings[0]`).
8257    pub fn set_bindings(&mut self, bindings: Vec<SqliteValue>) {
8258        self.bindings = bindings.into_iter().collect();
8259    }
8260
8261    /// Replace bindings from a slice while keeping small parameter sets inline.
8262    pub fn set_bindings_slice(&mut self, bindings: &[SqliteValue]) {
8263        self.bindings.clear();
8264        self.bindings.extend(bindings.iter().cloned());
8265    }
8266
8267    /// Set the schema cookie that `ReadCookie` will return and
8268    /// `Transaction` will use for stale-schema detection (bd-3mmj).
8269    pub fn set_schema_cookie(&mut self, cookie: u32) {
8270        self.schema_cookie = cookie;
8271    }
8272
8273    /// Read the current schema cookie value (possibly updated by `SetCookie`).
8274    pub fn schema_cookie(&self) -> u32 {
8275        self.schema_cookie
8276    }
8277
8278    /// Provide AUTOINCREMENT high-water marks keyed by root page (bd-31j76).
8279    /// The engine uses these to guarantee monotonically increasing rowids
8280    /// for tables declared with `AUTOINCREMENT`.
8281    pub fn set_autoincrement_sequence_by_root_page(&mut self, map: HashMap<i32, i64>) {
8282        self.autoincrement_seq_by_root_page = map;
8283    }
8284
8285    /// Provide INTEGER PRIMARY KEY alias column positions keyed by root page.
8286    pub fn set_rowid_alias_column_by_root_page(&mut self, map: HashMap<i32, usize>) {
8287        self.rowid_alias_col_by_root_page = Arc::new(map);
8288    }
8289
8290    /// Reuse shared INTEGER PRIMARY KEY alias column positions keyed by root page.
8291    pub fn set_shared_rowid_alias_column_by_root_page(&mut self, map: Arc<HashMap<i32, usize>>) {
8292        self.rowid_alias_col_by_root_page = map;
8293    }
8294
8295    /// Provide declared table column counts keyed by root page.
8296    pub fn set_table_column_count_by_root_page(&mut self, map: HashMap<i32, usize>) {
8297        self.table_column_count_by_root_page = Arc::new(map);
8298    }
8299
8300    /// Reuse shared declared table column counts keyed by root page.
8301    pub fn set_shared_table_column_count_by_root_page(&mut self, map: Arc<HashMap<i32, usize>>) {
8302        self.table_column_count_by_root_page = map;
8303    }
8304
8305    /// Provide earliest non-IPK NOT NULL columns keyed by root page.
8306    pub fn set_first_not_null_non_ipk_col_by_root_page(&mut self, map: HashMap<i32, usize>) {
8307        self.first_not_null_non_ipk_col_by_root_page = Arc::new(map);
8308    }
8309
8310    /// Reuse shared earliest non-IPK NOT NULL columns keyed by root page.
8311    pub fn set_shared_first_not_null_non_ipk_col_by_root_page(
8312        &mut self,
8313        map: Arc<HashMap<i32, usize>>,
8314    ) {
8315        self.first_not_null_non_ipk_col_by_root_page = map;
8316    }
8317
8318    /// Set column default values by root page, used for ALTER TABLE ADD COLUMN.
8319    /// Each entry maps a root page to a list of per-column defaults (None = no default).
8320    pub fn set_column_defaults_by_root_page(
8321        &mut self,
8322        map: HashMap<i32, Vec<Option<SqliteValue>>>,
8323    ) {
8324        self.column_defaults_by_root_page = Arc::new(map);
8325    }
8326
8327    /// Reuse shared column default values by root page.
8328    pub fn set_shared_column_defaults_by_root_page(
8329        &mut self,
8330        map: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
8331    ) {
8332        self.column_defaults_by_root_page = map;
8333    }
8334
8335    /// Provide per-index descending flags keyed by index root page.
8336    pub fn set_index_desc_flags_by_root_page(&mut self, map: HashMap<i32, Vec<bool>>) {
8337        self.index_desc_flags_by_root_page = Arc::new(map);
8338    }
8339
8340    /// Reuse shared per-index descending flags keyed by index root page.
8341    pub fn set_shared_index_desc_flags_by_root_page(&mut self, map: Arc<HashMap<i32, Vec<bool>>>) {
8342        self.index_desc_flags_by_root_page = map;
8343    }
8344
8345    /// Provide per-index collation sequences keyed by index root page.
8346    pub fn set_index_collations_by_root_page(&mut self, map: HashMap<i32, Vec<Option<String>>>) {
8347        self.index_collations_by_root_page = Arc::new(map);
8348    }
8349
8350    /// Reuse shared per-index collation sequences keyed by index root page.
8351    pub fn set_shared_index_collations_by_root_page(
8352        &mut self,
8353        map: Arc<HashMap<i32, Vec<Option<String>>>>,
8354    ) {
8355        self.index_collations_by_root_page = map;
8356    }
8357
8358    /// Execute a VDBE program to completion.
8359    ///
8360    /// Returns `Ok(ExecOutcome::Done)` on normal halt, or an error if the
8361    /// program encounters a fatal condition.
8362    #[allow(
8363        clippy::too_many_lines,
8364        clippy::match_same_arms,
8365        clippy::cast_sign_loss,
8366        clippy::cast_possible_truncation,
8367        clippy::cast_possible_wrap
8368    )]
8369    pub async fn execute(&mut self, program: &VdbeProgram) -> Result<ExecOutcome> {
8370        self.execute_with_borrowed_bindings_internal(program, None, None)
8371            .await
8372    }
8373
8374    /// Execute a program using a borrowed binding slice for this run only.
8375    ///
8376    /// This avoids cloning bound parameters into the engine when the caller
8377    /// already owns a short-lived binding slice for a single execution.
8378    #[allow(
8379        clippy::too_many_lines,
8380        clippy::match_same_arms,
8381        clippy::cast_sign_loss,
8382        clippy::cast_possible_truncation,
8383        clippy::cast_possible_wrap
8384    )]
8385    pub async fn execute_with_borrowed_bindings(
8386        &mut self,
8387        program: &VdbeProgram,
8388        borrowed_bindings: Option<&[SqliteValue]>,
8389    ) -> Result<ExecOutcome> {
8390        self.execute_with_borrowed_bindings_internal(program, borrowed_bindings, None)
8391            .await
8392    }
8393
8394    /// Execute a program using a borrowed binding slice and a per-row callback.
8395    ///
8396    /// The callback is invoked for each `OP_ResultRow` payload as soon as the
8397    /// row is materialized, allowing callers to process large result sets
8398    /// without retaining them in `self.results`.
8399    pub async fn execute_with_borrowed_bindings_and_row_handler(
8400        &mut self,
8401        program: &VdbeProgram,
8402        borrowed_bindings: Option<&[SqliteValue]>,
8403        row_handler: &mut ResultRowCallback<'_>,
8404    ) -> Result<ExecOutcome> {
8405        self.execute_with_borrowed_bindings_internal(program, borrowed_bindings, Some(row_handler))
8406            .await
8407    }
8408
8409    async fn execute_with_borrowed_bindings_internal(
8410        &mut self,
8411        program: &VdbeProgram,
8412        borrowed_bindings: Option<&[SqliteValue]>,
8413        mut row_handler: Option<&mut ResultRowCallback<'_>>,
8414    ) -> Result<ExecOutcome> {
8415        let _record_profile_scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
8416        let _pipeline_profile_stage = enter_vdbe_execute_profile_stage();
8417        let estimated_make_record_capacity =
8418            estimate_make_record_buffer_capacity(program, self.page_size);
8419        self.make_record_lookaside
8420            .prepare_for_statement(estimated_make_record_capacity);
8421        if !self.statement_state_clean {
8422            self.clear_statement_cold_state();
8423            self.results.clear();
8424            self.last_compare_result = None;
8425            self.changes = 0;
8426            self.replace_victims.clear();
8427            self.last_insert_rowid = 0;
8428            self.last_insert_rowid_valid = false;
8429            self.last_insert_cursor_id = None;
8430            self.fk_counter = 0;
8431            self.cursor_root_pages.clear();
8432        }
8433        self.statement_state_clean = false;
8434        self.table_index_meta = Arc::clone(program.shared_table_index_meta());
8435
8436        let ops = program.ops();
8437        if ops.is_empty() {
8438            return Ok(ExecOutcome::Done);
8439        }
8440
8441        // Pre-size the register file to the program's declared register count
8442        // so that per-opcode register writes never need bounds-check + resize.
8443        // This eliminates a branch from every set_reg/set_reg_fast in the hot loop.
8444        let reg_count = usize::try_from(program.register_count()).unwrap_or(0);
8445        if self.registers.len() < reg_count {
8446            self.registers.resize(reg_count, SqliteValue::Null);
8447        }
8448
8449        let statement_debug_enabled =
8450            tracing::enabled!(target: "fsqlite_vdbe::statement", tracing::Level::DEBUG);
8451        let jit_enabled = vdbe_jit_enabled();
8452        let jit_debug_enabled =
8453            jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::DEBUG);
8454        let jit_info_enabled =
8455            jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::INFO);
8456        let jit_warn_enabled =
8457            jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::WARN);
8458        let exec_info_enabled = tracing::enabled!(target: "fsqlite_vdbe", tracing::Level::INFO);
8459        let slow_query_info_enabled =
8460            tracing::enabled!(target: "fsqlite_vdbe::slow_query", tracing::Level::INFO);
8461        let collect_vdbe_metrics = vdbe_metrics_enabled();
8462        self.collect_vdbe_metrics = collect_vdbe_metrics;
8463        let needs_statement_timing = collect_vdbe_metrics
8464            || statement_debug_enabled
8465            || exec_info_enabled
8466            || slow_query_info_enabled;
8467        let program_id = if statement_debug_enabled
8468            || jit_debug_enabled
8469            || jit_info_enabled
8470            || jit_warn_enabled
8471            || exec_info_enabled
8472            || slow_query_info_enabled
8473        {
8474            VDBE_PROGRAM_ID_SEQ.fetch_add(1, AtomicOrdering::Relaxed)
8475        } else {
8476            0
8477        };
8478        let start_time = needs_statement_timing.then(Instant::now);
8479        let mut opcode_count: u64 = 0;
8480        let mut local_opcode_execution_totals =
8481            collect_vdbe_metrics.then(|| vec![0_u64; Opcode::COUNT + 1].into_boxed_slice());
8482
8483        if statement_debug_enabled {
8484            tracing::debug!(
8485                target: "fsqlite_vdbe::statement",
8486                program_id,
8487                num_ops = ops.len(),
8488                "vdbe statement begin",
8489            );
8490        }
8491
8492        let mut compiled_program = None;
8493        if jit_enabled {
8494            match maybe_trigger_jit(program) {
8495                JitDecision::Disabled => {}
8496                JitDecision::Warming {
8497                    plan_hash,
8498                    execution_count,
8499                } => {
8500                    if jit_debug_enabled {
8501                        tracing::debug!(
8502                            target: "fsqlite_vdbe::jit",
8503                            program_id,
8504                            plan_hash = format_args!("{plan_hash:016x}"),
8505                            execution_count,
8506                            hot_threshold = vdbe_jit_hot_threshold(),
8507                            "jit warmup (interpreter tier)"
8508                        );
8509                    }
8510                }
8511                JitDecision::CacheHit {
8512                    plan_hash,
8513                    code_size_bytes,
8514                    compiled_program: cached_program,
8515                } => {
8516                    if jit_info_enabled {
8517                        tracing::info!(
8518                            target: "fsqlite_vdbe::jit",
8519                            program_id,
8520                            plan_hash = format_args!("{plan_hash:016x}"),
8521                            code_size_bytes,
8522                            "jit trigger cache hit"
8523                        );
8524                    }
8525                    compiled_program = Some(cached_program);
8526                }
8527                JitDecision::UnsupportedCached { plan_hash } => {
8528                    if jit_debug_enabled {
8529                        tracing::debug!(
8530                            target: "fsqlite_vdbe::jit",
8531                            program_id,
8532                            plan_hash = format_args!("{plan_hash:016x}"),
8533                            "jit unsupported-plan cache hit (skipping recompilation)"
8534                        );
8535                    }
8536                }
8537                JitDecision::Compiled {
8538                    plan_hash,
8539                    compile_time_us,
8540                    code_size_bytes,
8541                    evicted_plan_hash,
8542                    compiled_program: newly_compiled_program,
8543                } => {
8544                    if jit_info_enabled {
8545                        let plan_hash_hex = format!("{plan_hash:016x}");
8546                        let span = tracing::info_span!(
8547                            target: "fsqlite_vdbe::jit",
8548                            "jit_compile",
8549                            plan_hash = %plan_hash_hex,
8550                            compile_time_us,
8551                            code_size_bytes,
8552                        );
8553                        let _compile_guard = span.enter();
8554                        tracing::info!(
8555                            target: "fsqlite_vdbe::jit",
8556                            program_id,
8557                            plan_hash = %plan_hash_hex,
8558                            compile_time_us,
8559                            code_size_bytes,
8560                            evicted_plan_hash = evicted_plan_hash.map(|value| format!("{value:016x}")),
8561                            "jit trigger compile completed"
8562                        );
8563                    }
8564                    compiled_program = Some(newly_compiled_program);
8565                }
8566                JitDecision::CompileFailed {
8567                    plan_hash,
8568                    compile_time_us,
8569                    reason,
8570                } => {
8571                    if jit_warn_enabled {
8572                        let plan_hash_hex = format!("{plan_hash:016x}");
8573                        let span = tracing::info_span!(
8574                            target: "fsqlite_vdbe::jit",
8575                            "jit_compile",
8576                            plan_hash = %plan_hash_hex,
8577                            compile_time_us,
8578                            code_size_bytes = 0_u64,
8579                        );
8580                        let _compile_guard = span.enter();
8581                        tracing::warn!(
8582                            target: "fsqlite_vdbe::jit",
8583                            program_id,
8584                            plan_hash = %plan_hash_hex,
8585                            compile_time_us,
8586                            reason,
8587                            "jit compilation failed; falling back to interpreter"
8588                        );
8589                    }
8590                }
8591            }
8592        }
8593
8594        if let Some(compiled_program) = compiled_program {
8595            let outcome = self
8596                .execute_compiled_program(
8597                    compiled_program.as_ref(),
8598                    borrowed_bindings,
8599                    collect_vdbe_metrics,
8600                    &mut row_handler,
8601                )
8602                .await?;
8603
8604            if !needs_statement_timing {
8605                return Ok(outcome);
8606            }
8607
8608            let elapsed = start_time
8609                .expect("statement timing state exists when post-execution bookkeeping is enabled")
8610                .elapsed();
8611            let elapsed_us = elapsed.as_micros();
8612            let result_rows = self.results.len();
8613
8614            if collect_vdbe_metrics {
8615                FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.fetch_add(0, AtomicOrdering::Relaxed);
8616                FSQLITE_VDBE_STATEMENTS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
8617                #[allow(clippy::cast_possible_truncation)]
8618                FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
8619                    .fetch_add(elapsed_us as u64, AtomicOrdering::Relaxed);
8620            }
8621
8622            let log_statement_done = || {
8623                if statement_debug_enabled {
8624                    tracing::debug!(
8625                        target: "fsqlite_vdbe::statement",
8626                        program_id,
8627                        opcode_count = 0_u64,
8628                        result_rows,
8629                        elapsed_us = elapsed_us as u64,
8630                        outcome = ?outcome,
8631                        "vdbe statement done",
8632                    );
8633                }
8634
8635                if slow_query_info_enabled && elapsed.as_millis() >= SLOW_QUERY_THRESHOLD_MS {
8636                    #[allow(clippy::cast_possible_truncation)]
8637                    let millis = elapsed.as_millis() as u64;
8638                    tracing::info!(
8639                        target: "fsqlite_vdbe::slow_query",
8640                        program_id,
8641                        opcode_count = 0_u64,
8642                        result_rows,
8643                        elapsed_ms = millis,
8644                        "slow vdbe statement",
8645                    );
8646                }
8647            };
8648
8649            if exec_info_enabled {
8650                let span = tracing::info_span!(
8651                    target: "fsqlite_vdbe",
8652                    "vdbe_exec",
8653                    opcode_count = 0_u64,
8654                    program_id,
8655                    result_rows,
8656                    elapsed_us = elapsed_us as u64,
8657                );
8658                let _guard = span.enter();
8659                log_statement_done();
8660            } else {
8661                log_statement_done();
8662            }
8663
8664            return Ok(outcome);
8665        }
8666
8667        let mut pc: usize = 0;
8668        // "once" flags: one bit per instruction address (stack-backed for small programs).
8669        let n_ops = ops.len();
8670        let mut once_stack = [0u64; 4]; // covers up to 256 opcodes on the stack
8671        let mut once_heap: Vec<u64> = if n_ops > 256 {
8672            vec![0u64; n_ops.div_ceil(64)]
8673        } else {
8674            Vec::new()
8675        };
8676        let once_bits: &mut [u64] = if n_ops > 256 {
8677            &mut once_heap
8678        } else {
8679            &mut once_stack
8680        };
8681
8682        // bd-perf (V2.3): Bounds check eliminated — all programs terminate via
8683        // OP_Halt which breaks the loop. The sentinel is guaranteed by codegen.
8684        // This saves ~1ns per opcode on the hot path.
8685        let outcome = loop {
8686            // Safety: pc is always in-bounds because:
8687            // 1. Every program ends with Halt (codegen guarantee)
8688            // 2. Goto/If targets are validated at compile time
8689            // 3. pc is only modified by opcode handlers to valid targets
8690            debug_assert!(
8691                pc < ops.len(),
8692                "VDBE pc={pc} out of bounds (len={})",
8693                ops.len()
8694            );
8695            if opcode_count & (VDBE_EXECUTION_CHECKPOINT_INTERVAL - 1) == 0 {
8696                observe_execution_cancellation(&self.execution_cx)?;
8697            }
8698
8699            let op = &ops[pc];
8700            opcode_count += 1;
8701            if collect_vdbe_metrics {
8702                if let Some(local_opcode_execution_totals) = local_opcode_execution_totals.as_mut()
8703                {
8704                    let opcode_idx = usize::from(op.opcode as u8);
8705                    local_opcode_execution_totals[opcode_idx] =
8706                        local_opcode_execution_totals[opcode_idx].saturating_add(1);
8707                }
8708            }
8709            if self.trace_opcodes {
8710                self.trace_opcode(pc, op);
8711            }
8712            if self
8713                .try_execute_hot_opcode(
8714                    op,
8715                    &mut pc,
8716                    collect_vdbe_metrics,
8717                    &mut row_handler,
8718                    borrowed_bindings,
8719                )
8720                .await?
8721            {
8722                continue;
8723            }
8724            #[allow(unreachable_patterns)]
8725            match op.opcode {
8726                // ── Control Flow ────────────────────────────────────────
8727                Opcode::Init => {
8728                    // Jump to p2 if it points to a valid instruction.
8729                    // In the standard SQLite pattern, p2 points to a Goto
8730                    // at the end that bounces back. If p2 points past the
8731                    // end (our codegen pattern), fall through.
8732                    let target = op.p2 as usize;
8733                    if op.p2 > 0 && target < ops.len() {
8734                        pc = target;
8735                        continue;
8736                    }
8737                    pc += 1;
8738                }
8739
8740                Opcode::Goto => {
8741                    pc = op.p2 as usize;
8742                }
8743
8744                Opcode::Halt => {
8745                    if op.p1 != 0 {
8746                        if op.p1 == ErrorCode::Constraint as i32 && op.p5 == OPFLAG_HALT_UNIQUE {
8747                            let columns = match &op.p4 {
8748                                P4::Str(columns) => columns.clone(),
8749                                _ => String::new(),
8750                            };
8751                            return Err(FrankenError::UniqueViolation { columns });
8752                        }
8753                        let msg = match &op.p4 {
8754                            P4::Str(s) => s.clone(),
8755                            _ => format!("halt with error code {}", op.p1),
8756                        };
8757                        break ExecOutcome::Error {
8758                            code: op.p1,
8759                            message: msg,
8760                        };
8761                    }
8762                    break ExecOutcome::Done;
8763                }
8764
8765                Opcode::Noop => {
8766                    pc += 1;
8767                }
8768
8769                Opcode::SetSnapshot => {
8770                    self.execute_set_snapshot_cold(op)?;
8771                    pc += 1;
8772                }
8773
8774                // ── Constants ───────────────────────────────────────────
8775                Opcode::Integer => {
8776                    // Set register p2 to integer value p1.
8777                    self.set_reg_int(op.p2, i64::from(op.p1));
8778                    pc += 1;
8779                }
8780
8781                Opcode::Int64 => {
8782                    let val = match &op.p4 {
8783                        P4::Int64(v) => *v,
8784                        _ => 0,
8785                    };
8786                    self.set_reg_int(op.p2, val);
8787                    pc += 1;
8788                }
8789
8790                Opcode::Real => {
8791                    let val = match &op.p4 {
8792                        P4::Real(v) => *v,
8793                        _ => 0.0,
8794                    };
8795                    self.set_reg_real(op.p2, val);
8796                    pc += 1;
8797                }
8798
8799                Opcode::String8 => {
8800                    // Write text constant to register, reusing the
8801                    // register's existing String buffer when possible.
8802                    match &op.p4 {
8803                        P4::Str(s) => self.write_text_to_reg(op.p2, s),
8804                        _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
8805                    }
8806                    pc += 1;
8807                }
8808
8809                Opcode::String => {
8810                    // p1 = length, p4 = string data. Same as String8 for us.
8811                    match &op.p4 {
8812                        P4::Str(s) => self.write_text_to_reg(op.p2, s),
8813                        _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
8814                    }
8815                    pc += 1;
8816                }
8817
8818                Opcode::Null => {
8819                    // Set registers p2..p3 to NULL.  When p3 == 0 only p2 is
8820                    // set.  p3 is an absolute register number (matching C
8821                    // SQLite where cnt = p3 - p2).
8822                    let start = op.p2;
8823                    let end = if op.p3 > 0 { op.p3 } else { start };
8824                    for r in start..=end {
8825                        self.set_reg_null(r);
8826                    }
8827                    pc += 1;
8828                }
8829
8830                Opcode::SoftNull => {
8831                    self.set_reg_null(op.p1);
8832                    pc += 1;
8833                }
8834
8835                Opcode::Blob => {
8836                    // Write blob constant to register, reusing the
8837                    // register's existing Vec<u8> buffer when possible.
8838                    match &op.p4 {
8839                        P4::Blob(b) => self.write_blob_to_reg(op.p2, b),
8840                        _ => self.set_reg(op.p2, SqliteValue::Blob(Arc::from([] as [u8; 0]))),
8841                    }
8842                    pc += 1;
8843                }
8844
8845                // ── Register Operations ─────────────────────────────────
8846                Opcode::Move => {
8847                    let count = usize::try_from(op.p3).unwrap_or(0);
8848                    self.move_reg_range(op.p1, op.p2, count);
8849                    pc += 1;
8850                }
8851
8852                Opcode::Copy => {
8853                    // Copy registers p1..p1+p3 to p2..p2+p3 (deep copy).
8854                    // p3 = number of ADDITIONAL registers to copy (0 = just one).
8855                    self.copy_reg_range(op.p1, op.p2, op.p3);
8856                    pc += 1;
8857                }
8858
8859                Opcode::SCopy => {
8860                    // Shallow copy register p1 to p2.
8861                    self.copy_single_reg(op.p1, op.p2);
8862                    pc += 1;
8863                }
8864
8865                Opcode::IntCopy => {
8866                    let val = self.get_reg(op.p1).to_integer();
8867                    self.set_reg_int(op.p2, val);
8868                    pc += 1;
8869                }
8870
8871                // ── Result Row ──────────────────────────────────────────
8872                Opcode::ResultRow => {
8873                    self.execute_result_row_hot(op, collect_vdbe_metrics, &mut row_handler)?;
8874                    pc += 1;
8875                }
8876
8877                // ── Arithmetic ──────────────────────────────────────────
8878                Opcode::Add => {
8879                    // p3 = p2 + p1
8880                    let a = self.get_reg(op.p2);
8881                    let b = self.get_reg(op.p1);
8882                    let result = a.sql_add(b);
8883                    self.set_reg_fast(op.p3, result);
8884                    pc += 1;
8885                }
8886
8887                Opcode::Subtract => {
8888                    // p3 = p2 - p1
8889                    let a = self.get_reg(op.p2);
8890                    let b = self.get_reg(op.p1);
8891                    let result = a.sql_sub(b);
8892                    self.set_reg_fast(op.p3, result);
8893                    pc += 1;
8894                }
8895
8896                Opcode::Multiply => {
8897                    // p3 = p2 * p1
8898                    let a = self.get_reg(op.p2);
8899                    let b = self.get_reg(op.p1);
8900                    let result = a.sql_mul(b);
8901                    self.set_reg_fast(op.p3, result);
8902                    pc += 1;
8903                }
8904
8905                Opcode::Divide => {
8906                    // p3 = p2 / p1
8907                    let divisor = self.get_reg(op.p1);
8908                    let dividend = self.get_reg(op.p2);
8909                    let result = sql_div(dividend, divisor);
8910                    self.set_reg_fast(op.p3, result);
8911                    pc += 1;
8912                }
8913
8914                Opcode::Remainder => {
8915                    // p3 = p2 % p1
8916                    let divisor = self.get_reg(op.p1);
8917                    let dividend = self.get_reg(op.p2);
8918                    let result = sql_rem(dividend, divisor);
8919                    self.set_reg_fast(op.p3, result);
8920                    pc += 1;
8921                }
8922
8923                // ── String Concatenation ────────────────────────────────
8924                Opcode::Concat => {
8925                    // Concatenate p1 and p2 into p3.
8926                    // SQLite concat order: result = b || a  (p2 first, then p1).
8927                    let a = self.get_reg(op.p1);
8928                    let b = self.get_reg(op.p2);
8929                    let result = if a.is_null() || b.is_null() {
8930                        SqliteValue::Null
8931                    } else {
8932                        // Fast path: when both are already Text, avoid the
8933                        // to_text() clone+allocation on the second operand.
8934                        match (b, a) {
8935                            (SqliteValue::Text(bs), SqliteValue::Text(as_)) => {
8936                                let mut s = String::with_capacity(bs.len() + as_.len());
8937                                s.push_str(bs);
8938                                s.push_str(as_);
8939                                SqliteValue::Text(s.into())
8940                            }
8941                            (SqliteValue::Text(bs), a_val) => {
8942                                let a_text = a_val.to_text();
8943                                let mut s = String::with_capacity(bs.len() + a_text.len());
8944                                s.push_str(bs);
8945                                s.push_str(&a_text);
8946                                SqliteValue::Text(s.into())
8947                            }
8948                            (b_val, SqliteValue::Text(as_)) => {
8949                                let mut s = b_val.to_text();
8950                                s.push_str(as_);
8951                                SqliteValue::Text(s.into())
8952                            }
8953                            _ => {
8954                                let mut s = b.to_text();
8955                                s.push_str(&a.to_text());
8956                                SqliteValue::Text(s.into())
8957                            }
8958                        }
8959                    };
8960                    self.set_reg_fast(op.p3, result);
8961                    pc += 1;
8962                }
8963
8964                // ── Bitwise ─────────────────────────────────────────────
8965                Opcode::BitAnd => {
8966                    let a = self.get_reg(op.p1);
8967                    let b = self.get_reg(op.p2);
8968                    if a.is_null() || b.is_null() {
8969                        self.set_reg_fast(op.p3, SqliteValue::Null);
8970                    } else {
8971                        self.set_reg_int(op.p3, a.to_integer() & b.to_integer());
8972                    }
8973                    pc += 1;
8974                }
8975
8976                Opcode::BitOr => {
8977                    let a = self.get_reg(op.p1);
8978                    let b = self.get_reg(op.p2);
8979                    if a.is_null() || b.is_null() {
8980                        self.set_reg_fast(op.p3, SqliteValue::Null);
8981                    } else {
8982                        self.set_reg_int(op.p3, a.to_integer() | b.to_integer());
8983                    }
8984                    pc += 1;
8985                }
8986
8987                Opcode::ShiftLeft => {
8988                    let a = self.get_reg(op.p1);
8989                    let b = self.get_reg(op.p2);
8990                    if a.is_null() || b.is_null() {
8991                        self.set_reg_fast(op.p3, SqliteValue::Null);
8992                    } else {
8993                        let result = sql_shift_left(b.to_integer(), a.to_integer());
8994                        self.set_reg_fast(op.p3, result);
8995                    }
8996                    pc += 1;
8997                }
8998
8999                Opcode::ShiftRight => {
9000                    let a = self.get_reg(op.p1);
9001                    let b = self.get_reg(op.p2);
9002                    if a.is_null() || b.is_null() {
9003                        self.set_reg_fast(op.p3, SqliteValue::Null);
9004                    } else {
9005                        let result = sql_shift_right(b.to_integer(), a.to_integer());
9006                        self.set_reg_fast(op.p3, result);
9007                    }
9008                    pc += 1;
9009                }
9010
9011                Opcode::BitNot => {
9012                    // p2 = ~p1
9013                    let a = self.get_reg(op.p1);
9014                    if a.is_null() {
9015                        self.set_reg_fast(op.p2, SqliteValue::Null);
9016                    } else {
9017                        self.set_reg_int(op.p2, !a.to_integer());
9018                    }
9019                    pc += 1;
9020                }
9021
9022                // ── Type Conversion ─────────────────────────────────────
9023                Opcode::AddImm => {
9024                    // Add integer p2 to register p1.
9025                    let val = self
9026                        .get_reg(op.p1)
9027                        .to_integer()
9028                        .wrapping_add(i64::from(op.p2));
9029                    self.set_reg_int(op.p1, val);
9030                    pc += 1;
9031                }
9032
9033                Opcode::Cast => {
9034                    // Cast register p1 to type indicated by p2.
9035                    let val = self.take_reg(op.p1);
9036                    if collect_vdbe_metrics {
9037                        let casted = sql_cast(val.clone(), op.p2);
9038                        record_type_coercion(&val, &casted);
9039                        self.set_reg_fast(op.p1, casted);
9040                    } else {
9041                        let casted = sql_cast(val, op.p2);
9042                        self.set_reg_fast(op.p1, casted);
9043                    }
9044                    pc += 1;
9045                }
9046
9047                Opcode::MustBeInt => {
9048                    let val = self.take_reg(op.p1);
9049                    let coerced = val.apply_affinity(fsqlite_types::TypeAffinity::Integer);
9050                    let is_int = coerced.as_integer().is_some();
9051                    self.set_reg_fast(op.p1, coerced);
9052                    if is_int {
9053                        pc += 1;
9054                    } else {
9055                        if op.p2 > 0 {
9056                            pc = op.p2 as usize;
9057                            continue;
9058                        }
9059                        return Err(FrankenError::TypeMismatch {
9060                            expected: "integer".to_owned(),
9061                            actual: self.get_reg(op.p1).typeof_str().to_owned(),
9062                        });
9063                    }
9064                }
9065
9066                #[allow(clippy::cast_precision_loss)]
9067                Opcode::RealAffinity => {
9068                    if let SqliteValue::Integer(i) = self.get_reg(op.p1) {
9069                        let i_val = *i;
9070                        let f = i_val as f64;
9071                        if collect_vdbe_metrics {
9072                            record_type_coercion(
9073                                &SqliteValue::Integer(i_val),
9074                                &SqliteValue::Float(f),
9075                            );
9076                        }
9077                        self.set_reg_fast(op.p1, SqliteValue::Float(f));
9078                    }
9079                    pc += 1;
9080                }
9081
9082                // ── Comparison Jumps ────────────────────────────────────
9083                Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
9084                    let lhs = self.get_reg(op.p3);
9085                    let rhs = self.get_reg(op.p1);
9086                    let store_p2 = (op.p5 & 0x20) != 0; // SQLITE_STOREP2
9087
9088                    if lhs.is_null() || rhs.is_null() {
9089                        let null_eq = (op.p5 & 0x80) != 0;
9090                        if null_eq {
9091                            // IS / IS NOT semantics: NULL == NULL is true.
9092                            let both_null = lhs.is_null() && rhs.is_null();
9093                            let should_jump = match op.opcode {
9094                                Opcode::Eq => both_null,
9095                                Opcode::Ne => !both_null,
9096                                _ => false,
9097                            };
9098                            if store_p2 {
9099                                self.set_reg_int(op.p2, i64::from(should_jump));
9100                                pc += 1;
9101                            } else if should_jump {
9102                                pc = op.p2 as usize;
9103                            } else {
9104                                pc += 1;
9105                            }
9106                        } else if store_p2 {
9107                            // STOREP2 with NULL: store NULL in P2.
9108                            self.set_reg_fast(op.p2, SqliteValue::Null);
9109                            pc += 1;
9110                        } else {
9111                            // JUMPIFNULL (0x10): jump to P2 when either is NULL.
9112                            let jump_if_null = (op.p5 & 0x10) != 0;
9113                            if jump_if_null {
9114                                pc = op.p2 as usize;
9115                            } else {
9116                                pc += 1;
9117                            }
9118                        }
9119                    } else {
9120                        // Same-storage values can bypass affinity coercion only
9121                        // when P5 cannot change that storage class. In
9122                        // particular, TEXT P5 must stringify numeric values and
9123                        // NUMERIC P5 must attempt to coerce each TEXT value.
9124                        let cmp = if let Some(cmp) =
9125                            fast_compare_same_storage_class(lhs, rhs, &op.p4, op.p5)
9126                        {
9127                            cmp
9128                        } else {
9129                            // General path: affinity coercion + collation.
9130                            let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, op.p5);
9131                            if let P4::Collation(ref coll_name) = op.p4 {
9132                                let coll = self.lock_collation();
9133                                collate_compare(&cmp_lhs, &cmp_rhs, coll_name, &coll)
9134                            } else {
9135                                cmp_lhs.partial_cmp(&cmp_rhs)
9136                            }
9137                        };
9138                        let should_jump = matches!(
9139                            (op.opcode, cmp),
9140                            (Opcode::Eq, Some(std::cmp::Ordering::Equal))
9141                                | (Opcode::Lt, Some(std::cmp::Ordering::Less))
9142                                | (
9143                                    Opcode::Le,
9144                                    Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
9145                                )
9146                                | (Opcode::Gt, Some(std::cmp::Ordering::Greater))
9147                                | (
9148                                    Opcode::Ge,
9149                                    Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
9150                                )
9151                        ) || matches!(
9152                            (op.opcode, cmp),
9153                            (Opcode::Ne, Some(ord)) if ord != std::cmp::Ordering::Equal
9154                        );
9155
9156                        if store_p2 {
9157                            if cmp.is_none() {
9158                                // Indeterminate comparison (e.g., NaN): store NULL, not 0.
9159                                self.set_reg_fast(op.p2, SqliteValue::Null);
9160                            } else {
9161                                self.set_reg_int(op.p2, i64::from(should_jump));
9162                            }
9163                            pc += 1;
9164                        } else if should_jump {
9165                            pc = op.p2 as usize;
9166                        } else {
9167                            pc += 1;
9168                        }
9169                    }
9170                }
9171
9172                // ── Boolean Logic ───────────────────────────────────────
9173                Opcode::And => {
9174                    // Three-valued AND: p3 = p1 AND p2
9175                    let a = self.get_reg(op.p1);
9176                    let b = self.get_reg(op.p2);
9177                    let result = sql_and(a, b);
9178                    self.set_reg_fast(op.p3, result);
9179                    pc += 1;
9180                }
9181
9182                Opcode::Or => {
9183                    // Three-valued OR: p3 = p1 OR p2
9184                    let a = self.get_reg(op.p1);
9185                    let b = self.get_reg(op.p2);
9186                    let result = sql_or(a, b);
9187                    self.set_reg_fast(op.p3, result);
9188                    pc += 1;
9189                }
9190
9191                Opcode::Not => {
9192                    // p2 = NOT p1
9193                    let a = self.get_reg(op.p1);
9194                    if a.is_null() {
9195                        self.set_reg_fast(op.p2, SqliteValue::Null);
9196                    } else {
9197                        self.set_reg_int(op.p2, i64::from(!vdbe_real_is_truthy(a)));
9198                    }
9199                    pc += 1;
9200                }
9201
9202                // ── Conditional Jumps ───────────────────────────────────
9203                Opcode::If => {
9204                    // Jump to p2 if p1 is true.
9205                    // If p1 is NULL, jump iff p3 != 0 (SQLite semantics).
9206                    let val = self.get_reg(op.p1);
9207                    let should_jump = if val.is_null() {
9208                        op.p3 != 0
9209                    } else {
9210                        vdbe_real_is_truthy(val)
9211                    };
9212                    if should_jump {
9213                        pc = op.p2 as usize;
9214                    } else {
9215                        pc += 1;
9216                    }
9217                }
9218
9219                Opcode::IfNot => {
9220                    // Jump to p2 if p1 is false (zero).
9221                    // If p1 is NULL, jump iff p3 != 0 (SQLite semantics).
9222                    let val = self.get_reg(op.p1);
9223                    let should_jump = if val.is_null() {
9224                        op.p3 != 0
9225                    } else {
9226                        !vdbe_real_is_truthy(val)
9227                    };
9228                    if should_jump {
9229                        pc = op.p2 as usize;
9230                    } else {
9231                        pc += 1;
9232                    }
9233                }
9234
9235                Opcode::IsNull => {
9236                    // Jump to p2 if p1 is NULL.
9237                    if self.get_reg(op.p1).is_null() {
9238                        pc = op.p2 as usize;
9239                    } else {
9240                        pc += 1;
9241                    }
9242                }
9243
9244                Opcode::NotNull => {
9245                    // Jump to p2 if p1 is NOT NULL.
9246                    if self.get_reg(op.p1).is_null() {
9247                        pc += 1;
9248                    } else {
9249                        pc = op.p2 as usize;
9250                    }
9251                }
9252
9253                Opcode::Once => {
9254                    // Fall through on first execution (run the body), jump to
9255                    // p2 on subsequent executions (skip the body).  This
9256                    // matches C SQLite's OP_Once semantics.
9257                    let word = pc / 64;
9258                    let bit = 1u64 << (pc % 64);
9259                    if once_bits[word] & bit != 0 {
9260                        // Already fired — skip the body.
9261                        pc = op.p2 as usize;
9262                    } else {
9263                        // First time — mark as fired, fall through.
9264                        once_bits[word] |= bit;
9265                        pc += 1;
9266                    }
9267                }
9268
9269                // ── Gosub / Return ──────────────────────────────────────
9270                Opcode::Gosub => {
9271                    // Store return address in p1, jump to p2.
9272                    let return_addr = (pc + 1) as i32;
9273                    self.set_reg(op.p1, SqliteValue::Integer(i64::from(return_addr)));
9274                    pc = op.p2 as usize;
9275                }
9276
9277                Opcode::Return => {
9278                    // Jump to address stored in p1.
9279                    let addr = self.get_reg(op.p1).to_integer();
9280                    if addr < 0 || addr as usize >= ops.len() {
9281                        return Err(FrankenError::Internal(format!(
9282                            "Return address {} out of bounds",
9283                            addr
9284                        )));
9285                    }
9286                    pc = addr as usize;
9287                }
9288
9289                // ── Transaction / AutoCommit / TableLock ────────────────
9290                //
9291                // In C SQLite, Transaction opens a read or write transaction
9292                // via the pager, AutoCommit commits/rollbacks, and TableLock
9293                // acquires table-level schema locks. In FrankenSQLite, the
9294                // Connection layer manages transaction lifecycle externally
9295                // (wrapping VDBE execution in BEGIN/COMMIT), so these opcodes
9296                // validate intent rather than performing the lifecycle step.
9297                //
9298                // Transaction: p1=db number, p2=1 for write txn, p3=schema version
9299                // AutoCommit: p1=1 for rollback/0 for commit, p2=auto-commit flag
9300                // TableLock: p1=db, p2=root page, p3=1 for write, p4=table name
9301                Opcode::Transaction => {
9302                    let _db_number = op.p1;
9303                    let is_write = op.p2 != 0;
9304                    if is_write && self.txn_page_io.is_some() {
9305                        // Record that this program requires write access.
9306                        // The Connection layer should have already opened a
9307                        // write transaction; if not, downstream write opcodes
9308                        // (Insert, Delete) will fail with appropriate errors.
9309                        tracing::trace!(
9310                            target: "fsqlite.vdbe",
9311                            opcode = "Transaction",
9312                            db = _db_number,
9313                            write = is_write,
9314                            "write transaction intent acknowledged"
9315                        );
9316                    }
9317                    pc += 1;
9318                }
9319                Opcode::AutoCommit => {
9320                    // AutoCommit is handled by the Connection layer after
9321                    // VDBE execution completes. The opcode is a no-op here
9322                    // because the Connection checks the final program counter
9323                    // and transaction state to determine commit/rollback.
9324                    pc += 1;
9325                }
9326                Opcode::TableLock => {
9327                    // Table-level locks are subsumed by MVCC page-level
9328                    // locking. Schema stability is guaranteed by the schema
9329                    // cookie check (ReadCookie/SetCookie opcodes). No
9330                    // additional locking is needed here.
9331                    pc += 1;
9332                }
9333
9334                // ── Cookie operations (bd-3mmj) ────────────────────────
9335                //
9336                // ReadCookie: P1=db, P2=dest register, P3=cookie number
9337                //   cookie 1 = schema_cookie (offset 40 in header)
9338                // SetCookie: P1=db, P2=cookie number, P3=new value
9339                Opcode::ReadCookie => {
9340                    let dest_reg = op.p2;
9341                    let cookie_num = op.p3;
9342                    let value = match cookie_num {
9343                        // Cookie 1 = BTREE_SCHEMA_VERSION (schema cookie)
9344                        1 => i64::from(self.schema_cookie),
9345                        // Other cookies return 0 for now.
9346                        _ => 0,
9347                    };
9348                    self.set_reg(dest_reg, SqliteValue::Integer(value));
9349                    pc += 1;
9350                }
9351                Opcode::SetCookie => {
9352                    let cookie_num = op.p2;
9353                    let new_value = op.p3;
9354                    if cookie_num == 1 {
9355                        #[allow(clippy::cast_sign_loss)]
9356                        {
9357                            self.schema_cookie = new_value as u32;
9358                        }
9359                    }
9360                    // Other cookie numbers are silently ignored for now.
9361                    pc += 1;
9362                }
9363
9364                // ── Cursor operations ─────────────────────────────────
9365                Opcode::OpenRead => {
9366                    // bd-1xrs: StorageCursor is now the ONLY cursor path.
9367                    // No MemCursor fallback - open_storage_cursor must succeed.
9368                    if op.p3 > 1 {
9369                        // Bead bd-bldc5.1.7 (T7): ATTACH DATABASE support.
9370                        // p3=0 is main db, p3=1 is temp db. p3>1 means an
9371                        // ATTACHed database, which requires multi-database pager
9372                        // management (Connection maintains a map of name→pager,
9373                        // VDBE looks up pager by db number). ATTACH is used for
9374                        // cross-database queries, migration, and backup.
9375                        return Err(FrankenError::NotImplemented(format!(
9376                            "ATTACH DATABASE (db number {}) not yet supported — \
9377                                 use the main database or :memory:",
9378                            op.p3
9379                        )));
9380                    }
9381                    let cursor_id = op.p1;
9382                    let root_page = op.p2;
9383                    self.pending_next_after_delete.remove(&cursor_id);
9384                    if op.p3 == 1 {
9385                        let has_temp_root = self
9386                            .db
9387                            .as_ref()
9388                            .is_some_and(|db| db.get_table(root_page).is_some());
9389                        if !has_temp_root {
9390                            return Err(FrankenError::Internal(format!(
9391                                "OpenRead failed: TEMP root page {root_page} is not attached"
9392                            )));
9393                        }
9394                        self.storage_cursors.remove(&cursor_id);
9395                        self.cursors
9396                            .insert(cursor_id, MemCursor::new(root_page, false));
9397                        self.cursor_root_pages.insert(cursor_id, root_page);
9398                        pc += 1;
9399                        continue;
9400                    }
9401                    if !self.open_storage_cursor(cursor_id, root_page, false).await {
9402                        return Err(FrankenError::Internal(format!(
9403                            "OpenRead failed: could not open storage cursor on root page {root_page}"
9404                        )));
9405                    }
9406                    self.cursor_root_pages.insert(cursor_id, root_page);
9407                    self.cursors.remove(&cursor_id);
9408                    pc += 1;
9409                }
9410                Opcode::OpenWrite => {
9411                    // bd-1xrs: StorageCursor is now the ONLY cursor path.
9412                    // No MemCursor fallback - open_storage_cursor must succeed.
9413                    if op.p3 > 1 {
9414                        // Bead bd-bldc5.1.7 (T7): ATTACH DATABASE support.
9415                        // p3=0 is main db, p3=1 is temp db. p3>1 means an
9416                        // ATTACHed database, which requires multi-database pager
9417                        // management (Connection maintains a map of name→pager,
9418                        // VDBE looks up pager by db number). ATTACH is used for
9419                        // cross-database queries, migration, and backup.
9420                        return Err(FrankenError::NotImplemented(format!(
9421                            "ATTACH DATABASE (db number {}) not yet supported — \
9422                                 use the main database or :memory:",
9423                            op.p3
9424                        )));
9425                    }
9426                    let cursor_id = op.p1;
9427                    let root_page = op.p2;
9428                    self.pending_next_after_delete.remove(&cursor_id);
9429                    if op.p3 == 1 {
9430                        let has_temp_root = self
9431                            .db
9432                            .as_ref()
9433                            .is_some_and(|db| db.get_table(root_page).is_some());
9434                        if !has_temp_root {
9435                            return Err(FrankenError::Internal(format!(
9436                                "OpenWrite failed: TEMP root page {root_page} is not attached"
9437                            )));
9438                        }
9439                        self.storage_cursors.remove(&cursor_id);
9440                        self.cursors
9441                            .insert(cursor_id, MemCursor::new(root_page, true));
9442                        self.cursor_root_pages.insert(cursor_id, root_page);
9443                        pc += 1;
9444                        continue;
9445                    }
9446                    if !self.open_storage_cursor(cursor_id, root_page, true).await {
9447                        return Err(FrankenError::Internal(format!(
9448                            "OpenWrite failed: could not open storage cursor on root page {root_page}"
9449                        )));
9450                    }
9451                    self.cursor_root_pages.insert(cursor_id, root_page);
9452                    self.cursors.remove(&cursor_id);
9453                    pc += 1;
9454                }
9455
9456                Opcode::OpenEphemeral => {
9457                    // Ephemeral table: create an in-memory table on-the-fly.
9458                    let cursor_id = op.p1;
9459                    self.pending_next_after_delete.remove(&cursor_id);
9460                    let num_cols = op.p2.max(1);
9461                    if let Some(db) = self.db.as_mut() {
9462                        let root_page = db.create_table(num_cols as usize);
9463                        self.storage_cursors.remove(&cursor_id);
9464                        self.cursors
9465                            .insert(cursor_id, MemCursor::new(root_page, true));
9466                    }
9467                    pc += 1;
9468                }
9469
9470                Opcode::OpenAutoindex => {
9471                    // Autoindex: create an ephemeral INDEX B-tree.
9472                    // Unlike OpenEphemeral (table B-tree), autoindexes use
9473                    // index B-tree semantics (no rowid, key-only cells).
9474                    // We create a StorageCursor backed by MemPageStore with
9475                    // is_table=false so IdxInsert/IdxGE etc. work correctly.
9476                    let autoindex_page_size = self.page_size.get();
9477                    let cursor_id = op.p1;
9478                    self.pending_next_after_delete.remove(&cursor_id);
9479                    let root_pgno = if let Some(db) = self.db.as_mut() {
9480                        let rp = db.allocate_root_page();
9481                        PageNumber::new(rp as u32)
9482                    } else {
9483                        PageNumber::new(2)
9484                    };
9485                    if let Some(root_pgno) = root_pgno {
9486                        let store = MemPageStore::with_empty_index(root_pgno, autoindex_page_size);
9487                        let cx = self.derive_execution_cx();
9488                        let mut cursor =
9489                            BtCursor::new(store, root_pgno, autoindex_page_size, false);
9490                        let autoindex_collations = extract_collation_names_owned(&op.p4);
9491                        if !autoindex_collations.is_empty() {
9492                            cursor.set_index_collation_context(
9493                                autoindex_collations,
9494                                Arc::clone(&self.collation_registry),
9495                            );
9496                        }
9497                        self.cursor_root_pages
9498                            .insert(cursor_id, root_pgno.get() as i32);
9499                        self.cursors.remove(&cursor_id);
9500                        self.storage_cursors.insert(
9501                            cursor_id,
9502                            StorageCursor {
9503                                cursor: CursorBackend::Mem(cursor),
9504                                cx,
9505                                writable: true,
9506                                root_page: root_pgno.get() as i32,
9507                                rowid_mode: RowIdMode::Normal,
9508                                autoincrement_high_water: 0,
9509                                last_alloc_rowid: 0,
9510                                payload_buf: Vec::new(),
9511                                target_vals_buf: Vec::new(),
9512                                cur_vals_buf: Vec::new(),
9513                                row_decode: RowDecodeScratch::default(),
9514                                last_position_stamp: None,
9515                                last_successful_insert_rowid: None,
9516                                last_rightmost_unique_index_prefix: None,
9517                                last_rightmost_unique_index_position: None,
9518                                cached_rowid: None,
9519                                payload_includes_rowid_alias: None,
9520                                ipk_col_idx: None,
9521                                table_column_count: None,
9522                                first_not_null_non_ipk_col: None,
9523                            },
9524                        );
9525                    }
9526                    pc += 1;
9527                }
9528
9529                Opcode::OpenPseudo => {
9530                    let cursor_id = op.p1;
9531                    self.pending_next_after_delete.remove(&cursor_id);
9532                    self.storage_cursors.remove(&cursor_id);
9533                    self.cursors.insert(cursor_id, MemCursor::new_pseudo(op.p2));
9534                    pc += 1;
9535                }
9536
9537                Opcode::OpenDup | Opcode::ReopenIdx => {
9538                    // Reopen: reuse existing cursor configuration.
9539                    pc += 1;
9540                }
9541
9542                Opcode::SorterOpen => {
9543                    let cursor_id = op.p1;
9544                    self.pending_next_after_delete.remove(&cursor_id);
9545                    let key_columns = usize::try_from(op.p2.max(1)).unwrap_or(1);
9546                    let top_n_from_register = (op.p5 & SORTER_OPEN_TOP_N_REGISTER) != 0;
9547                    let top_n_bound = if top_n_from_register {
9548                        self.get_reg(op.p3).to_integer()
9549                    } else {
9550                        i64::from(op.p3)
9551                    };
9552                    let top_n_limit = usize::try_from(top_n_bound)
9553                        .ok()
9554                        .filter(|limit| top_n_from_register || *limit > 0);
9555                    // P4::Str format: ORDER_CHARS or ORDER_CHARS|COLL1,COLL2,...
9556                    // where ORDER_CHARS are '+'/'-' per key column,
9557                    // and COLL values are collation names (empty = BINARY).
9558                    let (order_str, collation_str) = match &op.p4 {
9559                        P4::Str(s) => {
9560                            if let Some((orders, colls)) = s.split_once('|') {
9561                                (orders.to_owned(), Some(colls.to_owned()))
9562                            } else {
9563                                (s.clone(), None)
9564                            }
9565                        }
9566                        _ => (String::new(), None),
9567                    };
9568                    let sort_key_orders: Vec<SortKeyOrder> = order_str
9569                        .chars()
9570                        .take(key_columns)
9571                        .map(|ch| match ch {
9572                            '-' => SortKeyOrder::Desc,
9573                            '>' => SortKeyOrder::AscNullsLast,
9574                            '<' => SortKeyOrder::DescNullsFirst,
9575                            _ => SortKeyOrder::Asc,
9576                        })
9577                        .collect();
9578                    let collations: Vec<Option<String>> = if let Some(cs) = collation_str {
9579                        cs.split(',')
9580                            .take(key_columns)
9581                            .map(|c| {
9582                                if c.is_empty() {
9583                                    None
9584                                } else {
9585                                    Some(c.to_owned())
9586                                }
9587                            })
9588                            .collect()
9589                    } else {
9590                        Vec::new()
9591                    };
9592                    self.sorters.insert(
9593                        cursor_id,
9594                        SorterCursor::with_collation_registry(
9595                            key_columns,
9596                            sort_key_orders,
9597                            collations,
9598                            Arc::clone(&self.collation_registry),
9599                            top_n_limit,
9600                        ),
9601                    );
9602                    // A cursor id cannot be both table and sorter cursor.
9603                    self.cursors.remove(&cursor_id);
9604                    self.storage_cursors.remove(&cursor_id);
9605                    pc += 1;
9606                }
9607
9608                Opcode::Close => {
9609                    self.cursors.remove(&op.p1);
9610                    if self.retain_storage_cursors_on_close {
9611                        if let Some(sc) = self.storage_cursors.get_mut(&op.p1) {
9612                            Self::clear_storage_cursor_statement_state(sc);
9613                        }
9614                    } else {
9615                        self.storage_cursors.remove(&op.p1);
9616                    }
9617                    self.sorters.remove(&op.p1);
9618                    if let Some(cold_state) = self.cold_state_mut() {
9619                        cold_state.vtab_cursors.remove(&op.p1);
9620                    }
9621                    self.pending_next_after_delete.remove(&op.p1);
9622                    pc += 1;
9623                }
9624
9625                Opcode::ColumnsUsed => {
9626                    pc += 1;
9627                }
9628
9629                Opcode::Rewind | Opcode::Sort | Opcode::SorterSort => {
9630                    // Position cursor at the first row. Jump to p2 if empty.
9631                    let cursor_id = op.p1;
9632                    // Rewind repositions the cursor, so clear any pending delete state.
9633                    self.pending_next_after_delete.remove(&cursor_id);
9634                    let is_empty = if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
9635                        if matches!(op.opcode, Opcode::Sort | Opcode::SorterSort) {
9636                            sorter.sort()?;
9637                            // Flush per-sorter metrics to global counters.
9638                            let rows = sorter.rows_sorted_total;
9639                            let spill_pages = sorter.spill_pages_total;
9640                            let merge_runs = sorter.spill_runs.len() as u64;
9641                            if collect_vdbe_metrics {
9642                                FSQLITE_SORT_ROWS_TOTAL.fetch_add(rows, AtomicOrdering::Relaxed);
9643                                FSQLITE_SORT_SPILL_PAGES_TOTAL
9644                                    .fetch_add(spill_pages, AtomicOrdering::Relaxed);
9645                            }
9646                            sorter.rows_sorted_total = 0;
9647                            sorter.spill_pages_total = 0;
9648                            // Tracing span for sort observability.
9649                            let _span = tracing::debug_span!(
9650                                "sort",
9651                                rows_sorted = rows,
9652                                spill_pages = spill_pages,
9653                                merge_runs = merge_runs,
9654                            )
9655                            .entered();
9656                            tracing::debug!(
9657                                rows_sorted = rows,
9658                                spill_pages = spill_pages,
9659                                merge_runs = merge_runs,
9660                                "sort completed"
9661                            );
9662                        }
9663                        if sorter.rows.is_empty() {
9664                            sorter.position = None;
9665                            true
9666                        } else {
9667                            sorter.position = Some(0);
9668                            false
9669                        }
9670                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9671                        if cursor.is_pseudo {
9672                            cursor.pseudo_row.is_none()
9673                        } else if let Some(db) = self.db.as_ref() {
9674                            if let Some(table) = db.get_table(cursor.root_page) {
9675                                if table.rows.is_empty() {
9676                                    true
9677                                } else {
9678                                    cursor.position = Some(0);
9679                                    false
9680                                }
9681                            } else {
9682                                true
9683                            }
9684                        } else {
9685                            true
9686                        }
9687                    } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9688                        !cursor.cursor.first(&cursor.cx).await?
9689                    } else {
9690                        true
9691                    };
9692                    if is_empty {
9693                        pc = op.p2 as usize;
9694                    } else {
9695                        pc += 1;
9696                    }
9697                }
9698
9699                Opcode::Last => {
9700                    // Position cursor at the last row. Jump to p2 if empty.
9701                    let cursor_id = op.p1;
9702                    // Last repositions the cursor, so clear any pending delete state.
9703                    self.pending_next_after_delete.remove(&cursor_id);
9704                    let is_empty = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9705                        !cursor.cursor.last(&cursor.cx).await?
9706                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9707                        if cursor.is_pseudo {
9708                            cursor.pseudo_row.is_none()
9709                        } else if let Some(db) = self.db.as_ref() {
9710                            if let Some(table) = db.get_table(cursor.root_page) {
9711                                if table.rows.is_empty() {
9712                                    true
9713                                } else {
9714                                    cursor.position = Some(table.rows.len() - 1);
9715                                    false
9716                                }
9717                            } else {
9718                                true
9719                            }
9720                        } else {
9721                            true
9722                        }
9723                    } else {
9724                        true
9725                    };
9726                    if is_empty {
9727                        pc = op.p2 as usize;
9728                    } else {
9729                        pc += 1;
9730                    }
9731                }
9732
9733                Opcode::Next | Opcode::SorterNext => {
9734                    // Advance cursor to the next row. Jump to p2 if more rows.
9735                    let cursor_id = op.p1;
9736                    let has_next = if !self.pending_next_after_delete.is_empty()
9737                        && self.pending_next_after_delete.remove(&cursor_id)
9738                    {
9739                        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9740                            !cursor.cursor.eof()
9741                        } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9742                            if cursor.is_pseudo {
9743                                false
9744                            } else if let Some(pos) = cursor.position {
9745                                if let Some(table) = self
9746                                    .db
9747                                    .as_ref()
9748                                    .and_then(|db| db.get_table(cursor.root_page))
9749                                {
9750                                    if pos < table.rows.len() {
9751                                        true
9752                                    } else {
9753                                        cursor.position = None;
9754                                        false
9755                                    }
9756                                } else {
9757                                    false
9758                                }
9759                            } else {
9760                                false
9761                            }
9762                        } else {
9763                            false
9764                        }
9765                    } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9766                        cursor.cursor.next(&cursor.cx).await?
9767                    } else if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
9768                        if let Some(pos) = sorter.position {
9769                            let next = pos + 1;
9770                            if next < sorter.rows.len() {
9771                                sorter.position = Some(next);
9772                                true
9773                            } else {
9774                                sorter.position = None;
9775                                false
9776                            }
9777                        } else {
9778                            false
9779                        }
9780                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9781                        if cursor.is_pseudo {
9782                            false
9783                        } else if let Some(db) = self.db.as_ref() {
9784                            if let Some(table) = db.get_table(cursor.root_page) {
9785                                if let Some(pos) = cursor.position {
9786                                    let next = pos + 1;
9787                                    if next < table.rows.len() {
9788                                        cursor.position = Some(next);
9789                                        true
9790                                    } else {
9791                                        cursor.position = None;
9792                                        false
9793                                    }
9794                                } else {
9795                                    false
9796                                }
9797                            } else {
9798                                false
9799                            }
9800                        } else {
9801                            false
9802                        }
9803                    } else {
9804                        false
9805                    };
9806                    if has_next {
9807                        pc = op.p2 as usize;
9808                    } else {
9809                        pc += 1;
9810                    }
9811                }
9812
9813                Opcode::Prev => {
9814                    // Move cursor backward. Jump to p2 if more rows.
9815                    let cursor_id = op.p1;
9816                    // Prev repositions the cursor, so clear any pending
9817                    // delete/next state before evaluating movement.
9818                    if !self.pending_next_after_delete.is_empty() {
9819                        self.pending_next_after_delete.remove(&cursor_id);
9820                    }
9821                    let has_prev = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9822                        cursor.cursor.prev(&cursor.cx).await?
9823                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9824                        if let Some(pos) = cursor.position {
9825                            if pos > 0 {
9826                                cursor.position = Some(pos - 1);
9827                                true
9828                            } else {
9829                                cursor.position = None;
9830                                false
9831                            }
9832                        } else {
9833                            false
9834                        }
9835                    } else {
9836                        false
9837                    };
9838                    if has_prev {
9839                        pc = op.p2 as usize;
9840                    } else {
9841                        pc += 1;
9842                    }
9843                }
9844
9845                Opcode::Column => {
9846                    // Read column p2 from cursor p1 into register p3.
9847                    //
9848                    // Fast path for storage cursors: write from the lazy
9849                    // column cache directly into the target register,
9850                    // reusing existing Text/Blob buffer capacity.  This
9851                    // eliminates the alloc+dealloc cycle that the
9852                    // cursor_column→clone→set_reg path requires for
9853                    // every text/blob column on every row.
9854                    let cursor_id = op.p1;
9855                    let col_idx = op.p2 as usize;
9856                    let target = op.p3;
9857                    if !self
9858                        .column_to_reg_direct(cursor_id, col_idx, target)
9859                        .await?
9860                    {
9861                        let val = self.cursor_column(cursor_id, col_idx).await?;
9862                        self.set_reg_fast(target, val);
9863                    }
9864                    pc += 1;
9865                }
9866
9867                Opcode::Rowid => {
9868                    // Get rowid from cursor p1 into register p2.
9869                    let cursor_id = op.p1;
9870                    let target = op.p2;
9871                    let val = self.cursor_rowid(cursor_id).await?;
9872                    self.set_reg_fast(target, val);
9873                    pc += 1;
9874                }
9875
9876                Opcode::RowData => {
9877                    // Store raw row data as a blob in register p2.
9878                    let cursor_id = op.p1;
9879                    let target = op.p2;
9880                    if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
9881                        if cursor.cursor.eof() {
9882                            self.set_reg_fast(target, SqliteValue::Null);
9883                        } else {
9884                            let payload = cursor.cursor.payload(&cursor.cx).await?;
9885                            self.set_reg_fast(target, SqliteValue::Blob(payload.into()));
9886                        }
9887                    } else if let Some(cursor) = self.cursors.get(&cursor_id) {
9888                        if cursor.is_pseudo {
9889                            if let Some(reg) = cursor.pseudo_reg {
9890                                let blob = self.get_reg(reg).clone();
9891                                self.set_reg_fast(target, blob);
9892                            } else {
9893                                self.set_reg_fast(target, SqliteValue::Null);
9894                            }
9895                        } else {
9896                            self.set_reg_fast(target, SqliteValue::Null);
9897                        }
9898                    } else {
9899                        self.set_reg_fast(target, SqliteValue::Null);
9900                    }
9901                    pc += 1;
9902                }
9903
9904                Opcode::NullRow => {
9905                    // Set cursor p1 to a null row. Subsequent Column/Rowid
9906                    // reads will return NULL (storage cursor via eof(),
9907                    // mem cursor via position=None, vtab cursor via
9908                    // synthetic null-row state).
9909                    if let Some(cursor) = self.storage_cursors.get_mut(&op.p1) {
9910                        // Move storage cursor past the last entry so eof()
9911                        // returns true for subsequent Column/Rowid reads.
9912                        cursor.cursor.clear_position();
9913                    }
9914                    if let Some(cursor) = self.cursors.get_mut(&op.p1) {
9915                        cursor.position = None;
9916                    }
9917                    if let Some(state) = self
9918                        .cold_state_mut()
9919                        .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&op.p1))
9920                    {
9921                        state.null_row = true;
9922                    }
9923                    pc += 1;
9924                }
9925
9926                Opcode::Offset => {
9927                    self.set_reg_fast(op.p3, SqliteValue::Null);
9928                    pc += 1;
9929                }
9930
9931                // ── Seek operations (in-memory) ─────────────────────────
9932                Opcode::SeekRowid => {
9933                    // Seek cursor p1 to the row with rowid in register p3.
9934                    // If not found, jump to p2.  NULL key → not found.
9935                    let cursor_id = op.p1;
9936                    // Seek repositions the cursor, so clear any pending delete state.
9937                    self.pending_next_after_delete.remove(&cursor_id);
9938                    let key = self.get_reg(op.p3);
9939                    if key.is_null() {
9940                        pc = op.p2 as usize;
9941                        continue;
9942                    }
9943                    let rowid_val = key.to_integer();
9944                    let use_rowset_advance = pc > 0
9945                        && ops.get(pc - 1).is_some_and(|prev| {
9946                            prev.opcode == Opcode::RowSetRead && prev.p3 == op.p3
9947                        });
9948                    let found = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9949                        let seek_result = if use_rowset_advance {
9950                            cursor
9951                                .cursor
9952                                .table_advance_to(&cursor.cx, rowid_val)
9953                                .await?
9954                        } else {
9955                            cursor.cursor.table_move_to(&cursor.cx, rowid_val).await?
9956                        };
9957                        seek_result.is_found()
9958                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9959                        if let Some(db) = self.db.as_ref() {
9960                            if let Some(table) = db.get_table(cursor.root_page) {
9961                                if let Some(idx) = table.find_by_rowid(rowid_val) {
9962                                    cursor.position = Some(idx);
9963                                    true
9964                                } else {
9965                                    false
9966                                }
9967                            } else {
9968                                false
9969                            }
9970                        } else {
9971                            false
9972                        }
9973                    } else {
9974                        false
9975                    };
9976                    if found {
9977                        pc += 1;
9978                    } else {
9979                        pc = op.p2 as usize;
9980                    }
9981                }
9982
9983                Opcode::SeekGE | Opcode::SeekGT | Opcode::SeekLE | Opcode::SeekLT => {
9984                    // bd-3pti: Route seek opcodes through B-tree cursor.
9985                    //
9986                    // Seek operations position the cursor relative to a key:
9987                    // - SeekGE: Position at first row >= key
9988                    // - SeekGT: Position at first row > key
9989                    // - SeekLE: Position at last row <= key
9990                    // - SeekLT: Position at last row < key
9991                    //
9992                    // Jump to p2 if no matching row exists.  NULL key → not found.
9993                    let cursor_id = op.p1;
9994                    // Seek repositions the cursor, so clear any pending delete state.
9995                    self.pending_next_after_delete.remove(&cursor_id);
9996                    let key_val = self.clone_reg_materialized(op.p3);
9997                    if key_val.is_null() {
9998                        pc = op.p2 as usize;
9999                        continue;
10000                    }
10001
10002                    // Dispatch based on cursor type (table vs index), NOT on
10003                    // key value type. Using the key type was incorrect: an
10004                    // index cursor receiving an integer key would wrongly call
10005                    // table_move_to, triggering "table leaf cell has no rowid"
10006                    // on index pages. (Fixes br#138-140, #144, #145.)
10007                    let found = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10008                        if cursor.cursor.is_table_btree() {
10009                            // Table seek: key is a rowid (integer).
10010                            let key = key_val.to_integer();
10011                            let seek_result = cursor.cursor.table_move_to(&cursor.cx, key).await?;
10012
10013                            match op.opcode {
10014                                Opcode::SeekGE => {
10015                                    // Need first row >= key.
10016                                    // table_move_to already positions at key (Found) or
10017                                    // at next larger (NotFound). Check for EOF.
10018                                    !cursor.cursor.eof()
10019                                }
10020                                Opcode::SeekGT => {
10021                                    // Need first row > key.
10022                                    // If Found (at exact key), advance past it.
10023                                    // If NotFound, already past key.
10024                                    if seek_result.is_found() {
10025                                        cursor.cursor.next(&cursor.cx).await?
10026                                    } else {
10027                                        !cursor.cursor.eof()
10028                                    }
10029                                }
10030                                Opcode::SeekLE => {
10031                                    // Need last row <= key.
10032                                    // If Found, we're at the exact key - done.
10033                                    // If NotFound, cursor is at entry > key, so prev().
10034                                    if seek_result.is_found() {
10035                                        true
10036                                    } else if cursor.cursor.eof() {
10037                                        // All entries < key, position at last.
10038                                        cursor.cursor.last(&cursor.cx).await?
10039                                    } else {
10040                                        // Cursor at entry > key, move to previous.
10041                                        cursor.cursor.prev(&cursor.cx).await?
10042                                    }
10043                                }
10044                                Opcode::SeekLT => {
10045                                    // Need last row < key.
10046                                    // Cursor is either at key (Found) or past key (NotFound).
10047                                    // Either way, we need to go to the previous entry.
10048                                    if cursor.cursor.eof() {
10049                                        // All entries < key, position at last.
10050                                        cursor.cursor.last(&cursor.cx).await?
10051                                    } else {
10052                                        // Go to previous entry (which will be < key).
10053                                        cursor.cursor.prev(&cursor.cx).await?
10054                                    }
10055                                }
10056                                _ => unreachable!(),
10057                            }
10058                        } else {
10059                            // Index seek: key is a packed record blob.
10060                            let key_bytes = record_blob_bytes(&key_val);
10061                            match op.opcode {
10062                                Opcode::SeekGE => {
10063                                    cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
10064                                    !cursor.cursor.eof()
10065                                }
10066                                Opcode::SeekGT => {
10067                                    cursor
10068                                        .cursor
10069                                        .index_move_to_upper_bound(&cursor.cx, key_bytes)
10070                                        .await?;
10071                                    !cursor.cursor.eof()
10072                                }
10073                                Opcode::SeekLE => {
10074                                    cursor
10075                                        .cursor
10076                                        .index_move_to_upper_bound(&cursor.cx, key_bytes)
10077                                        .await?;
10078                                    if cursor.cursor.eof() {
10079                                        cursor.cursor.last(&cursor.cx).await?
10080                                    } else {
10081                                        cursor.cursor.prev(&cursor.cx).await?
10082                                    }
10083                                }
10084                                Opcode::SeekLT => {
10085                                    cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
10086                                    if cursor.cursor.eof() {
10087                                        cursor.cursor.last(&cursor.cx).await?
10088                                    } else {
10089                                        cursor.cursor.prev(&cursor.cx).await?
10090                                    }
10091                                }
10092                                _ => unreachable!(),
10093                            }
10094                        }
10095                    } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
10096                        // MemCursor fallback (Phase 4 path).
10097                        let key = key_val.to_integer();
10098                        if let Some(db) = self.db.as_ref() {
10099                            if let Some(table) = db.get_table(cursor.root_page) {
10100                                if table.rows.is_empty() {
10101                                    false
10102                                } else {
10103                                    match op.opcode {
10104                                        Opcode::SeekGE => {
10105                                            let pos = table
10106                                                .rows
10107                                                .binary_search_by_key(&key, |r| r.rowid)
10108                                                .unwrap_or_else(|e| e);
10109                                            if pos < table.rows.len() {
10110                                                cursor.position = Some(pos);
10111                                                true
10112                                            } else {
10113                                                false
10114                                            }
10115                                        }
10116                                        Opcode::SeekGT => {
10117                                            let pos = match table
10118                                                .rows
10119                                                .binary_search_by_key(&key, |r| r.rowid)
10120                                            {
10121                                                Ok(idx) => idx + 1,
10122                                                Err(idx) => idx,
10123                                            };
10124                                            if pos < table.rows.len() {
10125                                                cursor.position = Some(pos);
10126                                                true
10127                                            } else {
10128                                                false
10129                                            }
10130                                        }
10131                                        Opcode::SeekLE => {
10132                                            let pos = match table
10133                                                .rows
10134                                                .binary_search_by_key(&key, |r| r.rowid)
10135                                            {
10136                                                Ok(idx) => Some(idx),
10137                                                Err(idx) => idx.checked_sub(1),
10138                                            };
10139                                            if let Some(idx) = pos {
10140                                                cursor.position = Some(idx);
10141                                                true
10142                                            } else {
10143                                                false
10144                                            }
10145                                        }
10146                                        Opcode::SeekLT => {
10147                                            let pos = table
10148                                                .rows
10149                                                .binary_search_by_key(&key, |r| r.rowid)
10150                                                .unwrap_or_else(|e| e)
10151                                                .checked_sub(1);
10152                                            if let Some(idx) = pos {
10153                                                cursor.position = Some(idx);
10154                                                true
10155                                            } else {
10156                                                false
10157                                            }
10158                                        }
10159                                        _ => unreachable!(),
10160                                    }
10161                                }
10162                            } else {
10163                                false
10164                            }
10165                        } else {
10166                            false
10167                        }
10168                    } else {
10169                        false
10170                    };
10171                    if found {
10172                        pc += 1;
10173                    } else {
10174                        pc = op.p2 as usize;
10175                    }
10176                }
10177
10178                Opcode::SeekScan | Opcode::SeekEnd | Opcode::SeekHit => {
10179                    pc += 1;
10180                }
10181
10182                Opcode::NotFound | Opcode::NotExists | Opcode::IfNoHope => {
10183                    // Check if key in register P3 exists in cursor P1.
10184                    // Jump to P2 if NOT found; fall through if found.
10185                    // NULL key → always "not found".
10186                    let cursor_id = op.p1;
10187                    // Probe repositions the cursor; clear pending delete/next
10188                    // state so a following Next advances relative to the new
10189                    // cursor position.
10190                    if self.storage_cursors.contains_key(&cursor_id) {
10191                        self.pending_next_after_delete.remove(&cursor_id);
10192                    }
10193                    let key_val = self.clone_reg_materialized(op.p3);
10194                    if key_val.is_null() {
10195                        pc = op.p2 as usize;
10196                        continue;
10197                    }
10198                    let exists = if matches!(key_val, SqliteValue::Blob(_)) {
10199                        // Index seek path: P3 contains a packed record blob
10200                        // (from MakeRecord). Use index_move_to to find the key.
10201                        let key_bytes = record_blob_bytes(&key_val);
10202                        self.storage_cursor_find_exact_index_key(
10203                            cursor_id,
10204                            key_bytes,
10205                            "NotFound: missing collation registry for collated probe",
10206                        )
10207                        .await?
10208                    } else {
10209                        // Table seek path: P3 contains an integer rowid.
10210                        let rowid_val = key_val.to_integer();
10211                        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10212                            cursor
10213                                .cursor
10214                                .table_move_to(&cursor.cx, rowid_val)
10215                                .await?
10216                                .is_found()
10217                        } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10218                            if let Some(db) = self.db.as_ref() {
10219                                if let Some(table) = db.get_table(cursor.root_page) {
10220                                    table.find_by_rowid(rowid_val).is_some()
10221                                } else {
10222                                    false
10223                                }
10224                            } else {
10225                                false
10226                            }
10227                        } else {
10228                            false
10229                        }
10230                    };
10231                    if exists {
10232                        pc += 1; // Found: fall through.
10233                    } else {
10234                        pc = op.p2 as usize; // Not found: jump.
10235                    }
10236                }
10237
10238                Opcode::Found => {
10239                    // Jump to P2 if key found in cursor P1 (exact match).
10240                    // NULL key → never found (don't jump).
10241                    let cursor_id = op.p1;
10242                    if self.storage_cursors.contains_key(&cursor_id) {
10243                        self.pending_next_after_delete.remove(&cursor_id);
10244                    }
10245                    let key_val = self.clone_reg_materialized(op.p3);
10246                    if key_val.is_null() {
10247                        pc += 1;
10248                        continue;
10249                    }
10250                    let exists = if matches!(key_val, SqliteValue::Blob(_)) {
10251                        let key_bytes = record_blob_bytes(&key_val);
10252                        self.storage_cursor_find_exact_index_key(
10253                            cursor_id,
10254                            key_bytes,
10255                            "Found: missing collation registry for collated probe",
10256                        )
10257                        .await?
10258                    } else {
10259                        let rowid_val = key_val.to_integer();
10260                        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10261                            cursor
10262                                .cursor
10263                                .table_move_to(&cursor.cx, rowid_val)
10264                                .await?
10265                                .is_found()
10266                        } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10267                            if let Some(db) = self.db.as_ref() {
10268                                if let Some(table) = db.get_table(cursor.root_page) {
10269                                    table.find_by_rowid(rowid_val).is_some()
10270                                } else {
10271                                    false
10272                                }
10273                            } else {
10274                                false
10275                            }
10276                        } else {
10277                            false
10278                        }
10279                    };
10280                    if exists {
10281                        pc = op.p2 as usize;
10282                    } else {
10283                        pc += 1;
10284                    }
10285                }
10286
10287                Opcode::NoConflict => {
10288                    // Jump to P2 if NO matching key prefix exists in index
10289                    // cursor P1.  Falls through when a conflict IS found
10290                    // (cursor positioned on the conflicting entry).
10291                    // NULL in any key field → always jump (no conflict).
10292                    let cursor_id = op.p1;
10293                    if self.storage_cursors.contains_key(&cursor_id) {
10294                        self.pending_next_after_delete.remove(&cursor_id);
10295                    }
10296                    let key_val = self.clone_reg_materialized(op.p3);
10297                    let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
10298                    let index_collations = self.index_collations_for_cursor(cursor_id);
10299
10300                    // NULL short-circuit: NULL != NULL for UNIQUE purposes.
10301                    if key_val.is_null() {
10302                        pc = op.p2 as usize;
10303                        continue;
10304                    }
10305
10306                    let uses_collated_probe = if let SqliteValue::Blob(ref bytes) = key_val {
10307                        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10308                            if try_decode_storage_cursor_target_index_record(cursor, bytes)
10309                                && cursor.target_vals_buf.iter().any(SqliteValue::is_null)
10310                            {
10311                                pc = op.p2 as usize;
10312                                continue;
10313                            }
10314                            cursor.target_vals_buf.iter().enumerate().any(|(idx, _)| {
10315                                index_collations
10316                                    .get(idx)
10317                                    .and_then(|collation| collation.as_deref())
10318                                    .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
10319                            })
10320                        } else {
10321                            false
10322                        }
10323                    } else {
10324                        false
10325                    };
10326                    let collated_probe_registry = uses_collated_probe.then(|| {
10327                        self.collation_registry
10328                            .lock()
10329                            .unwrap_or_else(|err| err.into_inner())
10330                            .clone()
10331                    });
10332
10333                    // Prefix-based conflict check: seek the index, then
10334                    // compare only the first N fields (where N = number of
10335                    // fields in the probe key) against the entry at the
10336                    // cursor position.  The index stores (columns, rowid)
10337                    // but the probe key has only (columns).
10338                    let conflict = if let SqliteValue::Blob(ref bytes) = key_val {
10339                        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10340                            if uses_collated_probe {
10341                                let Some(collation_registry) = collated_probe_registry.as_ref()
10342                                else {
10343                                    return Err(FrankenError::internal(
10344                                        "NoConflict: missing collation registry for collated probe",
10345                                    ));
10346                                };
10347                                storage_cursor_no_conflict_prefix_match_collated(
10348                                    cursor_id,
10349                                    cursor,
10350                                    bytes,
10351                                    &index_desc_flags,
10352                                    &index_collations,
10353                                    collation_registry,
10354                                )
10355                                .await?
10356                            } else {
10357                                storage_cursor_no_conflict_prefix_match(cursor_id, cursor, bytes)
10358                                    .await?
10359                            }
10360                        } else {
10361                            false
10362                        }
10363                    } else {
10364                        false
10365                    };
10366
10367                    if conflict {
10368                        pc += 1; // Conflict found: fall through.
10369                    } else {
10370                        pc = op.p2 as usize; // No conflict: jump.
10371                    }
10372                }
10373
10374                // ── Insert / Delete / NewRowid ──────────────────────────
10375                Opcode::NewRowid => {
10376                    // Allocate a new rowid for cursor p1, store in register p2.
10377                    //
10378                    // In concurrent mode, StorageCursor inserts must reserve
10379                    // rowids from the shared coordinator allocator so separate
10380                    // writers do not reuse `max(rowid)+1` from the same
10381                    // snapshot and silently overwrite each other.
10382                    let cursor_id = op.p1;
10383                    let target = op.p2;
10384                    let concurrent_mode = op.p3 != 0;
10385                    let concurrent_allocator = if concurrent_mode {
10386                        self.concurrent_rowid_allocator.clone()
10387                    } else {
10388                        None
10389                    };
10390                    let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
10391                    let rowid = if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10392                        let root_page = sc.root_page;
10393                        let autoinc_max = sc.autoincrement_high_water;
10394                        let rowid_mode = sc.rowid_mode;
10395                        // Storage NewRowid probes max rowid via `last()`, which
10396                        // repositions the cursor. Clear any pending delete/next
10397                        // state so subsequent Next/Prev behavior is consistent
10398                        // with the new position.
10399                        self.pending_next_after_delete.remove(&cursor_id);
10400                        if concurrent_mode {
10401                            if let Some(allocator) = concurrent_allocator.as_ref() {
10402                                Self::allocate_concurrent_storage_rowid(
10403                                    allocator,
10404                                    concurrent_schema_epoch,
10405                                    root_page,
10406                                    rowid_mode,
10407                                    autoinc_max,
10408                                    sc,
10409                                    "rowid overflow: maximum rowid reached",
10410                                )
10411                                .await?
10412                            } else {
10413                                Self::allocate_serialized_storage_rowid(
10414                                    sc,
10415                                    autoinc_max,
10416                                    "rowid overflow: maximum rowid reached",
10417                                )
10418                                .await?
10419                            }
10420                        } else {
10421                            Self::allocate_serialized_storage_rowid(
10422                                sc,
10423                                autoinc_max,
10424                                "rowid overflow: maximum rowid reached",
10425                            )
10426                            .await?
10427                        }
10428                    } else {
10429                        // MemDatabase fallback (Phase 4 in-memory cursors).
10430                        let root = self.cursors.get(&cursor_id).map(|c| c.root_page);
10431                        if let Some(root) = root {
10432                            if let Some(db) = self.db.as_mut() {
10433                                if concurrent_mode {
10434                                    db.alloc_rowid_concurrent(root)
10435                                } else {
10436                                    db.alloc_rowid(root)
10437                                }
10438                            } else {
10439                                1
10440                            }
10441                        } else {
10442                            1
10443                        }
10444                    };
10445                    self.set_reg(target, SqliteValue::Integer(rowid));
10446                    pc += 1;
10447                }
10448
10449                Opcode::Insert => {
10450                    // Insert record in register p2 with rowid from register p3
10451                    // into cursor p1. p5 encodes conflict resolution mode:
10452                    // 1=ROLLBACK, 2=ABORT (default), 3=FAIL, 4=IGNORE, 5=REPLACE.
10453                    // Higher bits carry OPFLAG_* metadata.
10454                    //
10455                    // OE_* constants matching SQLite (4=IGNORE, 5=REPLACE)
10456                    let cursor_id = op.p1;
10457                    let record_reg = op.p2;
10458                    let rowid_reg = op.p3;
10459                    let oe_flag = op.p5 & 0x0F; // Low 4 bits for OE_* mode
10460                    let is_update = (op.p5 & OPFLAG_ISUPDATE) != 0;
10461                    let rowid = self.get_reg(rowid_reg).to_integer();
10462                    let concurrent_allocator = self.concurrent_rowid_allocator.clone();
10463                    let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
10464                    let previous_last_insert_rowid = self.last_insert_rowid;
10465                    let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
10466                    let pending_update_restore = if is_update {
10467                        self.take_pending_update_restore()
10468                    } else {
10469                        self.set_pending_update_restore(None);
10470                        None
10471                    };
10472
10473                    // Phase 5B.2 (bd-1yi8): write-through — route ONLY through
10474                    // StorageCursor when one exists; fall back to MemDatabase
10475                    // only for legacy Phase 4 cursors.
10476                    //
10477                    // bd-perf: Sideband buffer — if MakeRecord stored bytes in
10478                    // the MakeRecord sideband lookaside, take ownership here to avoid
10479                    // Arc allocation. Otherwise fall back to register blob.
10480                    let preformatted_record = match &op.p4 {
10481                        P4::Blob(bytes) => Some(bytes.as_slice()),
10482                        _ => None,
10483                    };
10484                    let sideband_active = preformatted_record.is_none()
10485                        && self.make_record_lookaside.sideband_is_armed_for(record_reg);
10486                    // Only move the register value out when there is no active
10487                    // MakeRecord sideband. Doing `take_reg()` first would
10488                    // eagerly materialize the sideband into an Arc-backed blob
10489                    // and defeat the INSERT hot-path optimization.
10490                    let record_val = if preformatted_record.is_some() {
10491                        self.clear_register_subtype(record_reg);
10492                        self.set_reg(record_reg, SqliteValue::Null);
10493                        SqliteValue::Null
10494                    } else if sideband_active {
10495                        self.clear_register_subtype(record_reg);
10496                        SqliteValue::Null
10497                    } else {
10498                        // take_reg moves the value out (replacing with Null)
10499                        // instead of cloning — avoids a heap allocation for
10500                        // Blob/Text records.
10501                        self.take_reg(record_reg)
10502                    };
10503                    let mut sideband_buf = if sideband_active {
10504                        self.make_record_lookaside.disarm();
10505                        self.make_record_lookaside.take_buf()
10506                    } else {
10507                        Vec::new()
10508                    };
10509                    let mut actually_inserted = false;
10510                    let mut inserted_via_storage = false;
10511                    let mut inserted_root_page = None;
10512                    let insert_result: Result<Option<ExecOutcome>> = async {
10513                        if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10514                            if sc.writable {
10515                                let root_page = sc.root_page;
10516                                let autoinc_max = sc.autoincrement_high_water;
10517                                let rowid_mode = sc.rowid_mode;
10518                                let blob = if let Some(record) = preformatted_record {
10519                                    record
10520                                } else if sideband_active {
10521                                    &sideband_buf[..]
10522                                } else {
10523                                    record_blob_bytes(&record_val)
10524                                };
10525                                // bd-p666i: Append fast-path — if the new rowid
10526                                // is strictly greater than the last successfully
10527                                // inserted rowid on this cursor, the row cannot
10528                                // already exist (B-tree keys are unique) and we
10529                                // can skip the full B-tree seek.  This matches
10530                                // C SQLite's BTREE_APPEND optimization.
10531                                //
10532                                // bd-0zxi6: Safe in concurrent mode because:
10533                                // 1. last_successful_insert_rowid is per-StorageCursor (per-txn)
10534                                // 2. Each concurrent txn has its own COW page copies
10535                                // 3. SSI conflict detection happens at commit time
10536                                let append_eligible = !is_update
10537                                    && sc
10538                                        .last_successful_insert_rowid
10539                                        .is_some_and(|last| rowid > last);
10540                                let mut insert_seek_result = None;
10541                                let exists = if append_eligible {
10542                                    FSQLITE_VDBE_INSERT_APPEND_COUNT
10543                                        .fetch_add(1, AtomicOrdering::Relaxed);
10544                                    false // Append: key is larger than anything in the table
10545                                } else {
10546                                    FSQLITE_VDBE_INSERT_SEEK_COUNT
10547                                        .fetch_add(1, AtomicOrdering::Relaxed);
10548                                    let seek_result =
10549                                        sc.cursor.table_move_to(&sc.cx, rowid).await?;
10550                                    insert_seek_result = Some(seek_result);
10551                                    seek_result.is_found()
10552                                };
10553
10554                                if exists {
10555                                    if sc.last_successful_insert_rowid.take().is_some() {
10556                                        FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10557                                            .fetch_add(1, AtomicOrdering::Relaxed);
10558                                    }
10559                                    // Match on the low OE_* bits directly — p5 is
10560                                    // not a plain bitfield in this engine because
10561                                    // it also carries the custom OPFLAG_ISUPDATE
10562                                    // bit above the conflict-mode nibble.
10563                                    if oe_flag == 5 {
10564                                        // OE_REPLACE: Delete old, insert new
10565                                        self.native_replace_row(cursor_id, rowid).await?;
10566                                        let sc2 = self
10567                                            .storage_cursors
10568                                            .get_mut(&cursor_id)
10569                                            .ok_or_else(|| {
10570                                                FrankenError::internal(
10571                                                    "cursor disappeared during REPLACE",
10572                                                )
10573                                            })?;
10574                                        sc2.cursor.table_insert(&sc2.cx, rowid, blob).await?;
10575                                        invalidate_storage_cursor_row_cache_with_reason(
10576                                            sc2,
10577                                            self.collect_vdbe_metrics,
10578                                            DecodeCacheInvalidationReason::WriteMutation,
10579                                        );
10580                                        if let Some(allocator) = concurrent_allocator.as_ref() {
10581                                            Self::bump_concurrent_storage_rowid_floor(
10582                                                allocator,
10583                                                concurrent_schema_epoch,
10584                                                root_page,
10585                                                rowid_mode,
10586                                                autoinc_max,
10587                                                sc2,
10588                                                rowid,
10589                                            )
10590                                            .await?;
10591                                        }
10592                                        inserted_via_storage = true;
10593                                        inserted_root_page = Some(root_page);
10594                                        actually_inserted = true;
10595                                    } else if oe_flag == 4 {
10596                                        // OE_IGNORE: Skip insert for conflicting row
10597                                        if let Some(update_restore) = pending_update_restore.clone()
10598                                        {
10599                                            self.restore_pending_update_after_conflict(
10600                                                update_restore,
10601                                            )
10602                                            .await?;
10603                                        }
10604                                    } else {
10605                                        // Default (ABORT/FAIL/ROLLBACK): constraint error.
10606                                        if let Some(update_restore) = pending_update_restore.clone()
10607                                        {
10608                                            self.restore_pending_update_after_conflict(
10609                                                update_restore,
10610                                            )
10611                                            .await?;
10612                                        }
10613                                        return Ok(Some(ExecOutcome::Error {
10614                                            code: ErrorCode::Constraint as i32,
10615                                            message: "PRIMARY KEY constraint failed".to_owned(),
10616                                        }));
10617                                    }
10618                                } else {
10619                                    // No conflict — reuse the successor/EOF position from the
10620                                    // existence probe when it already proved the row is absent.
10621                                    if insert_seek_result == Some(SeekResult::NotFound) {
10622                                        let rightmost_insert = sc.cursor.eof();
10623                                        sc.cursor
10624                                            .table_insert_prechecked_absent(&sc.cx, rowid, blob)
10625                                            .await?;
10626                                        if rightmost_insert {
10627                                            sc.cursor
10628                                                .table_refresh_rightmost_leaf_cache_after_insert(
10629                                                    &sc.cx, rowid,
10630                                                )
10631                                                .await?;
10632                                            sc.last_successful_insert_rowid = Some(rowid);
10633                                        } else if sc.last_successful_insert_rowid.take().is_some() {
10634                                            FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10635                                                .fetch_add(1, AtomicOrdering::Relaxed);
10636                                        }
10637                                    } else {
10638                                        sc.cursor.table_insert(&sc.cx, rowid, blob).await?;
10639                                        if append_eligible {
10640                                            sc.last_successful_insert_rowid = Some(rowid);
10641                                        } else if sc.last_successful_insert_rowid.take().is_some() {
10642                                            FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10643                                                .fetch_add(1, AtomicOrdering::Relaxed);
10644                                        }
10645                                    }
10646                                    invalidate_storage_cursor_row_cache_with_reason(
10647                                        sc,
10648                                        self.collect_vdbe_metrics,
10649                                        DecodeCacheInvalidationReason::WriteMutation,
10650                                    );
10651                                    if let Some(allocator) = concurrent_allocator.as_ref() {
10652                                        Self::bump_concurrent_storage_rowid_floor(
10653                                            allocator,
10654                                            concurrent_schema_epoch,
10655                                            root_page,
10656                                            rowid_mode,
10657                                            autoinc_max,
10658                                            sc,
10659                                            rowid,
10660                                        )
10661                                        .await?;
10662                                    }
10663                                    inserted_via_storage = true;
10664                                    inserted_root_page = Some(root_page);
10665                                    actually_inserted = true;
10666                                }
10667                            }
10668                        } else if let Some(root) = self.cursors.get(&cursor_id).map(|c| c.root_page)
10669                        {
10670                            // MemDatabase fallback (Phase 4 in-memory cursors).
10671                            let values = if let Some(record) = preformatted_record {
10672                                let values = parse_record(record).ok_or_else(|| {
10673                                    FrankenError::internal("malformed SQLite record blob")
10674                                })?;
10675                                if self.collect_vdbe_metrics {
10676                                    FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
10677                                        .fetch_add(1, AtomicOrdering::Relaxed);
10678                                    for value in &values {
10679                                        record_decoded_value_metrics(value);
10680                                    }
10681                                }
10682                                values
10683                            } else if sideband_active {
10684                                let values = parse_record(&sideband_buf).ok_or_else(|| {
10685                                    FrankenError::internal("malformed SQLite record blob")
10686                                })?;
10687                                if self.collect_vdbe_metrics {
10688                                    FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
10689                                        .fetch_add(1, AtomicOrdering::Relaxed);
10690                                    for value in &values {
10691                                        record_decoded_value_metrics(value);
10692                                    }
10693                                }
10694                                values
10695                            } else {
10696                                decode_record_with_metrics(&record_val, self.collect_vdbe_metrics)?
10697                            };
10698                            // Exact rows implicitly deleted by REPLACE below;
10699                            // pushed into `replace_victims` after the &mut db
10700                            // borrow ends so the connection can run victim FK
10701                            // and delete-trigger semantics.
10702                            let mut mem_replace_victims: Vec<(i64, Vec<SqliteValue>)> = Vec::new();
10703                            if let Some(db) = self.db.as_mut() {
10704                                // Check rowid conflict first.
10705                                let rowid_conflict = db
10706                                    .get_table(root)
10707                                    .and_then(|t| t.find_by_rowid(rowid))
10708                                    .is_some();
10709
10710                                // Check UNIQUE column constraint conflicts (non-IPK).
10711                                // We check even if rowid_conflict is true, because
10712                                // the new values might conflict with a DIFFERENT
10713                                // row on a UNIQUE column.
10714                                let unique_conflicts = db
10715                                    .get_table(root)
10716                                    .map(|t| t.find_unique_conflicts(&values))
10717                                    .unwrap_or_default();
10718
10719                                let has_conflict = rowid_conflict || !unique_conflicts.is_empty();
10720
10721                                if has_conflict {
10722                                    match oe_flag {
10723                                        4 => {
10724                                            // OE_IGNORE: Skip insert for conflicting row
10725                                            if let Some(update_restore) =
10726                                                pending_update_restore.clone()
10727                                            {
10728                                                self.restore_pending_update_after_conflict(
10729                                                    update_restore,
10730                                                )
10731                                                .await?;
10732                                            }
10733                                        }
10734                                        5 => {
10735                                            // OE_REPLACE: Delete conflicting row(s),
10736                                            // then insert new.
10737                                            for conflict_rid in unique_conflicts {
10738                                                // Delete conflicting rows that are not the new rowid
10739                                                // (which will be replaced by upsert_row).
10740                                                if conflict_rid != rowid {
10741                                                    if let Some(old_values) =
10742                                                        db.get_table(root).and_then(|t| {
10743                                                            t.row_values_by_rowid(conflict_rid)
10744                                                        })
10745                                                    {
10746                                                        mem_replace_victims.push((
10747                                                            conflict_rid,
10748                                                            old_values.to_vec(),
10749                                                        ));
10750                                                    }
10751                                                    db.delete_rowid(root, conflict_rid);
10752                                                }
10753                                            }
10754                                            if rowid_conflict
10755                                                && let Some(old_values) = db
10756                                                    .get_table(root)
10757                                                    .and_then(|t| t.row_values_by_rowid(rowid))
10758                                            {
10759                                                mem_replace_victims
10760                                                    .push((rowid, old_values.to_vec()));
10761                                            }
10762                                            db.upsert_row(root, rowid, values);
10763                                            actually_inserted = true;
10764                                        }
10765                                        _ => {
10766                                            // Default (ABORT/FAIL/ROLLBACK): constraint error.
10767                                            if let Some(update_restore) =
10768                                                pending_update_restore.clone()
10769                                            {
10770                                                self.restore_pending_update_after_conflict(
10771                                                    update_restore,
10772                                                )
10773                                                .await?;
10774                                            }
10775                                            return Ok(Some(ExecOutcome::Error {
10776                                                code: ErrorCode::Constraint as i32,
10777                                                message: "PRIMARY KEY constraint failed".to_owned(),
10778                                            }));
10779                                        }
10780                                    }
10781                                } else {
10782                                    // No conflict — insert normally
10783                                    db.upsert_row(root, rowid, values);
10784                                    actually_inserted = true;
10785                                }
10786                            }
10787                            for (victim_rowid, old_values) in mem_replace_victims {
10788                                let victim = self.logical_replace_victim(
10789                                    root,
10790                                    &old_values,
10791                                    Some(victim_rowid),
10792                                );
10793                                self.replace_victims.push(victim);
10794                            }
10795                        }
10796                        Ok(None)
10797                    }
10798                    .await;
10799                    // bd-perf: Return sideband buffer for reuse (keeps capacity)
10800                    // before propagating every fallible Insert path.
10801                    if sideband_active {
10802                        sideband_buf.clear();
10803                        self.make_record_lookaside.replace_buf(sideband_buf);
10804                    }
10805                    if let Some(outcome) = insert_result? {
10806                        break outcome;
10807                    }
10808                    if inserted_via_storage && let Some(root_page) = inserted_root_page {
10809                        // Hot INSERT path already knows the table root page, so
10810                        // avoid another cursor_root_pages lookup when marking
10811                        // the MemDB mirror stale.
10812                        self.mark_storage_root_page_dirty(root_page);
10813                    }
10814
10815                    // Track last insert rowid only when a row was actually inserted.
10816                    // C SQLite does not update last_insert_rowid() when IGNORE skips.
10817                    if actually_inserted {
10818                        self.changes += 1;
10819                        if !is_update {
10820                            self.last_insert_rowid = rowid;
10821                            self.last_insert_rowid_valid = true;
10822                        }
10823                        self.set_pending_insert_rollback(Some(PendingInsertRollback {
10824                            cursor_id,
10825                            rowid,
10826                            previous_last_insert_rowid,
10827                            previous_last_insert_rowid_valid,
10828                            update_restore: pending_update_restore,
10829                        }));
10830                    } else {
10831                        self.set_pending_insert_rollback(None);
10832                        // When OE_IGNORE skips the insert (unique or rowid
10833                        // conflict handled internally), tell subsequent
10834                        // IdxInsert opcodes to skip this row's index entries.
10835                        if oe_flag == 4 {
10836                            self.set_conflict_skip_idx(true);
10837                        }
10838                    }
10839                    self.last_insert_cursor_id = Some(cursor_id);
10840                    if actually_inserted {
10841                        self.set_conflict_skip_idx(false);
10842                    }
10843                    self.clear_pending_idx_entries();
10844
10845                    // br-22iss: Clear pending_next_after_delete since Insert repositions
10846                    // the cursor. This is critical for UPDATE (Delete+Insert) to avoid
10847                    // infinite loops when the rowid doesn't change.
10848                    self.pending_next_after_delete.remove(&cursor_id);
10849                    pc += 1;
10850                }
10851
10852                Opcode::Delete => {
10853                    // Delete the row at the current cursor position.
10854                    let cursor_id = op.p1;
10855                    let is_update = (op.p5 & OPFLAG_ISUPDATE) != 0;
10856                    let mut deleted = false;
10857                    let mut deleted_storage_rowid = None;
10858                    let mut update_restore = None;
10859                    // Phase 5B.3 (bd-1r0d): write-through — route ONLY through
10860                    // storage cursor when one exists; fall back to MemDatabase
10861                    // only for legacy Phase 4 cursors.
10862                    if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10863                        if sc.writable && !sc.cursor.eof() {
10864                            let current_rowid = sc.cursor.rowid(&sc.cx).await?;
10865                            if is_update {
10866                                update_restore = Some(PendingUpdateRestore::Storage {
10867                                    cursor_id,
10868                                    rowid: current_rowid,
10869                                    payload: sc.cursor.payload(&sc.cx).await?,
10870                                });
10871                            }
10872                            sc.cursor.delete(&sc.cx).await?;
10873                            invalidate_storage_cursor_row_cache_with_reason(
10874                                sc,
10875                                self.collect_vdbe_metrics,
10876                                DecodeCacheInvalidationReason::WriteMutation,
10877                            );
10878                            deleted_storage_rowid = Some(current_rowid);
10879                            deleted = true;
10880                        }
10881                    } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10882                        // Pure in-memory path (Phase 4).
10883                        if let Some(pos) = cursor.position {
10884                            let root = cursor.root_page;
10885                            let can_delete = self
10886                                .db
10887                                .as_ref()
10888                                .and_then(|db| db.get_table(root))
10889                                .is_some_and(|table| pos < table.rows.len());
10890                            if can_delete && let Some(db) = self.db.as_mut() {
10891                                if is_update
10892                                    && let Some(row) = db
10893                                        .get_table(root)
10894                                        .and_then(|table| table.rows.get(pos))
10895                                        .cloned()
10896                                {
10897                                    update_restore = Some(PendingUpdateRestore::Mem {
10898                                        root_page: root,
10899                                        rowid: row.rowid,
10900                                        values: row.values,
10901                                    });
10902                                }
10903                                db.delete_at(root, pos);
10904                                deleted = true;
10905                            }
10906                        }
10907                    }
10908                    if let Some(storage_rowid) = deleted_storage_rowid {
10909                        self.sync_storage_table_delete_into_memdb_mirror(cursor_id, storage_rowid);
10910                    }
10911                    if deleted {
10912                        if is_update && update_restore.is_some() {
10913                            self.mark_statement_cold_state(StatementColdState::CONFLICT_TRACKING);
10914                        }
10915                        self.set_pending_update_restore(if is_update {
10916                            update_restore
10917                        } else {
10918                            None
10919                        });
10920                        // P5 bit 0 = OPFLAG_NCHANGE: only count standalone
10921                        // DELETE changes. UPDATE's internal Delete uses P5=0
10922                        // so only the subsequent Insert counts.
10923                        if op.p5 & 1 != 0 {
10924                            self.changes += 1;
10925                        }
10926                        self.pending_next_after_delete.insert(cursor_id);
10927                    } else if is_update {
10928                        self.set_pending_update_restore(None);
10929                    }
10930                    pc += 1;
10931                }
10932
10933                Opcode::IdxInsert => {
10934                    // Insert key from register P2 into index cursor P1.
10935                    // bd-qluy: Phase 5I.6 - Wire to B-tree index_insert.
10936                    // P5 encoding: bit 0 = is_unique, bits 1-4 = oe_flag
10937                    // (conflict resolution mode for UNIQUE violations).
10938                    // P3 = number of indexed columns (excluding trailing
10939                    // rowid). P4 = columns string for the error message.
10940                    let cursor_id = op.p1;
10941                    let key_reg = op.p2;
10942                    let is_unique = (op.p5 & 1) != 0;
10943                    #[allow(clippy::cast_possible_truncation)]
10944                    let oe_flag = ((op.p5 >> 1) & 0x0F) as u8;
10945                    let n_idx_cols = op.p3 as usize;
10946                    let count_logical_change = (op.p5 & OPFLAG_IDX_NCHANGE) != 0;
10947                    let mut inserted = false;
10948
10949                    // If a previous IdxInsert for the same row triggered IGNORE,
10950                    // skip all remaining index inserts for this row before touching
10951                    // the MakeRecord sideband. Otherwise a skipped index entry drops
10952                    // the reusable sideband buffer and forces the next row to
10953                    // reallocate it.
10954                    if self.conflict_skip_idx() {
10955                        pc += 1;
10956                        continue;
10957                    }
10958
10959                    let key_val = self.get_reg(key_reg).clone();
10960                    let sideband_active = self.make_record_lookaside.sideband_is_armed_for(key_reg);
10961                    let key_blob = if sideband_active {
10962                        self.make_record_lookaside.disarm();
10963                        self.make_record_lookaside.take_buf()
10964                    } else {
10965                        record_blob_bytes(&key_val).to_vec()
10966                    };
10967
10968                    let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
10969                    let index_collations = self.index_collations_for_cursor(cursor_id);
10970                    let uses_collated_unique_probe = is_unique
10971                        && n_idx_cols > 0
10972                        && index_collations.iter().take(n_idx_cols).any(|collation| {
10973                            collation
10974                                .as_deref()
10975                                .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
10976                        });
10977                    let collated_unique_registry = uses_collated_unique_probe.then(|| {
10978                        self.collation_registry
10979                            .lock()
10980                            .unwrap_or_else(|err| err.into_inner())
10981                            .clone()
10982                    });
10983
10984                    if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10985                        if sc.writable {
10986                            let key_bytes = key_blob.as_slice();
10987
10988                            if is_unique && n_idx_cols > 0 {
10989                                let columns_label = match &op.p4 {
10990                                    P4::Table(s) => s.as_str(),
10991                                    _ => "",
10992                                };
10993                                let mut rightmost_prefix_after_insert: Option<Vec<SqliteValue>> =
10994                                    None;
10995                                let unique_insert_result = if uses_collated_unique_probe {
10996                                    sc.last_rightmost_unique_index_prefix = None;
10997                                    sc.last_rightmost_unique_index_position = None;
10998                                    if let Some(collation_registry) =
10999                                        collated_unique_registry.as_ref()
11000                                    {
11001                                        match find_conflicting_rowid_in_index_collated(
11002                                            sc,
11003                                            key_bytes,
11004                                            n_idx_cols,
11005                                            &index_desc_flags,
11006                                            &index_collations,
11007                                            collation_registry,
11008                                        )
11009                                        .await
11010                                        {
11011                                            Ok(Some(conflict_rowid)) => Err((
11012                                                FrankenError::UniqueViolation {
11013                                                    columns: columns_label.to_owned(),
11014                                                },
11015                                                Some(conflict_rowid),
11016                                            )),
11017                                            Ok(None) => {
11018                                                match sc
11019                                                    .cursor
11020                                                    .index_insert(&sc.cx, key_bytes)
11021                                                    .await
11022                                                {
11023                                                    Ok(()) => Ok(()),
11024                                                    Err(err) => Err((err, None)),
11025                                                }
11026                                            }
11027                                            Err(err) => Err((err, None)),
11028                                        }
11029                                    } else {
11030                                        Err((
11031                                            FrankenError::internal(
11032                                                "IdxInsert: missing collation registry for collated unique probe",
11033                                            ),
11034                                            None,
11035                                        ))
11036                                    }
11037                                } else {
11038                                    let fast_append_prefix =
11039                                        if let (Some(last_prefix), Some(last_position)) = (
11040                                            sc.last_rightmost_unique_index_prefix.as_ref(),
11041                                            sc.last_rightmost_unique_index_position,
11042                                        ) {
11043                                            if sc.cursor.position_stamp() == Some(last_position) {
11044                                                parse_non_null_index_prefix(key_bytes, n_idx_cols)
11045                                                    .and_then(|new_prefix| {
11046                                                        (compare_index_prefix_keys(
11047                                                            last_prefix,
11048                                                            &new_prefix,
11049                                                            n_idx_cols,
11050                                                            &index_desc_flags,
11051                                                            &index_collations,
11052                                                            &BUILTIN_COLLATION_REGISTRY,
11053                                                        ) == Ordering::Less)
11054                                                            .then_some(new_prefix)
11055                                                    })
11056                                            } else {
11057                                                None
11058                                            }
11059                                        } else {
11060                                            None
11061                                        };
11062                                    if let Some(prefix) = fast_append_prefix {
11063                                        match sc
11064                                            .cursor
11065                                            .index_append_after_current_rightmost_position(
11066                                                &sc.cx, key_bytes,
11067                                            )
11068                                            .await
11069                                        {
11070                                            Ok(true) => {
11071                                                rightmost_prefix_after_insert = Some(prefix);
11072                                                Ok(())
11073                                            }
11074                                            Ok(false) => {
11075                                                match sc
11076                                                    .cursor
11077                                                    .index_insert_unique_with_rightmost_report(
11078                                                        &sc.cx,
11079                                                        key_bytes,
11080                                                        n_idx_cols,
11081                                                        columns_label,
11082                                                    )
11083                                                    .await
11084                                                {
11085                                                    Ok(inserted_after_rightmost) => {
11086                                                        if inserted_after_rightmost {
11087                                                            rightmost_prefix_after_insert =
11088                                                                Some(prefix);
11089                                                        }
11090                                                        Ok(())
11091                                                    }
11092                                                    Err(FrankenError::UniqueViolation {
11093                                                        columns,
11094                                                    }) => Err((
11095                                                        FrankenError::UniqueViolation { columns },
11096                                                        None,
11097                                                    )),
11098                                                    Err(err) => Err((err, None)),
11099                                                }
11100                                            }
11101                                            Err(err) => Err((err, None)),
11102                                        }
11103                                    } else {
11104                                        match sc
11105                                            .cursor
11106                                            .index_insert_unique_with_rightmost_report(
11107                                                &sc.cx,
11108                                                key_bytes,
11109                                                n_idx_cols,
11110                                                columns_label,
11111                                            )
11112                                            .await
11113                                        {
11114                                            Ok(inserted_after_rightmost) => {
11115                                                if inserted_after_rightmost {
11116                                                    rightmost_prefix_after_insert =
11117                                                        parse_non_null_index_prefix(
11118                                                            key_bytes, n_idx_cols,
11119                                                        );
11120                                                }
11121                                                Ok(())
11122                                            }
11123                                            Err(FrankenError::UniqueViolation { columns }) => Err(
11124                                                (FrankenError::UniqueViolation { columns }, None),
11125                                            ),
11126                                            Err(err) => Err((err, None)),
11127                                        }
11128                                    }
11129                                };
11130                                match unique_insert_result {
11131                                    Ok(()) => {
11132                                        inserted = true;
11133                                        if let (Some(prefix), Some(position)) = (
11134                                            rightmost_prefix_after_insert,
11135                                            sc.cursor.position_stamp(),
11136                                        ) {
11137                                            sc.last_rightmost_unique_index_prefix = Some(prefix);
11138                                            sc.last_rightmost_unique_index_position =
11139                                                Some(position);
11140                                        } else {
11141                                            sc.last_rightmost_unique_index_prefix = None;
11142                                            sc.last_rightmost_unique_index_position = None;
11143                                        }
11144                                        invalidate_storage_cursor_row_cache_with_reason(
11145                                            sc,
11146                                            self.collect_vdbe_metrics,
11147                                            DecodeCacheInvalidationReason::WriteMutation,
11148                                        );
11149                                        self.push_pending_idx_entry(cursor_id, key_bytes.to_vec());
11150                                    }
11151                                    Err((FrankenError::UniqueViolation { .. }, conflict_rowid)) => {
11152                                        sc.last_rightmost_unique_index_prefix = None;
11153                                        sc.last_rightmost_unique_index_position = None;
11154                                        match oe_flag {
11155                                            // OE_IGNORE (4): Undo the table
11156                                            // insert, roll back any already-inserted
11157                                            // index entries, and skip remaining indexes.
11158                                            4 => {
11159                                                if let Err(error) = self
11160                                                    .rollback_pending_insert_after_index_conflict(
11161                                                        true,
11162                                                    )
11163                                                    .await
11164                                                {
11165                                                    if sideband_active {
11166                                                        self.make_record_lookaside
11167                                                            .replace_cleared_buf(key_blob);
11168                                                    }
11169                                                    return Err(error);
11170                                                }
11171                                                self.set_conflict_skip_idx(true);
11172                                                if sideband_active {
11173                                                    self.make_record_lookaside
11174                                                        .replace_cleared_buf(key_blob);
11175                                                }
11176                                                pc += 1;
11177                                                continue;
11178                                            }
11179                                            // OE_REPLACE (5 or 8): Find the
11180                                            // conflicting row, delete it (and its
11181                                            // index entries), then insert the new
11182                                            // index entry.
11183                                            5 | 8 => {
11184                                                // Find the rowid of the
11185                                                // conflicting row from the index.
11186                                                let conflict_rowid = if uses_collated_unique_probe {
11187                                                    conflict_rowid
11188                                                } else {
11189                                                    match find_conflicting_rowid_in_index(
11190                                                        sc, key_bytes, n_idx_cols,
11191                                                    )
11192                                                    .await
11193                                                    {
11194                                                        Ok(rowid) => rowid,
11195                                                        Err(error) => {
11196                                                            if sideband_active {
11197                                                                self.make_record_lookaside
11198                                                                    .replace_cleared_buf(key_blob);
11199                                                            }
11200                                                            return Err(error);
11201                                                        }
11202                                                    }
11203                                                };
11204
11205                                                if let Some(old_rowid) = conflict_rowid {
11206                                                    if let Some(tbl_cid) =
11207                                                        self.last_insert_cursor_id
11208                                                    {
11209                                                        if let Err(error) = self
11210                                                            .native_replace_row(tbl_cid, old_rowid)
11211                                                            .await
11212                                                        {
11213                                                            if sideband_active {
11214                                                                self.make_record_lookaside
11215                                                                    .replace_cleared_buf(key_blob);
11216                                                            }
11217                                                            return Err(error);
11218                                                        }
11219                                                    }
11220                                                }
11221
11222                                                // Now insert the new index entry. The binary probe
11223                                                // deletes the conflicting index entry directly; the
11224                                                // collated probe leaves exact-key cleanup to
11225                                                // native_replace_row above.
11226                                                let Some(sc2) =
11227                                                    self.storage_cursors.get_mut(&cursor_id)
11228                                                else {
11229                                                    if sideband_active {
11230                                                        self.make_record_lookaside
11231                                                            .replace_cleared_buf(key_blob);
11232                                                    }
11233                                                    return Err(FrankenError::internal(
11234                                                        "cursor must exist",
11235                                                    ));
11236                                                };
11237                                                sc2.last_rightmost_unique_index_prefix = None;
11238                                                sc2.last_rightmost_unique_index_position = None;
11239                                                if let Err(error) = sc2
11240                                                    .cursor
11241                                                    .index_insert(&sc2.cx, key_bytes)
11242                                                    .await
11243                                                {
11244                                                    if sideband_active {
11245                                                        self.make_record_lookaside
11246                                                            .replace_cleared_buf(key_blob);
11247                                                    }
11248                                                    return Err(error);
11249                                                }
11250                                                invalidate_storage_cursor_row_cache_with_reason(
11251                                                    sc2,
11252                                                    self.collect_vdbe_metrics,
11253                                                    DecodeCacheInvalidationReason::WriteMutation,
11254                                                );
11255                                                self.push_pending_idx_entry(
11256                                                    cursor_id,
11257                                                    key_blob.clone(),
11258                                                );
11259                                                inserted = true;
11260                                            }
11261                                            // Default: propagate the error
11262                                            // (ABORT/FAIL/ROLLBACK).
11263                                            _ => {
11264                                                if let Err(error) = self
11265                                                    .rollback_pending_insert_after_index_conflict(
11266                                                        false,
11267                                                    )
11268                                                    .await
11269                                                {
11270                                                    if sideband_active {
11271                                                        self.make_record_lookaside
11272                                                            .replace_cleared_buf(key_blob);
11273                                                    }
11274                                                    return Err(error);
11275                                                }
11276                                                if sideband_active {
11277                                                    self.make_record_lookaside
11278                                                        .replace_cleared_buf(key_blob);
11279                                                }
11280                                                return Err(FrankenError::UniqueViolation {
11281                                                    columns: columns_label.to_owned(),
11282                                                });
11283                                            }
11284                                        }
11285                                    }
11286                                    Err((error, _)) => {
11287                                        if sideband_active {
11288                                            self.make_record_lookaside
11289                                                .replace_cleared_buf(key_blob);
11290                                        }
11291                                        return Err(error);
11292                                    }
11293                                }
11294                            } else {
11295                                sc.last_rightmost_unique_index_prefix = None;
11296                                sc.last_rightmost_unique_index_position = None;
11297                                if let Err(error) = sc.cursor.index_insert(&sc.cx, key_bytes).await
11298                                {
11299                                    if sideband_active {
11300                                        self.make_record_lookaside.replace_cleared_buf(key_blob);
11301                                    }
11302                                    return Err(error);
11303                                }
11304                                invalidate_storage_cursor_row_cache_with_reason(
11305                                    sc,
11306                                    self.collect_vdbe_metrics,
11307                                    DecodeCacheInvalidationReason::WriteMutation,
11308                                );
11309                                self.push_pending_idx_entry(cursor_id, key_blob.clone());
11310                                inserted = true;
11311                            }
11312                        }
11313                    }
11314                    // No MemDatabase fallback: Phase 4 in-memory backend doesn't
11315                    // support indexes (they're a no-op there).
11316                    if sideband_active {
11317                        self.make_record_lookaside.replace_cleared_buf(key_blob);
11318                    }
11319                    if count_logical_change && inserted {
11320                        self.changes = self.changes.saturating_add(1);
11321                    }
11322                    pc += 1;
11323                }
11324
11325                Opcode::SorterInsert => {
11326                    // Lazy key decode: only decode the first `key_columns`
11327                    // values (the sort key) instead of all columns.  The
11328                    // raw blob is retained for output via `SorterData`.
11329                    let cursor_id = op.p1;
11330                    let record_reg = op.p2;
11331                    let sideband_active =
11332                        self.make_record_lookaside.sideband_is_armed_for(record_reg);
11333                    let blob = if sideband_active {
11334                        self.make_record_lookaside.disarm();
11335                        self.make_record_lookaside.take_buf()
11336                    } else {
11337                        // Move the record value out of the register instead of
11338                        // cloning it; MakeRecord sideband consumers above avoid
11339                        // this path because take_reg would materialize the blob.
11340                        let record = self.take_reg(record_reg);
11341                        record_blob_bytes(&record).to_vec()
11342                    };
11343                    if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
11344                        let key_values =
11345                            fsqlite_types::record::parse_record_prefix(&blob, sorter.key_columns)
11346                                .ok_or_else(|| FrankenError::DatabaseCorrupt {
11347                                detail: "malformed record in SorterInsert".to_owned(),
11348                            })?;
11349                        sorter.insert_row(key_values, blob)?;
11350                    }
11351                    pc += 1;
11352                }
11353
11354                Opcode::IdxDelete => {
11355                    // Delete entry at current position in index cursor P1.
11356                    // bd-qluy: Phase 5I.6 - Wire to B-tree delete.
11357                    //
11358                    // If P2 and P3 are provided, they specify the key to delete:
11359                    // P2 = start register, P3 = number of registers forming the key.
11360                    // In that case, we first seek to the key, then delete.
11361                    let cursor_id = op.p1;
11362                    let key_start_reg = op.p2;
11363                    let key_count = op.p3;
11364                    let replace_victim = if op.p5 & OPFLAG_REPLACE_VICTIM != 0 {
11365                        let (root_page, payload) = {
11366                            let cursor = self
11367                                .storage_cursors
11368                                .get_mut(&cursor_id)
11369                                .filter(|cursor| cursor.writable && !cursor.cursor.eof())
11370                                .ok_or_else(|| {
11371                                    FrankenError::internal(
11372                                        "REPLACE victim capture requires a positioned writable cursor",
11373                                    )
11374                                })?;
11375                            let root_page = cursor.root_page;
11376                            let payload = cursor.cursor.payload(&cursor.cx).await?;
11377                            (root_page, payload)
11378                        };
11379                        let stored_values =
11380                            parse_record(&payload).ok_or_else(|| FrankenError::DatabaseCorrupt {
11381                                detail: "REPLACE victim capture encountered a malformed WITHOUT ROWID record"
11382                                    .to_owned(),
11383                            })?;
11384                        Some(self.logical_replace_victim(root_page, &stored_values, None))
11385                    } else {
11386                        None
11387                    };
11388                    let mut deleted = false;
11389
11390                    // Collect key bytes BEFORE borrowing cursor (borrow checker).
11391                    let key_bytes: Option<Vec<u8>> = if key_count > 0 {
11392                        let iter = (0..key_count).map(|i| self.get_reg(key_start_reg + i));
11393                        Some(fsqlite_types::record::serialize_record_iter(iter))
11394                    } else {
11395                        None
11396                    };
11397
11398                    if let Some(ref key) = key_bytes {
11399                        let writable = self
11400                            .storage_cursors
11401                            .get(&cursor_id)
11402                            .map(|sc| sc.writable)
11403                            .unwrap_or(false);
11404                        if writable
11405                            && self.storage_cursor_find_exact_index_key(
11406                                cursor_id,
11407                                key,
11408                                "IdxDelete: missing collation registry for collated exact probe",
11409                            )
11410                            .await?
11411                        {
11412                            if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11413                                sc.last_rightmost_unique_index_prefix = None;
11414                                sc.last_rightmost_unique_index_position = None;
11415                                sc.cursor.delete(&sc.cx).await?;
11416                                deleted = true;
11417                                invalidate_storage_cursor_row_cache_with_reason(
11418                                    sc,
11419                                    self.collect_vdbe_metrics,
11420                                    DecodeCacheInvalidationReason::WriteMutation,
11421                                );
11422                            }
11423                        }
11424                    } else if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11425                        if sc.writable && !sc.cursor.eof() {
11426                            // Delete at current position.
11427                            sc.last_rightmost_unique_index_prefix = None;
11428                            sc.last_rightmost_unique_index_position = None;
11429                            sc.cursor.delete(&sc.cx).await?;
11430                            deleted = true;
11431                            invalidate_storage_cursor_row_cache_with_reason(
11432                                sc,
11433                                self.collect_vdbe_metrics,
11434                                DecodeCacheInvalidationReason::WriteMutation,
11435                            );
11436                        }
11437                    }
11438                    if deleted && let Some(replace_victim) = replace_victim {
11439                        self.replace_victims.push(replace_victim);
11440                    }
11441                    // No MemDatabase fallback for indexes.
11442                    pc += 1;
11443                }
11444
11445                Opcode::SorterCompare => {
11446                    // In preflight mode, compare a candidate key with the
11447                    // bounded sorter's current worst key and jump when the
11448                    // candidate cannot survive. Otherwise compare the current
11449                    // sorter key with P3 and jump when the keys differ.
11450                    let cursor_id = op.p1;
11451                    let is_top_n_preflight = (op.p5 & SORTER_COMPARE_TOP_N_PREFLIGHT) != 0;
11452                    let preflight_sorter_exists =
11453                        !is_top_n_preflight || self.sorters.get(&cursor_id).is_some();
11454                    let preflight_needs_compare = is_top_n_preflight
11455                        && self.sorters.get(&cursor_id).is_some_and(|sorter| {
11456                            sorter
11457                                .top_n_limit
11458                                .is_some_and(|limit| sorter.rows.len() >= limit)
11459                        });
11460                    let preflight_probe = if is_top_n_preflight {
11461                        if self.make_record_lookaside.sideband_is_armed_for(op.p3) {
11462                            // Consume MakeRecord's sideband directly, then
11463                            // return its allocation to the lookaside pool.
11464                            // Materializing an Arc-backed register blob here
11465                            // would add an allocation to every source row.
11466                            self.make_record_lookaside.disarm();
11467                            let probe_buf = self.make_record_lookaside.take_buf();
11468                            let decoded = preflight_needs_compare
11469                                .then(|| {
11470                                    decode_record_bytes_with_metrics(
11471                                        &probe_buf,
11472                                        self.collect_vdbe_metrics,
11473                                    )
11474                                })
11475                                .transpose();
11476                            self.make_record_lookaside.replace_cleared_buf(probe_buf);
11477                            decoded?
11478                        } else if preflight_needs_compare {
11479                            Some(decode_record_with_metrics(
11480                                self.get_reg(op.p3),
11481                                self.collect_vdbe_metrics,
11482                            )?)
11483                        } else {
11484                            None
11485                        }
11486                    } else {
11487                        None
11488                    };
11489                    let ordinary_needs_compare = !is_top_n_preflight
11490                        && self.sorters.get(&cursor_id).is_some_and(|sorter| {
11491                            sorter
11492                                .position
11493                                .is_some_and(|position| sorter.rows.get(position).is_some())
11494                        });
11495                    let ordinary_probe = if ordinary_needs_compare {
11496                        if self.make_record_lookaside.sideband_is_armed_for(op.p3) {
11497                            // Decode the logical P3 value without consuming
11498                            // MakeRecord's sideband. Ordinary SorterCompare is
11499                            // a read operation, so a later opcode must still
11500                            // observe the packed record in P3.
11501                            Some(decode_record_bytes_with_metrics(
11502                                self.make_record_lookaside.as_slice(),
11503                                self.collect_vdbe_metrics,
11504                            )?)
11505                        } else {
11506                            Some(decode_record_with_metrics(
11507                                self.get_reg(op.p3),
11508                                self.collect_vdbe_metrics,
11509                            )?)
11510                        }
11511                    } else {
11512                        None
11513                    };
11514                    let coll = self.lock_collation();
11515                    let should_jump = if is_top_n_preflight {
11516                        if !preflight_sorter_exists {
11517                            true
11518                        } else if !preflight_needs_compare {
11519                            false
11520                        } else if let Some(sorter) = self.sorters.get(&cursor_id) {
11521                            !sorter.would_retain_top_n(
11522                                preflight_probe
11523                                    .as_deref()
11524                                    .expect("preflight probe should be decoded"),
11525                                &coll,
11526                            )
11527                        } else {
11528                            true
11529                        }
11530                    } else if let Some(sorter) = self.sorters.get(&cursor_id) {
11531                        if let Some(pos) = sorter.position {
11532                            if let Some(current) = sorter.rows.get(pos) {
11533                                let probe = ordinary_probe
11534                                    .as_deref()
11535                                    .expect("positioned sorter probe should be decoded");
11536                                !sorter_keys_equal(
11537                                    &current.values,
11538                                    probe,
11539                                    sorter.key_columns,
11540                                    &sorter.collations,
11541                                    &coll,
11542                                )
11543                            } else {
11544                                true
11545                            }
11546                        } else {
11547                            true
11548                        }
11549                    } else {
11550                        true
11551                    };
11552                    if should_jump {
11553                        pc = op.p2 as usize;
11554                    } else {
11555                        pc += 1;
11556                    }
11557                }
11558
11559                Opcode::SorterData => {
11560                    // Encode current sorter row into register p2.
11561                    let cursor_id = op.p1;
11562                    let target = op.p2;
11563                    let value = if let Some(sorter) = self.sorters.get(&cursor_id) {
11564                        if let Some(pos) = sorter.position {
11565                            if let Some(row) = sorter.rows.get(pos) {
11566                                SqliteValue::Blob(row.blob.clone().into())
11567                            } else {
11568                                SqliteValue::Null
11569                            }
11570                        } else {
11571                            SqliteValue::Null
11572                        }
11573                    } else {
11574                        SqliteValue::Null
11575                    };
11576                    self.set_reg(target, value);
11577                    pc += 1;
11578                }
11579
11580                Opcode::RowCell => {
11581                    pc += 1;
11582                }
11583
11584                Opcode::ResetCount => {
11585                    pc += 1;
11586                }
11587
11588                // ── Record building (SQLite record format) ──────────────
11589                Opcode::MakeRecord => {
11590                    self.execute_make_record_hot(op, collect_vdbe_metrics);
11591                    pc += 1;
11592                }
11593
11594                Opcode::Affinity => {
11595                    // Apply type affinity to p2 registers starting at p1.
11596                    // Uses p4 as affinity string.
11597                    if let P4::Affinity(aff) = &op.p4 {
11598                        let start = op.p1;
11599                        for (i, ch) in aff.chars().enumerate() {
11600                            #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
11601                            let reg = start + i as i32;
11602                            let val = self.take_reg(reg);
11603                            let affinity = char_to_affinity(ch);
11604                            if collect_vdbe_metrics {
11605                                let before = val.clone();
11606                                let coerced = val.apply_affinity(affinity);
11607                                record_type_coercion(&before, &coerced);
11608                                self.set_reg(reg, coerced);
11609                            } else {
11610                                self.set_reg(reg, val.apply_affinity(affinity));
11611                            }
11612                        }
11613                    }
11614                    pc += 1;
11615                }
11616
11617                // ── Miscellaneous ───────────────────────────────────────
11618                Opcode::HaltIfNull => {
11619                    if self.get_reg(op.p3).is_null() {
11620                        let msg = match &op.p4 {
11621                            P4::Str(s) => s.clone(),
11622                            _ => "NOT NULL constraint failed".to_owned(),
11623                        };
11624                        break ExecOutcome::Error {
11625                            code: op.p1,
11626                            message: msg,
11627                        };
11628                    }
11629                    pc += 1;
11630                }
11631
11632                Opcode::Count => {
11633                    // Count rows in cursor P1, store result in register P2.
11634                    //
11635                    // IMPORTANT: file-backed connections often keep a schema-only
11636                    // MemDatabase mirror for correctness/perf reasons. In that
11637                    // mode, `db.get_table(root).rows.len()` is deliberately 0 even
11638                    // when the pager-backed table has real rows. Only true
11639                    // MemCursors can trust the MemDatabase row count directly.
11640                    let cursor_id = op.p1;
11641                    let count: i64 = if let Some(cursor) = self.cursors.get(&cursor_id) {
11642                        if let Some(db) = self.db.as_ref()
11643                            && let Some(table) = db.get_table(cursor.root_page)
11644                        {
11645                            i64::try_from(table.rows.len()).unwrap_or(0)
11646                        } else {
11647                            0
11648                        }
11649                    } else if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11650                        // bd-wwqen.3: storage-cursor COUNT must report the
11651                        // cursor's own storage view, not a connection-local
11652                        // MemDatabase mirror that may lag retained same-connection
11653                        // writes.
11654                        sc.cursor.count_all_rows(&sc.cx).await?
11655                    } else {
11656                        0
11657                    };
11658                    self.set_reg_int(op.p2, count);
11659                    pc += 1;
11660                }
11661
11662                Opcode::CountIndexEqRun => {
11663                    let probe_value = self.get_reg(op.p3).clone();
11664                    let matched = if probe_value.is_null() {
11665                        0
11666                    } else {
11667                        let cursor = self.storage_cursors.get_mut(&op.p1).ok_or_else(|| {
11668                            FrankenError::internal(
11669                                "CountIndexEqRun requires an open storage cursor",
11670                            )
11671                        })?;
11672                        if cursor.cursor.is_table_btree() {
11673                            return Err(FrankenError::internal(
11674                                "CountIndexEqRun requires an index cursor",
11675                            ));
11676                        }
11677                        storage_cursor_count_equal_first_key_run(
11678                            cursor,
11679                            &probe_value,
11680                            self.collect_vdbe_metrics,
11681                        )
11682                        .await?
11683                    };
11684                    let next_count = self.get_reg(op.p2).to_integer().wrapping_add(matched);
11685                    self.set_reg_int(op.p2, next_count);
11686                    pc += 1;
11687                }
11688
11689                Opcode::Sequence => {
11690                    let counter = self
11691                        .ensure_cold_state_for(StatementColdState::SEQUENCE_COUNTERS)
11692                        .sequence_counters
11693                        .entry(op.p1)
11694                        .or_insert(0);
11695                    let val = *counter;
11696                    *counter += 1;
11697                    self.set_reg_int(op.p2, val);
11698                    pc += 1;
11699                }
11700
11701                Opcode::SequenceTest => {
11702                    pc += 1;
11703                }
11704
11705                Opcode::Variable => {
11706                    // Bind parameter (1-indexed). Unbound params read as NULL.
11707                    let idx = usize::try_from(op.p1)
11708                        .ok()
11709                        .and_then(|one_based| one_based.checked_sub(1));
11710                    let value = idx
11711                        .and_then(|idx| {
11712                            borrowed_bindings
11713                                .and_then(|bindings| bindings.get(idx))
11714                                .or_else(|| self.bindings.get(idx))
11715                        })
11716                        .cloned()
11717                        .unwrap_or(SqliteValue::Null);
11718                    self.set_reg(op.p2, value);
11719                    pc += 1;
11720                }
11721
11722                Opcode::BeginSubrtn => {
11723                    self.set_reg(op.p2, SqliteValue::Null);
11724                    pc += 1;
11725                }
11726
11727                Opcode::IsTrue => {
11728                    // Synopsis: r[P2] = coalesce(IsTrue(r[P1]),P3) ^ P4
11729                    // Implements IS TRUE, IS FALSE, IS NOT TRUE, IS NOT FALSE.
11730                    let val = self.get_reg(op.p1);
11731                    let p4_val = match &op.p4 {
11732                        P4::Int(n) => *n,
11733                        _ => 0,
11734                    };
11735                    if val.is_null() {
11736                        self.set_reg(op.p2, SqliteValue::Integer(i64::from(op.p3 ^ p4_val)));
11737                    } else {
11738                        let v = i32::from(vdbe_real_is_truthy(val));
11739                        self.set_reg(op.p2, SqliteValue::Integer(i64::from((v ^ p4_val) & 1)));
11740                    }
11741                    pc += 1;
11742                }
11743
11744                Opcode::ZeroOrNull => {
11745                    // If either P1 or P3 is NULL, set P2 to NULL.
11746                    // Otherwise set P2 to 0.
11747                    // Reference: ZeroOrNull semantics (OP_ZeroOrNull spec).
11748                    if self.get_reg(op.p1).is_null() || self.get_reg(op.p3).is_null() {
11749                        self.set_reg(op.p2, SqliteValue::Null);
11750                    } else {
11751                        self.set_reg(op.p2, SqliteValue::Integer(0));
11752                    }
11753                    pc += 1;
11754                }
11755
11756                Opcode::IfNullRow => {
11757                    // Jump to p2 if cursor p1 is not positioned on a row.
11758                    // C SQLite also sets register P3 to NULL before jumping.
11759                    let is_null = if let Some(cursor) = self.storage_cursors.get(&op.p1) {
11760                        cursor.cursor.eof()
11761                    } else if let Some(state) = self
11762                        .cold_state()
11763                        .and_then(|cold_state| cold_state.vtab_cursors.get(&op.p1))
11764                    {
11765                        state.null_row || state.cursor.eof()
11766                    } else {
11767                        self.cursors
11768                            .get(&op.p1)
11769                            .is_none_or(|c| c.position.is_none() && !c.is_pseudo)
11770                    };
11771                    if is_null {
11772                        if op.p3 > 0 {
11773                            self.set_reg(op.p3, SqliteValue::Null);
11774                        }
11775                        pc = op.p2 as usize;
11776                    } else {
11777                        pc += 1;
11778                    }
11779                }
11780
11781                Opcode::IfNotOpen => {
11782                    // Jump to p2 if cursor p1 is not open.
11783                    if self.cursors.contains_key(&op.p1)
11784                        || self.storage_cursors.contains_key(&op.p1)
11785                        || self
11786                            .cold_state()
11787                            .is_some_and(|cold_state| cold_state.vtab_cursors.contains_key(&op.p1))
11788                        || self.sorters.contains_key(&op.p1)
11789                    {
11790                        pc += 1;
11791                    } else {
11792                        pc = op.p2 as usize;
11793                    }
11794                }
11795
11796                Opcode::Compare => {
11797                    // Compare P1..P1+P3-1 with P2..P2+P3-1.
11798                    let start_a = op.p1;
11799                    let start_b = op.p2;
11800                    let count = op.p3;
11801                    let coll_arc = Arc::clone(&self.collation_registry);
11802                    let result = {
11803                        let coll = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
11804                        let mut result = Ordering::Equal;
11805                        for i in 0..count {
11806                            let val_a = self.get_reg(start_a + i);
11807                            let val_b = self.get_reg(start_b + i);
11808                            let coll_name = usize::try_from(i).ok().and_then(|field_idx| {
11809                                compare_collation_for_field_from_p4(&op.p4, field_idx)
11810                            });
11811                            // SQLite NULL sort order: NULLs sort before all other
11812                            // values.  When partial_cmp returns None (NULL vs
11813                            // non-NULL or NaN), apply NULL-first ordering.
11814                            let ord = if let Some(coll_name) = coll_name {
11815                                collate_compare(val_a, val_b, coll_name, &coll)
11816                            } else {
11817                                val_a.partial_cmp(val_b)
11818                            };
11819                            let o = match ord {
11820                                Some(o) => o,
11821                                None => {
11822                                    // NULL < non-NULL; NULL == NULL for sort purposes.
11823                                    match (val_a.is_null(), val_b.is_null()) {
11824                                        (true, true) => Ordering::Equal,
11825                                        (true, false) => Ordering::Less,
11826                                        (false, true) => Ordering::Greater,
11827                                        (false, false) => Ordering::Equal,
11828                                    }
11829                                }
11830                            };
11831                            if o != Ordering::Equal {
11832                                result = o;
11833                                break;
11834                            }
11835                        }
11836                        result
11837                    };
11838                    self.last_compare_result = Some(result);
11839                    pc += 1;
11840                }
11841
11842                Opcode::Jump => {
11843                    // Jump to one of p1/p2/p3 based on last comparison.
11844                    let target = match self.last_compare_result {
11845                        Some(Ordering::Less) => op.p1,
11846                        Some(Ordering::Equal) => op.p2,
11847                        Some(Ordering::Greater) => op.p3,
11848                        None => {
11849                            // If no comparison has happened, fall through or use p2?
11850                            // SQLite spec says Jump logic depends on the preceding Compare.
11851                            // If we haven't compared, neutral path (p2) is a safe fallback.
11852                            op.p2
11853                        }
11854                    };
11855                    pc = target as usize;
11856                }
11857
11858                Opcode::TypeCheck => {
11859                    // P4 is either Affinity("IRT") or Str("IRT\ttable\tcol1\tcol2\tcol3")
11860                    let p4_str = match &op.p4 {
11861                        P4::Affinity(s) | P4::Str(s) => s.as_str(),
11862                        _ => "",
11863                    };
11864                    // Split on tab: first part is affinity pattern, rest is table+columns.
11865                    let mut parts = p4_str.split('\t');
11866                    let pattern = parts.next().unwrap_or("").as_bytes();
11867                    let table_name = parts.next().unwrap_or("");
11868                    let col_names: Vec<&str> = parts.collect();
11869
11870                    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
11871                    let count = op.p2.max(0) as usize;
11872                    for offset in 0..count {
11873                        #[allow(clippy::cast_possible_wrap)]
11874                        let reg = op.p1 + offset as i32;
11875                        let value = self.get_reg(reg);
11876                        let strict_type = match pattern.get(offset).copied().unwrap_or(b'A') {
11877                            b'A' | b'a' => None,
11878                            b'I' | b'i' => Some(StrictColumnType::Integer),
11879                            b'R' | b'r' => Some(StrictColumnType::Real),
11880                            b'T' | b't' => Some(StrictColumnType::Text),
11881                            b'L' | b'l' => Some(StrictColumnType::Blob),
11882                            other => {
11883                                return Err(FrankenError::Internal(format!(
11884                                    "unknown STRICT type code '{}' in OP_TypeCheck",
11885                                    char::from(other)
11886                                )));
11887                            }
11888                        };
11889
11890                        if let Some(expected) = strict_type {
11891                            let checked =
11892                                value.clone().validate_strict(expected).map_err(|err| {
11893                                    let col_label = col_names
11894                                        .get(offset)
11895                                        .filter(|s| !s.is_empty())
11896                                        .map_or_else(
11897                                            || format!("column {offset}"),
11898                                            |name| {
11899                                                if table_name.is_empty() {
11900                                                    (*name).to_owned()
11901                                                } else {
11902                                                    format!("{table_name}.{name}")
11903                                                }
11904                                            },
11905                                        );
11906                                    let col_type = format!("{expected:?}").to_ascii_uppercase();
11907                                    let actual_str = err.actual.to_string();
11908                                    tracing::warn!(
11909                                        register = reg,
11910                                        expected = ?expected,
11911                                        actual = %actual_str,
11912                                        column = %col_label,
11913                                        value = ?value,
11914                                        "STRICT type violation"
11915                                    );
11916                                    FrankenError::DatatypeViolation {
11917                                        column: col_label,
11918                                        column_type: col_type,
11919                                        actual: actual_str,
11920                                    }
11921                                })?;
11922                            self.set_reg(reg, checked);
11923                        }
11924                    }
11925                    pc += 1;
11926                }
11927
11928                Opcode::Permutation | Opcode::CollSeq | Opcode::ElseEq | Opcode::FkCheck => {
11929                    pc += 1;
11930                }
11931
11932                Opcode::IsType => {
11933                    // Check datatype of a value against the P5 type bitmask.
11934                    // If P1 >= 0: check column P3 of cursor P1.
11935                    // If P1 == -1: check register P3.
11936                    // P4 (Int) = default type code if column is beyond row width.
11937                    // P5 bitmask: 0x01=INTEGER, 0x02=FLOAT, 0x04=TEXT, 0x08=BLOB, 0x10=NULL
11938                    // Jump to P2 if the value's type matches a bit in P5.
11939                    let val_ref;
11940                    let val = if op.p1 < 0 {
11941                        self.get_reg(op.p3)
11942                    } else {
11943                        val_ref = self.cursor_column(op.p1, op.p3 as usize).await?;
11944                        &val_ref
11945                    };
11946                    let type_bit: u16 = match val {
11947                        SqliteValue::Integer(_) => 0x01,
11948                        SqliteValue::Float(_) => 0x02,
11949                        SqliteValue::Text(_) => 0x04,
11950                        SqliteValue::Blob(_) => 0x08,
11951                        SqliteValue::Null => 0x10,
11952                    };
11953                    if op.p5 & type_bit != 0 {
11954                        pc = op.p2 as usize;
11955                    } else {
11956                        pc += 1;
11957                    }
11958                }
11959
11960                Opcode::IfEmpty => {
11961                    // Jump to P2 if the table/index at cursor P1 is empty.
11962                    // WARNING: For storage cursors, this repositions the cursor
11963                    // to the first entry via `cursor.first()` as a side-effect.
11964                    // This is currently dead code (codegen never emits IfEmpty),
11965                    // but if this opcode is ever used, the cursor repositioning
11966                    // may invalidate assumptions about cursor position.
11967                    let cursor_id = op.p1;
11968                    let empty = if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11969                        // Try moving to first; false means empty.
11970                        let had_row = sc.cursor.first(&sc.cx).await?;
11971                        !had_row
11972                    } else if let Some(cursor) = self.cursors.get(&cursor_id) {
11973                        if let Some(db) = self.db.as_ref()
11974                            && let Some(table) = db.get_table(cursor.root_page)
11975                        {
11976                            table.rows.is_empty()
11977                        } else {
11978                            true // no table = empty
11979                        }
11980                    } else {
11981                        true
11982                    };
11983                    if empty {
11984                        pc = op.p2 as usize;
11985                    } else {
11986                        pc += 1;
11987                    }
11988                }
11989
11990                Opcode::IfSizeBetween => {
11991                    // Compute X = 10*log2(N) where N = approx row count of
11992                    // cursor P1 (or -1 if empty). Jump to P2 if X is in
11993                    // [P3, P4]. When we lack exact stats, estimate from the
11994                    // MemTable row count or assume 0 (empty) for storage
11995                    // cursors (conservative).
11996                    let cursor_id = op.p1;
11997                    let row_count: i64 = if let Some(cursor) = self.cursors.get(&cursor_id) {
11998                        if let Some(db) = self.db.as_ref()
11999                            && let Some(table) = db.get_table(cursor.root_page)
12000                        {
12001                            i64::try_from(table.rows.len()).unwrap_or(0)
12002                        } else {
12003                            0
12004                        }
12005                    } else {
12006                        // For storage cursors, we don't have a cheap row count.
12007                        // Default to -1 (empty) which maps to X = -10.
12008                        -1
12009                    };
12010                    #[allow(clippy::cast_precision_loss)]
12011                    let x = if row_count <= 0 {
12012                        -10_i32 // empty sentinel
12013                    } else {
12014                        ((row_count as f64).log2() * 10.0) as i32
12015                    };
12016                    let lo = op.p3;
12017                    let hi = match &op.p4 {
12018                        P4::Int(v) => *v,
12019                        _ => i32::MAX,
12020                    };
12021                    if x >= lo && x <= hi {
12022                        pc = op.p2 as usize;
12023                    } else {
12024                        pc += 1;
12025                    }
12026                }
12027
12028                Opcode::IdxRowid => {
12029                    // Extract rowid from index cursor p1 into register p2.
12030                    // For storage cursors this delegates to B-tree cursor
12031                    // rowid(), which decodes the trailing rowid field from the
12032                    // index key record.
12033                    let cursor_id = op.p1;
12034                    let target = op.p2;
12035                    let val = self.cursor_rowid(cursor_id).await?;
12036                    self.set_reg_fast(target, val);
12037                    pc += 1;
12038                }
12039
12040                Opcode::DeferredSeek | Opcode::FinishSeek => {
12041                    pc += 1;
12042                }
12043
12044                // ── Index comparison ────────────────────────────────────
12045                //
12046                // Compare the current index cursor key against a probe
12047                // key record in register P3. Jump to P2 when the
12048                // condition holds.
12049                //
12050                //   IdxLE: jump if cursor_key <= probe_key
12051                //   IdxGT: jump if cursor_key >  probe_key
12052                //   IdxLT: jump if cursor_key <  probe_key
12053                //   IdxGE: jump if cursor_key >= probe_key
12054                //
12055                // P1 = cursor, P2 = jump target, P3 = register with
12056                // probe key blob, P5 = number of key columns to compare
12057                // (0 means use all columns from the probe).
12058                Opcode::IdxLE | Opcode::IdxGT | Opcode::IdxLT | Opcode::IdxGE => {
12059                    let cursor_id = op.p1;
12060                    let probe_val = self.clone_reg_materialized(op.p3);
12061
12062                    let desc_flags = self.index_desc_flags_for_cursor(cursor_id);
12063                    let collations = self.index_collations_for_cursor(cursor_id);
12064
12065                    // Extract current cursor key as parsed fields.
12066                    if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
12067                        if sc.cursor.eof() {
12068                            // EOF: IdxGT/IdxGE jump (past end), IdxLT/IdxLE fall through.
12069                            let jump = matches!(op.opcode, Opcode::IdxGT | Opcode::IdxGE);
12070                            if jump {
12071                                pc = op.p2 as usize;
12072                            } else {
12073                                pc += 1;
12074                            }
12075                            continue;
12076                        }
12077
12078                        sc.target_vals_buf.clear();
12079                        if let SqliteValue::Blob(bytes) = &probe_val {
12080                            fsqlite_types::record::parse_record_into(
12081                                bytes,
12082                                &mut sc.target_vals_buf,
12083                            )
12084                            .ok_or_else(|| {
12085                                FrankenError::internal("index seek: malformed probe key record")
12086                            })?;
12087                        }
12088
12089                        if op.p5 == 1
12090                            && let Some(probe_first) = sc.target_vals_buf.first().cloned()
12091                        {
12092                            let coll_arc = Arc::clone(&self.collation_registry);
12093                            let coll_registry =
12094                                coll_arc.lock().unwrap_or_else(|e| e.into_inner()).clone();
12095                            let cmp = storage_cursor_current_first_index_key_compare(
12096                                sc,
12097                                &probe_first,
12098                                self.collect_vdbe_metrics,
12099                                "IdxCmp: malformed index record at cursor position",
12100                                desc_flags.first().copied().unwrap_or(false),
12101                                collations
12102                                    .first()
12103                                    .and_then(|collation| collation.as_deref()),
12104                                &coll_registry,
12105                            )
12106                            .await?;
12107
12108                            if idx_compare_condition_met(op.opcode, cmp) {
12109                                pc = op.p2 as usize;
12110                            } else {
12111                                pc += 1;
12112                            }
12113                            continue;
12114                        }
12115
12116                        if !try_decode_storage_cursor_current_index_record(cursor_id, sc).await? {
12117                            return Err(FrankenError::internal(
12118                                "IdxCmp: malformed index record at cursor position",
12119                            ));
12120                        }
12121
12122                        let n_compare = if op.p5 > 0 {
12123                            op.p5 as usize
12124                        } else {
12125                            sc.target_vals_buf.len()
12126                        };
12127                        // Lock collation via a separately-owned Arc clone
12128                        // so the mutable borrow on `sc` is not conflicted.
12129                        // Avoids cloning cur/tgt vals into SmallVecs per cmp.
12130                        let coll_arc = Arc::clone(&self.collation_registry);
12131                        let coll_guard = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
12132                        let cmp = compare_index_prefix_keys(
12133                            &sc.cur_vals_buf,
12134                            &sc.target_vals_buf,
12135                            n_compare,
12136                            &desc_flags,
12137                            &collations,
12138                            &coll_guard,
12139                        );
12140                        drop(coll_guard);
12141
12142                        let condition_met = idx_compare_condition_met(op.opcode, cmp);
12143
12144                        if condition_met {
12145                            pc = op.p2 as usize;
12146                        } else {
12147                            pc += 1;
12148                        }
12149                    } else if let Some(cursor) = self.cursors.get(&cursor_id) {
12150                        // MemCursor fallback (Phase 4).
12151                        let probe_fields =
12152                            decode_record_with_metrics(&probe_val, self.collect_vdbe_metrics)?;
12153                        if let Some(pos) = cursor.position
12154                            && let Some(db) = self.db.as_ref()
12155                            && let Some(table) = db.get_table(cursor.root_page)
12156                            && let Some(row) = table.rows.get(pos)
12157                        {
12158                            let n_compare = if op.p5 > 0 {
12159                                op.p5 as usize
12160                            } else {
12161                                probe_fields.len()
12162                            };
12163                            let desc_flags = self.index_desc_flags_for_root(cursor.root_page);
12164                            let collations = self.index_collations_for_root(cursor.root_page);
12165                            let coll_guard = self.lock_collation();
12166                            let cmp = compare_index_prefix_keys(
12167                                &row.values,
12168                                &probe_fields,
12169                                n_compare,
12170                                &desc_flags,
12171                                &collations,
12172                                &coll_guard,
12173                            );
12174                            drop(coll_guard);
12175                            let condition_met = idx_compare_condition_met(op.opcode, cmp);
12176                            if condition_met {
12177                                pc = op.p2 as usize;
12178                            } else {
12179                                pc += 1;
12180                            }
12181                        } else {
12182                            // No position or no table: treat as past-end.
12183                            let jump = matches!(op.opcode, Opcode::IdxGT | Opcode::IdxGE);
12184                            if jump {
12185                                pc = op.p2 as usize;
12186                            } else {
12187                                pc += 1;
12188                            }
12189                        }
12190                    } else {
12191                        pc += 1;
12192                    }
12193                }
12194
12195                // ── Schema / DDL ────────────────────────────────────────
12196                Opcode::CreateBtree => {
12197                    // Create a new B-tree (table) and store the root page in
12198                    // register p2. In memory mode, allocate a new MemTable.
12199                    let target = op.p2;
12200                    let root_page = if let Some(db) = self.db.as_mut() {
12201                        db.create_table(0) // Column count set later.
12202                    } else {
12203                        0
12204                    };
12205                    self.set_reg(target, SqliteValue::Integer(i64::from(root_page)));
12206                    pc += 1;
12207                }
12208
12209                Opcode::Clear => {
12210                    // Clear all rows from a table. p1 = root page.
12211                    if let Some(db) = self.db.as_mut() {
12212                        db.clear_table(op.p1);
12213                    }
12214                    pc += 1;
12215                }
12216
12217                Opcode::Destroy => {
12218                    // Remove a table/index. p1 = root page.
12219                    //
12220                    // NOTE: This only removes the in-memory MemDatabase entry.
12221                    // Page-level freelist reclamation is handled by
12222                    // `Connection::execute_drop` → `free_btree_pages` which is
12223                    // the primary DDL path. If VDBE-driven DROP is ever used
12224                    // for pager-backed databases, this must also walk the B-tree
12225                    // and call `txn_page_io.free_page()` for every page.
12226                    if let Some(db) = self.db.as_mut() {
12227                        db.destroy_table(op.p1);
12228                    }
12229                    pc += 1;
12230                }
12231
12232                Opcode::SqlExec
12233                | Opcode::ParseSchema
12234                | Opcode::LoadAnalysis
12235                | Opcode::DropTable
12236                | Opcode::DropIndex
12237                | Opcode::DropTrigger => {
12238                    pc += 1;
12239                }
12240
12241                Opcode::ResetSorter => {
12242                    if let Some(sorter) = self.sorters.get_mut(&op.p1) {
12243                        sorter.reset();
12244                    }
12245                    pc += 1;
12246                }
12247
12248                // ── Savepoint ──────────────────────────────────────────
12249                Opcode::Savepoint => {
12250                    // P1: 0=BEGIN, 1=RELEASE, 2=ROLLBACK
12251                    // P4: savepoint name
12252                    // In the in-memory engine, savepoints use undo
12253                    // version tokens to snapshot/restore state.
12254                    // Full implementation deferred to WAL/pager integration.
12255                    pc += 1;
12256                }
12257
12258                // ── Checkpoint ────────────────────────────────────────────
12259                Opcode::Checkpoint => {
12260                    // WAL checkpoint. No-op for in-memory engine.
12261                    pc += 1;
12262                }
12263
12264                // ── Program execution (subprogram) ──────────────────────
12265                Opcode::Program | Opcode::Param => {
12266                    pc += 1;
12267                }
12268
12269                // ── Coroutine ───────────────────────────────────────────
12270                Opcode::InitCoroutine => {
12271                    self.set_reg(op.p1, SqliteValue::Integer(i64::from(op.p3)));
12272                    if op.p2 > 0 {
12273                        pc = op.p2 as usize;
12274                    } else {
12275                        pc += 1;
12276                    }
12277                }
12278
12279                Opcode::Yield => {
12280                    let saved = self.get_reg(op.p1).to_integer();
12281                    if saved < 0 || saved as usize >= ops.len() {
12282                        return Err(FrankenError::Internal(format!(
12283                            "Yield: coroutine address {} out of bounds",
12284                            saved
12285                        )));
12286                    }
12287                    let current = (pc + 1) as i32;
12288                    self.set_reg(op.p1, SqliteValue::Integer(i64::from(current)));
12289                    pc = saved as usize;
12290                }
12291
12292                Opcode::EndCoroutine => {
12293                    let saved = self.get_reg(op.p1).to_integer();
12294                    if saved < 0 || saved as usize >= ops.len() {
12295                        return Err(FrankenError::Internal(format!(
12296                            "EndCoroutine: coroutine address {} out of bounds",
12297                            saved
12298                        )));
12299                    }
12300                    pc = saved as usize;
12301                }
12302
12303                // ── Aggregation ─────────────────────────────────────────
12304                //
12305                // Phase 4 supports single-group aggregation (no GROUP BY) using
12306                // AggStep/AggFinal. Aggregate state is stored out-of-band and keyed
12307                // by the accumulator register.
12308                // AggStep1 is a single-argument fast-path with identical semantics.
12309                Opcode::AggStep | Opcode::AggStep1 => {
12310                    let (func_name, agg_collation): (&str, Option<&str>) = match &op.p4 {
12311                        P4::FuncName(name) => (name.as_str(), None),
12312                        P4::FuncNameCollated(name, coll) => (name.as_str(), Some(coll.as_str())),
12313                        _ => {
12314                            return Err(FrankenError::Internal(
12315                                "AggStep opcode missing P4::FuncName".to_owned(),
12316                            ));
12317                        }
12318                    };
12319
12320                    let arg_count = i32::from(op.p5);
12321                    let func_pc = pc;
12322                    let func = if let Some(func) = self.aggregate_function_cache.get(&func_pc) {
12323                        Arc::clone(func)
12324                    } else {
12325                        let registry = self.func_registry.as_ref().ok_or_else(|| {
12326                            FrankenError::Internal(
12327                                "AggStep opcode executed without function registry".to_owned(),
12328                            )
12329                        })?;
12330                        let func =
12331                            registry
12332                                .find_aggregate(func_name, arg_count)
12333                                .ok_or_else(|| {
12334                                    FrankenError::Internal(format!(
12335                                        "no such aggregate function: {func_name}/{arg_count}",
12336                                    ))
12337                                })?;
12338                        self.aggregate_function_cache
12339                            .insert(func_pc, Arc::clone(&func));
12340                        func
12341                    };
12342
12343                    let accum_reg = op.p3;
12344                    let is_distinct = op.p1 != 0;
12345                    let arg_count = usize::from(op.p5);
12346                    let execution_cx = self.execution_cx.clone();
12347                    if op.p2 >= 0
12348                        && (op.p2 as usize).saturating_add(arg_count) <= self.registers.len()
12349                    {
12350                        for offset in 0..arg_count {
12351                            if let Some(reg) = Self::reg_with_offset(op.p2, offset) {
12352                                self.materialize_make_record_sideband(reg);
12353                            }
12354                        }
12355                        let start_idx = op.p2 as usize;
12356                        let args = &self.registers[start_idx..start_idx + arg_count];
12357                        Self::agg_step_with_args(
12358                            &mut self.cold_state,
12359                            &mut self.statement_cold_state,
12360                            AggStepCall {
12361                                accum_reg,
12362                                is_distinct,
12363                                func: &func,
12364                                func_name,
12365                                agg_collation,
12366                                execution_cx: &execution_cx,
12367                                args,
12368                            },
12369                        )?;
12370                    } else {
12371                        let args = self.collect_reg_range(op.p2, arg_count);
12372                        Self::agg_step_with_args(
12373                            &mut self.cold_state,
12374                            &mut self.statement_cold_state,
12375                            AggStepCall {
12376                                accum_reg,
12377                                is_distinct,
12378                                func: &func,
12379                                func_name,
12380                                agg_collation,
12381                                execution_cx: &execution_cx,
12382                                args: &args,
12383                            },
12384                        )?;
12385                    }
12386                    pc += 1;
12387                }
12388
12389                Opcode::AggFinal => {
12390                    let func_name = match &op.p4 {
12391                        P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12392                        _ => {
12393                            return Err(FrankenError::Internal(
12394                                "AggFinal opcode missing P4::FuncName".to_owned(),
12395                            ));
12396                        }
12397                    };
12398
12399                    let arg_count = op.p2;
12400                    let func_pc = pc;
12401                    let func = if let Some(func) = self.aggregate_function_cache.get(&func_pc) {
12402                        Arc::clone(func)
12403                    } else {
12404                        let registry = self.func_registry.as_ref().ok_or_else(|| {
12405                            FrankenError::Internal(
12406                                "AggFinal opcode executed without function registry".to_owned(),
12407                            )
12408                        })?;
12409                        let func =
12410                            registry
12411                                .find_aggregate(func_name, arg_count)
12412                                .ok_or_else(|| {
12413                                    FrankenError::Internal(format!(
12414                                        "no such aggregate function: {func_name}/{arg_count}",
12415                                    ))
12416                                })?;
12417                        self.aggregate_function_cache
12418                            .insert(func_pc, Arc::clone(&func));
12419                        func
12420                    };
12421
12422                    let accum_reg = op.p1;
12423                    observe_execution_cancellation(&self.execution_cx)?;
12424                    let result = match self
12425                        .cold_state_mut()
12426                        .and_then(|cold_state| cold_state.aggregates.remove(&accum_reg))
12427                    {
12428                        Some(ctx) => {
12429                            if !Arc::ptr_eq(&ctx.func, &func) {
12430                                return Err(FrankenError::Internal(
12431                                    "AggFinal accumulator used for a different aggregate"
12432                                        .to_owned(),
12433                                ));
12434                            }
12435                            ctx.func.finalize(ctx.state)?
12436                        }
12437                        None => func.finalize(func.initial_state())?,
12438                    };
12439
12440                    observe_execution_cancellation(&self.execution_cx)?;
12441                    self.set_reg(accum_reg, result);
12442                    pc += 1;
12443                }
12444
12445                Opcode::AggInverse => {
12446                    // Inverse aggregate step for window functions.
12447                    // Remove a row from the sliding window frame.
12448                    // P4 = function name, P2 = first arg register,
12449                    // P5 = arg count, P3 = accumulator register.
12450                    let func_name = match &op.p4 {
12451                        P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12452                        _ => {
12453                            return Err(FrankenError::Internal(
12454                                "AggInverse opcode missing P4::FuncName".to_owned(),
12455                            ));
12456                        }
12457                    };
12458
12459                    let registry = self.func_registry.as_ref().ok_or_else(|| {
12460                        FrankenError::Internal(
12461                            "AggInverse opcode executed without function registry".to_owned(),
12462                        )
12463                    })?;
12464
12465                    let arg_count = i32::from(op.p5);
12466                    let func = registry.find_window(func_name, arg_count).ok_or_else(|| {
12467                        FrankenError::Internal(format!(
12468                            "no such window function: {func_name}/{arg_count}",
12469                        ))
12470                    })?;
12471
12472                    let accum_reg = op.p3;
12473                    let args = self.collect_reg_range(op.p2, usize::from(op.p5));
12474                    let execution_cx = self.execution_cx.clone();
12475                    let ctx = self
12476                        .ensure_cold_state_for(StatementColdState::WINDOW_CONTEXTS)
12477                        .window_contexts
12478                        .entry_or_insert_with(accum_reg, || {
12479                            let state = func.initial_state();
12480                            WindowContext {
12481                                func: func.clone(),
12482                                state,
12483                            }
12484                        });
12485
12486                    observe_execution_cancellation(&execution_cx)?;
12487                    ctx.func.inverse(&mut ctx.state, &args)?;
12488                    observe_execution_cancellation(&execution_cx)?;
12489                    pc += 1;
12490                }
12491
12492                Opcode::AggValue => {
12493                    // Extract the current intermediate value from the
12494                    // window accumulator in register P3, storing the
12495                    // result in register P3. Unlike AggFinal, this
12496                    // does NOT consume the accumulator.
12497                    // P4 = function name, P1 = accumulator register,
12498                    // P3 = destination register.
12499                    let func_name = match &op.p4 {
12500                        P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12501                        _ => {
12502                            return Err(FrankenError::Internal(
12503                                "AggValue opcode missing P4::FuncName".to_owned(),
12504                            ));
12505                        }
12506                    };
12507
12508                    let registry = self.func_registry.as_ref().ok_or_else(|| {
12509                        FrankenError::Internal(
12510                            "AggValue opcode executed without function registry".to_owned(),
12511                        )
12512                    })?;
12513
12514                    let arg_count = op.p2;
12515                    let func = registry.find_window(func_name, arg_count).ok_or_else(|| {
12516                        FrankenError::Internal(format!(
12517                            "no such window function: {func_name}/{arg_count}",
12518                        ))
12519                    })?;
12520
12521                    let accum_reg = op.p1;
12522                    observe_execution_cancellation(&self.execution_cx)?;
12523                    let result = match self
12524                        .cold_state()
12525                        .and_then(|cold_state| cold_state.window_contexts.get(&accum_reg))
12526                    {
12527                        Some(ctx) => ctx.func.value(&ctx.state)?,
12528                        None => func.value(&func.initial_state())?,
12529                    };
12530                    observe_execution_cancellation(&self.execution_cx)?;
12531                    self.set_reg(op.p3, result);
12532                    pc += 1;
12533                }
12534
12535                // ── Scalar function call ──────────────────────────────────
12536                //
12537                // Function/PureFunc: p1 = constant-p5-flags, p2 = first-arg register,
12538                // p3 = output register, p4 = FuncName, p5 = arg count.
12539                // Arguments are in registers p2..p2+p5.
12540                Opcode::Function | Opcode::PureFunc => {
12541                    let func_name = match &op.p4 {
12542                        P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12543                        _ => {
12544                            return Err(FrankenError::Internal(
12545                                "Function opcode missing P4::FuncName".to_owned(),
12546                            ));
12547                        }
12548                    };
12549                    let arg_count = op.p5 as usize;
12550                    let first_arg_reg = op.p2;
12551                    let output_reg = op.p3;
12552
12553                    let func_pc = pc;
12554                    #[allow(clippy::cast_possible_wrap)]
12555                    let (func, schema_safety, function_consumes_argument_collation) =
12556                        if let Some(resolved) = self.scalar_function_cache.get(&func_pc) {
12557                            (
12558                                resolved.function(),
12559                                resolved.schema_safety(),
12560                                resolved.consumes_argument_collation(),
12561                            )
12562                        } else {
12563                            let registry = self.func_registry.as_ref().ok_or_else(|| {
12564                                FrankenError::Internal(
12565                                    "Function opcode executed without function registry".to_owned(),
12566                                )
12567                            })?;
12568                            let resolved = registry
12569                                .resolve_scalar_precanonical(func_name, arg_count as i32)
12570                                .or_else(|| registry.resolve_scalar(func_name, arg_count as i32))
12571                                .ok_or_else(|| {
12572                                    registry
12573                                        .resolve_application_function(func_name, arg_count as i32)
12574                                        .filter(|resolution| {
12575                                            resolution.kind() != ApplicationFunctionKind::Scalar
12576                                        })
12577                                        .map_or_else(
12578                                            || {
12579                                                FrankenError::Internal(format!(
12580                                                    "no such function: {func_name}/{arg_count}",
12581                                                ))
12582                                            },
12583                                            |resolution| {
12584                                                FrankenError::function_error(format!(
12585                                                    "misuse of {} function {}()",
12586                                                    resolution.kind().label(),
12587                                                    func_name.to_ascii_lowercase(),
12588                                                ))
12589                                            },
12590                                        )
12591                                })?;
12592                            let func = resolved.function();
12593                            let schema_safety = resolved.schema_safety();
12594                            let consumes_argument_collation =
12595                                resolved.consumes_argument_collation();
12596                            self.scalar_function_cache.insert(func_pc, resolved);
12597                            (func, schema_safety, consumes_argument_collation)
12598                        };
12599
12600                    // Gather per-argument subtypes (cheap when none are set:
12601                    // `has_subtypes` short-circuits before any HashMap probes).
12602                    // JSON constructors consume these so a `json('[1]')` argument
12603                    // is embedded as a JSON value instead of a quoted string.
12604                    let arg_subtypes: smallvec::SmallVec<[u32; 4]> = (0..arg_count)
12605                        .map(|i| {
12606                            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
12607                            let reg = first_arg_reg + i as i32;
12608                            self.register_subtype(reg).unwrap_or(0)
12609                        })
12610                        .collect();
12611                    let any_arg_subtype = arg_subtypes.iter().any(|&st| st != 0);
12612                    let function_collation = if function_consumes_argument_collation {
12613                        match &op.p4 {
12614                            P4::FuncNameCollated(_, collation_name) => {
12615                                let registry = self
12616                                    .collation_registry
12617                                    .lock()
12618                                    .unwrap_or_else(std::sync::PoisonError::into_inner);
12619                                Some(registry.find(collation_name).ok_or_else(|| {
12620                                    FrankenError::function_error(format!(
12621                                        "no such collation sequence: {collation_name}"
12622                                    ))
12623                                })?)
12624                            }
12625                            P4::FuncName(_) => None,
12626                            _ => unreachable!("function P4 was validated above"),
12627                        }
12628                    } else {
12629                        None
12630                    };
12631
12632                    // Use a direct slice into the register file instead of
12633                    // allocating a SmallVec via collect_reg_range.  Same pattern as
12634                    // AggStep.  Falls back to collect_reg_range only when the
12635                    // register range is out of bounds or negative.
12636                    let result = if first_arg_reg >= 0
12637                        && (first_arg_reg as usize).saturating_add(arg_count)
12638                            <= self.registers.len()
12639                    {
12640                        let start_idx = first_arg_reg as usize;
12641                        let end_idx = start_idx + arg_count;
12642                        let args = &self.registers[start_idx..end_idx];
12643                        if let Some(context) = SchemaEvaluationContext::from_function_p1(op.p1) {
12644                            validate_schema_function_invocation(
12645                                context,
12646                                func_name,
12647                                schema_safety,
12648                                args,
12649                            )?;
12650                        }
12651                        observe_execution_cancellation(&self.execution_cx)?;
12652                        if function_consumes_argument_collation {
12653                            func.invoke_with_collation(args, function_collation.as_deref())?
12654                        } else if any_arg_subtype {
12655                            func.invoke_with_arg_subtypes(args, &arg_subtypes)?
12656                        } else {
12657                            func.invoke(args)?
12658                        }
12659                    } else {
12660                        let args = self.collect_reg_range(first_arg_reg, arg_count);
12661                        if let Some(context) = SchemaEvaluationContext::from_function_p1(op.p1) {
12662                            validate_schema_function_invocation(
12663                                context,
12664                                func_name,
12665                                schema_safety,
12666                                &args,
12667                            )?;
12668                        }
12669                        observe_execution_cancellation(&self.execution_cx)?;
12670                        if function_consumes_argument_collation {
12671                            func.invoke_with_collation(&args, function_collation.as_deref())?
12672                        } else if any_arg_subtype {
12673                            func.invoke_with_arg_subtypes(&args, &arg_subtypes)?
12674                        } else {
12675                            func.invoke(&args)?
12676                        }
12677                    };
12678                    observe_execution_cancellation(&self.execution_cx)?;
12679
12680                    // Propagate this function's result subtype (e.g. JSON) to the
12681                    // destination register so a nested `json(...)` keeps its tag.
12682                    let result_subtype = func.result_subtype();
12683
12684                    if self.trace_opcodes {
12685                        let result_type = match &result {
12686                            SqliteValue::Null => "null",
12687                            SqliteValue::Integer(_) => "integer",
12688                            SqliteValue::Float(_) => "real",
12689                            SqliteValue::Text(_) => "text",
12690                            SqliteValue::Blob(_) => "blob",
12691                        };
12692                        tracing::trace!(
12693                            target: "fsqlite_func::eval",
12694                            func_name,
12695                            arg_count,
12696                            result_type,
12697                            "func_eval",
12698                        );
12699                    }
12700
12701                    // Update global call count (fast path: no Instant::now).
12702                    fsqlite_func::record_func_call_count_only();
12703
12704                    self.set_reg(output_reg, result);
12705                    // set_reg clears any prior subtype on the destination, so
12706                    // (re)apply this function's result subtype afterwards.
12707                    if let Some(subtype) = result_subtype {
12708                        self.set_register_subtype(output_reg, subtype);
12709                    }
12710                    pc += 1;
12711                }
12712
12713                Opcode::LikeConstFast => {
12714                    let kind = SqlLikeFastPathKind::from_opcode_tag(op.p3).ok_or_else(|| {
12715                        FrankenError::Internal(
12716                            "LikeConstFast opcode has invalid fast-path tag".to_owned(),
12717                        )
12718                    })?;
12719                    let literal = match &op.p4 {
12720                        P4::Str(text) => text.as_str(),
12721                        _ => {
12722                            return Err(FrankenError::Internal(
12723                                "LikeConstFast opcode missing P4::Str literal".to_owned(),
12724                            ));
12725                        }
12726                    };
12727                    let input = self.get_reg(op.p1);
12728                    // Honor `PRAGMA case_sensitive_like` (set per-statement into a
12729                    // thread-local by the Connection before execution).
12730                    let case_sensitive = fsqlite_func::case_sensitive_like_active();
12731                    let result = if input.is_null() {
12732                        SqliteValue::Null
12733                    } else {
12734                        let matched = match input {
12735                            SqliteValue::Text(text) => sql_like_fast_path_matches_cased(
12736                                kind,
12737                                literal,
12738                                text,
12739                                case_sensitive,
12740                            ),
12741                            SqliteValue::Blob(bytes) => {
12742                                let text = String::from_utf8_lossy(bytes);
12743                                sql_like_fast_path_matches_cased(
12744                                    kind,
12745                                    literal,
12746                                    &text,
12747                                    case_sensitive,
12748                                )
12749                            }
12750                            _ => {
12751                                let text = input.to_text();
12752                                sql_like_fast_path_matches_cased(
12753                                    kind,
12754                                    literal,
12755                                    &text,
12756                                    case_sensitive,
12757                                )
12758                            }
12759                        };
12760                        let final_match = if op.p5 != 0 { !matched } else { matched };
12761                        SqliteValue::Integer(i64::from(final_match))
12762                    };
12763                    self.set_reg(op.p2, result);
12764                    pc += 1;
12765                }
12766
12767                // ── LIMIT/OFFSET support ────────────────────────────────
12768                // DecrJumpZero: decrement register p1; if result is zero
12769                // jump to p2. If value is initially zero or negative, do nothing.
12770                // Used to count down remaining LIMIT rows.
12771                Opcode::DecrJumpZero => {
12772                    let mut val = self.get_reg(op.p1).to_integer();
12773                    if val > 0 {
12774                        val -= 1;
12775                        self.set_reg_int(op.p1, val);
12776                        if val == 0 {
12777                            #[allow(clippy::cast_sign_loss)]
12778                            {
12779                                pc = op.p2 as usize;
12780                            }
12781                        } else {
12782                            pc += 1;
12783                        }
12784                    } else {
12785                        pc += 1;
12786                    }
12787                }
12788
12789                // IfPos: if register p1 > 0, subtract p3, then jump to p2.
12790                // Used for OFFSET counting (skip rows while offset > 0).
12791                Opcode::IfPos => {
12792                    let val = self.get_reg(op.p1).to_integer();
12793                    if val > 0 {
12794                        let decremented = val - i64::from(op.p3);
12795                        self.set_reg_int(op.p1, decremented);
12796                        #[allow(clippy::cast_sign_loss)]
12797                        {
12798                            pc = op.p2 as usize;
12799                        }
12800                    } else {
12801                        pc += 1;
12802                    }
12803                }
12804
12805                // ── RowSet operations ──────────────────────────────────
12806                // Used by OR-optimized queries and IN subqueries.
12807                Opcode::RowSetAdd => {
12808                    // Add integer P2 to rowset in register P1.
12809                    let rowset_reg = op.p1;
12810                    let val = self.get_reg(op.p2).to_integer();
12811                    self.ensure_cold_state_for(StatementColdState::ROWSETS)
12812                        .rowsets
12813                        .entry_or_insert_with(rowset_reg, RowSet::new)
12814                        .add(val);
12815                    pc += 1;
12816                }
12817
12818                Opcode::RowSetRead => {
12819                    // Read next value from rowset P1 into register P3;
12820                    // jump to P2 when exhausted.
12821                    let rowset_reg = op.p1;
12822                    let next_val = self
12823                        .cold_state_mut()
12824                        .and_then(|cold_state| cold_state.rowsets.get_mut(&rowset_reg))
12825                        .and_then(|rs| rs.read_next());
12826                    match next_val {
12827                        Some(val) => {
12828                            self.set_reg_int(op.p3, val);
12829                            pc += 1;
12830                        }
12831                        None => {
12832                            pc = op.p2 as usize;
12833                        }
12834                    }
12835                }
12836
12837                Opcode::RowSetTest => {
12838                    // Test if P3 exists in rowset P1; jump to P2 if found.
12839                    // If not found, add P3 to the rowset and fall through.
12840                    let rowset_reg = op.p1;
12841                    let val = self.get_reg(op.p3).to_integer();
12842                    let found = self
12843                        .cold_state()
12844                        .and_then(|cold_state| cold_state.rowsets.get(&rowset_reg))
12845                        .is_some_and(|rs| rs.contains(val));
12846                    if found {
12847                        pc = op.p2 as usize;
12848                    } else {
12849                        self.ensure_cold_state_for(StatementColdState::ROWSETS)
12850                            .rowsets
12851                            .entry_or_insert_with(rowset_reg, RowSet::new)
12852                            .add(val);
12853                        pc += 1;
12854                    }
12855                }
12856
12857                // ── Foreign Key counters ──────────────────────────────
12858                Opcode::FkCounter => {
12859                    // P1=0 → immediate FK counter, P1=1 → deferred.
12860                    // P2 = delta to add (positive or negative).
12861                    self.fk_counter += i64::from(op.p2);
12862                    pc += 1;
12863                }
12864
12865                Opcode::FkIfZero => {
12866                    // Jump to P2 if FK counter is zero.
12867                    // P1=0 → immediate, P1=1 → deferred.
12868                    if self.fk_counter == 0 {
12869                        pc = op.p2 as usize;
12870                    } else {
12871                        pc += 1;
12872                    }
12873                }
12874
12875                // ── MemMax: P2 = max(P2, P1) ─────────────────────────
12876                Opcode::MemMax => {
12877                    let val1 = self.get_reg(op.p1).to_integer();
12878                    let val2 = self.get_reg(op.p2).to_integer();
12879                    if val1 > val2 {
12880                        self.set_reg_int(op.p2, val1);
12881                    }
12882                    pc += 1;
12883                }
12884
12885                // ── OffsetLimit ───────────────────────────────────────
12886                // Compute the combined LIMIT+OFFSET value.
12887                // P1 = LIMIT, P2 = OFFSET output register,
12888                // P3 = combined output register.
12889                // If LIMIT is negative (no limit), store -1 in P3.
12890                // Otherwise store LIMIT+OFFSET in P3.
12891                Opcode::OffsetLimit => {
12892                    let limit = self.get_reg(op.p1).to_integer();
12893                    let offset = self.get_reg(op.p2).to_integer();
12894                    let combined = if limit < 0 {
12895                        -1
12896                    } else {
12897                        limit.saturating_add(offset)
12898                    };
12899                    self.set_reg_int(op.p3, combined);
12900                    pc += 1;
12901                }
12902
12903                // ── IfNotZero: jump if P1 != 0, decrement by 1 ───────
12904                Opcode::IfNotZero => {
12905                    let val = self.get_reg(op.p1).to_integer();
12906                    if val != 0 {
12907                        self.set_reg_int(op.p1, val.wrapping_sub(1));
12908                        pc = op.p2 as usize;
12909                    } else {
12910                        pc += 1;
12911                    }
12912                }
12913
12914                // ── Page info ────────────────────────────────────────
12915                Opcode::Pagecount => {
12916                    // Store the total page count of database P1 into register P2.
12917                    // In memory mode, approximate as number of tables.
12918                    let count = self.db.as_ref().map_or(0, |db| db.table_count());
12919                    self.set_reg(op.p2, SqliteValue::Integer(i64::from(count)));
12920                    pc += 1;
12921                }
12922
12923                Opcode::MaxPgcnt => {
12924                    // Return/set max page count. For now, return a large value.
12925                    self.set_reg(op.p2, SqliteValue::Integer(1_073_741_823));
12926                    pc += 1;
12927                }
12928
12929                // ── Journal mode ─────────────────────────────────────
12930                Opcode::JournalMode => {
12931                    // Return current journal mode as text in register P2.
12932                    // FrankenSQLite defaults to WAL mode.
12933                    self.set_reg(op.p2, SqliteValue::Text(SmallText::new("wal")));
12934                    pc += 1;
12935                }
12936
12937                // ── Vacuum ───────────────────────────────────────────
12938                Opcode::Vacuum | Opcode::IncrVacuum => {
12939                    // In the in-memory engine, vacuum is a no-op.
12940                    // IncrVacuum: jump to P2 when done (always done immediately).
12941                    if op.opcode == Opcode::IncrVacuum {
12942                        pc = op.p2 as usize;
12943                    } else {
12944                        pc += 1;
12945                    }
12946                }
12947
12948                // ── Integrity check ──────────────────────────────────
12949                Opcode::IntegrityCk => {
12950                    // Run integrity check. For now, always report OK.
12951                    // P1 = root page register, P2 = output register,
12952                    // P3 = number of tables to check.
12953                    self.set_reg(op.p2, SqliteValue::Text(SmallText::new("ok")));
12954                    pc += 1;
12955                }
12956
12957                // ── Expire ───────────────────────────────────────────
12958                Opcode::Expire => {
12959                    // Mark prepared statement as expired (no-op; we don't
12960                    // cache prepared statements yet).
12961                    pc += 1;
12962                }
12963
12964                // ── Cursor lock/unlock ───────────────────────────────
12965                Opcode::CursorLock | Opcode::CursorUnlock => {
12966                    // Advisory cursor locking. No-op in single-process mode.
12967                    pc += 1;
12968                }
12969
12970                // ── Subtype operations ───────────────────────────────
12971                // Subtypes tag registers with metadata (e.g. JSON
12972                // subtype 74/'J') without changing the stored value.
12973                Opcode::ClrSubtype => {
12974                    // Clear subtype flag on register P1.
12975                    self.clear_register_subtype(op.p1);
12976                    pc += 1;
12977                }
12978
12979                Opcode::GetSubtype => {
12980                    // Store the subtype of register P1 into register P2.
12981                    // Returns 0 if no subtype is set.
12982                    let st = self.register_subtype(op.p1).unwrap_or(0);
12983                    #[allow(clippy::cast_possible_wrap)]
12984                    self.set_reg(op.p2, SqliteValue::Integer(st as i64));
12985                    pc += 1;
12986                }
12987
12988                Opcode::SetSubtype => {
12989                    // Set the subtype of register P2 from the integer
12990                    // value in register P1.
12991                    let val = self.get_reg(op.p1);
12992                    #[allow(clippy::cast_sign_loss)]
12993                    let st = match val {
12994                        SqliteValue::Integer(i) => *i as u32,
12995                        _ => 0,
12996                    };
12997                    self.set_register_subtype(op.p2, st);
12998                    pc += 1;
12999                }
13000
13001                // ── Bloom filter ─────────────────────────────────────
13002                // Bloom filters provide early rejection during index
13003                // lookups. P1 is the filter register, P3 the hash key
13004                // register, P2 the jump target (for Filter).
13005                Opcode::FilterAdd => {
13006                    // Add hash of register P3 to the Bloom filter
13007                    // identified by P1.
13008                    let hash = bloom_hash(self.get_reg(op.p3));
13009                    let filter = self
13010                        .ensure_cold_state_for(StatementColdState::BLOOM_FILTERS)
13011                        .bloom_filters
13012                        .entry(op.p1)
13013                        .or_insert_with(|| vec![0u64; BLOOM_FILTER_WORDS]);
13014                    let bit = (hash as usize) % (filter.len() * 64);
13015                    filter[bit / 64] |= 1u64 << (bit % 64);
13016                    pc += 1;
13017                }
13018
13019                Opcode::Filter => {
13020                    // Test Bloom filter P1 for register P3's hash.
13021                    // Jump to P2 if definitely not present.
13022                    if let Some(filter) = self
13023                        .cold_state()
13024                        .and_then(|cold_state| cold_state.bloom_filters.get(&op.p1))
13025                    {
13026                        let hash = bloom_hash(self.get_reg(op.p3));
13027                        let bit = (hash as usize) % (filter.len() * 64);
13028                        let present = (filter[bit / 64] >> (bit % 64)) & 1 == 1;
13029                        if !present {
13030                            pc = op.p2 as usize;
13031                        } else {
13032                            pc += 1;
13033                        }
13034                    } else {
13035                        // No filter exists — conservatively fall through.
13036                        pc += 1;
13037                    }
13038                }
13039
13040                // ── Hints & debug ────────────────────────────────────
13041                Opcode::CursorHint | Opcode::Trace | Opcode::Abortable | Opcode::ReleaseReg => {
13042                    // Advisory/debug opcodes. No-op.
13043                    pc += 1;
13044                }
13045
13046                // ── Virtual Table opcodes ───────────────────────────────
13047                Opcode::VOpen => {
13048                    // Open a virtual table cursor.
13049                    // P1 = cursor number.
13050                    // If a vtab instance is registered, call open_cursor().
13051                    let cursor_id = op.p1;
13052                    if !self
13053                        .cold_state()
13054                        .is_some_and(|cold_state| cold_state.vtab_cursors.contains_key(&cursor_id))
13055                    {
13056                        if let Some(vtab) = self.vtab_instances.get(&cursor_id) {
13057                            match vtab.open_cursor() {
13058                                Ok(cursor) => {
13059                                    self.register_vtab_cursor(cursor_id, cursor);
13060                                }
13061                                Err(e) => {
13062                                    break vtab_exec_outcome("VOpen", e)?;
13063                                }
13064                            }
13065                        }
13066                    }
13067                    pc += 1;
13068                }
13069
13070                Opcode::VFilter => {
13071                    // Apply filter to virtual table cursor and begin scan.
13072                    // P1 = cursor number
13073                    // P2 = jump address if cursor is empty after filter
13074                    // P3 = register holding idx_num; P3+1 = argc; P3+2.. = argv
13075                    // P4 = optional idx_str
13076                    let cursor_id = op.p1;
13077                    let jump_if_empty = op.p2;
13078                    let cx = self.derive_execution_cx();
13079                    let idx_num_reg = self.get_reg(op.p3).to_integer();
13080                    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
13081                    let idx_num = idx_num_reg as i32;
13082                    let argc = self.get_reg(op.p3 + 1).to_integer();
13083                    let n_args = usize::try_from(argc).unwrap_or(0);
13084                    let args: Vec<SqliteValue> = (0..n_args)
13085                        .map(|i| {
13086                            #[allow(clippy::cast_possible_wrap)]
13087                            self.registers
13088                                .get((op.p3 + 2 + i as i32) as usize)
13089                                .cloned()
13090                                .unwrap_or(SqliteValue::Null)
13091                        })
13092                        .collect();
13093                    let idx_str = match &op.p4 {
13094                        P4::Str(s) => Some(s.as_str()),
13095                        _ => None,
13096                    };
13097
13098                    if let Some(state) = self
13099                        .cold_state_mut()
13100                        .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&cursor_id))
13101                    {
13102                        observe_execution_cancellation(&cx)?;
13103                        if let Err(e) = state.cursor.filter(&cx, idx_num, idx_str, &args) {
13104                            break vtab_exec_outcome("VFilter", e)?;
13105                        }
13106                        state.null_row = false;
13107                        observe_execution_cancellation(&cx)?;
13108                        if state.cursor.eof() {
13109                            #[allow(clippy::cast_sign_loss)]
13110                            {
13111                                pc = jump_if_empty as usize;
13112                            }
13113                            continue;
13114                        }
13115                    } else {
13116                        #[allow(clippy::cast_sign_loss)]
13117                        {
13118                            pc = jump_if_empty as usize;
13119                        }
13120                        continue;
13121                    }
13122                    pc += 1;
13123                }
13124
13125                Opcode::VColumn => {
13126                    // Read column from virtual table cursor.
13127                    // P1 = cursor number
13128                    // P2 = column index
13129                    // P3 = destination register
13130                    let cursor_id = op.p1;
13131                    let col = op.p2;
13132                    let dest = op.p3;
13133
13134                    let column_value = if let Some(state) = self
13135                        .cold_state()
13136                        .and_then(|cold_state| cold_state.vtab_cursors.get(&cursor_id))
13137                    {
13138                        if state.null_row || state.cursor.eof() {
13139                            SqliteValue::Null
13140                        } else {
13141                            observe_execution_cancellation(&self.execution_cx)?;
13142                            let mut ctx = ColumnContext::new();
13143                            if let Err(e) = state.cursor.column(&mut ctx, col) {
13144                                break vtab_exec_outcome("VColumn", e)?;
13145                            }
13146                            observe_execution_cancellation(&self.execution_cx)?;
13147                            ctx.take_value().unwrap_or(SqliteValue::Null)
13148                        }
13149                    } else {
13150                        SqliteValue::Null
13151                    };
13152                    self.set_reg(dest, column_value);
13153                    pc += 1;
13154                }
13155
13156                Opcode::VNext => {
13157                    // Advance virtual table cursor to the next row.
13158                    // P1 = cursor number
13159                    // P2 = jump address to loop body (go back if not eof)
13160                    let cursor_id = op.p1;
13161                    let jump_if_more = op.p2;
13162                    let cx = self.derive_execution_cx();
13163
13164                    if let Some(state) = self
13165                        .cold_state_mut()
13166                        .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&cursor_id))
13167                    {
13168                        if state.null_row {
13169                            pc += 1;
13170                            continue;
13171                        }
13172                        observe_execution_cancellation(&cx)?;
13173                        if let Err(e) = state.cursor.next(&cx) {
13174                            break vtab_exec_outcome("VNext", e)?;
13175                        }
13176                        observe_execution_cancellation(&cx)?;
13177                        if !state.cursor.eof() {
13178                            #[allow(clippy::cast_sign_loss)]
13179                            {
13180                                pc = jump_if_more as usize;
13181                            }
13182                            continue;
13183                        }
13184                    }
13185                    pc += 1;
13186                }
13187
13188                Opcode::VUpdate => {
13189                    // INSERT/UPDATE/DELETE on a virtual table.
13190                    // P1 = cursor number, P2 = arg count, P3 = first arg reg
13191                    let cursor_id = op.p1;
13192                    let n_args = op.p2;
13193                    let first_reg = op.p3;
13194                    let dest_reg = op.p5 as i32;
13195                    let cx = self.derive_execution_cx();
13196                    #[allow(clippy::cast_sign_loss)]
13197                    let args: Vec<SqliteValue> = (0..n_args)
13198                        .map(|i| {
13199                            self.registers
13200                                .get((first_reg + i) as usize)
13201                                .cloned()
13202                                .unwrap_or(SqliteValue::Null)
13203                        })
13204                        .collect();
13205                    observe_execution_cancellation(&cx)?;
13206                    let vtab_update_result =
13207                        if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13208                            match vtab.vtab_update(&cx, &args) {
13209                                Ok(Some(rowid)) => SqliteValue::Integer(rowid),
13210                                Ok(None) => SqliteValue::Null,
13211                                Err(e) => {
13212                                    break vtab_exec_outcome("VUpdate", e)?;
13213                                }
13214                            }
13215                        } else {
13216                            SqliteValue::Null
13217                        };
13218                    observe_execution_cancellation(&cx)?;
13219                    #[allow(clippy::cast_sign_loss)]
13220                    if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13221                        *reg = vtab_update_result;
13222                    }
13223                    pc += 1;
13224                }
13225
13226                Opcode::VBegin => {
13227                    // Begin a virtual table transaction.
13228                    // P1 = cursor number identifying the vtab instance.
13229                    let cursor_id = op.p1;
13230                    let cx = self.derive_execution_cx();
13231                    observe_execution_cancellation(&cx)?;
13232                    if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13233                        if let Err(e) = vtab.begin(&cx) {
13234                            break vtab_exec_outcome("VBegin", e)?;
13235                        }
13236                    }
13237                    observe_execution_cancellation(&cx)?;
13238                    pc += 1;
13239                }
13240
13241                Opcode::VCreate => {
13242                    // Create a virtual table — handled at Connection layer.
13243                    pc += 1;
13244                }
13245
13246                Opcode::VDestroy => {
13247                    // Destroy a virtual table — handled at Connection layer.
13248                    pc += 1;
13249                }
13250
13251                Opcode::VCheck => {
13252                    // Check virtual table integrity.
13253                    // P2 = destination register for error message (NULL if OK).
13254                    // P3 carries the integer xIntegrity() argument and must
13255                    // not be clobbered by this stub implementation.
13256                    let dest_reg = op.p2;
13257                    #[allow(clippy::cast_sign_loss)]
13258                    if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13259                        *reg = SqliteValue::Null;
13260                    }
13261                    pc += 1;
13262                }
13263
13264                Opcode::VInitIn => {
13265                    // Initialize IN constraint for virtual table.
13266                    // P2 = register containing the IN value list
13267                    // P3 = destination register
13268                    let _cursor_id = op.p1;
13269                    let src_reg = op.p2;
13270                    let dest_reg = op.p3;
13271                    #[allow(clippy::cast_sign_loss)]
13272                    {
13273                        let val = self
13274                            .registers
13275                            .get(src_reg as usize)
13276                            .cloned()
13277                            .unwrap_or(SqliteValue::Null);
13278                        if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13279                            *reg = val;
13280                        }
13281                    }
13282                    pc += 1;
13283                }
13284
13285                Opcode::VRename => {
13286                    // Rename a virtual table.
13287                    // P1 = cursor number for the vtab instance
13288                    // P4 = new table name (via P4::Str)
13289                    let cursor_id = op.p1;
13290                    let cx = self.derive_execution_cx();
13291                    observe_execution_cancellation(&cx)?;
13292                    if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13293                        let new_name = match &op.p4 {
13294                            P4::Str(s) => s.as_str(),
13295                            _ => "",
13296                        };
13297                        if let Err(e) = vtab.rename(&cx, new_name) {
13298                            break vtab_exec_outcome("VRename", e)?;
13299                        }
13300                    }
13301                    observe_execution_cancellation(&cx)?;
13302                    pc += 1;
13303                }
13304
13305                // ── Catch-all for future opcodes ─────────────────────
13306                #[allow(unreachable_patterns)]
13307                _ => {
13308                    break ExecOutcome::Error {
13309                        code: 1,
13310                        message: format!("unimplemented opcode {:?} at pc={}", op.opcode, pc),
13311                    };
13312                }
13313            }
13314        };
13315
13316        // ── Post-execution metrics and tracing (bd-1rw.1) ──────────────────
13317        if !needs_statement_timing {
13318            return Ok(outcome);
13319        }
13320
13321        let elapsed = start_time
13322            .expect("statement timing state exists when post-execution bookkeeping is enabled")
13323            .elapsed();
13324        let elapsed_us = elapsed.as_micros();
13325        let result_rows = self.results.len();
13326
13327        if collect_vdbe_metrics {
13328            let local_opcode_execution_totals = local_opcode_execution_totals
13329                .as_deref()
13330                .expect("opcode metrics buffer exists when VDBE metrics are enabled");
13331            FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.fetch_add(opcode_count, AtomicOrdering::Relaxed);
13332            FSQLITE_VDBE_STATEMENTS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
13333            #[allow(clippy::cast_possible_truncation)]
13334            FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
13335                .fetch_add(elapsed_us as u64, AtomicOrdering::Relaxed);
13336            for (idx, total) in local_opcode_execution_totals.iter().enumerate().skip(1) {
13337                if *total == 0 {
13338                    continue;
13339                }
13340                FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS[idx]
13341                    .fetch_add(*total, AtomicOrdering::Relaxed);
13342            }
13343        }
13344
13345        let log_statement_done = || {
13346            if statement_debug_enabled {
13347                tracing::debug!(
13348                    target: "fsqlite_vdbe::statement",
13349                    program_id,
13350                    opcode_count,
13351                    result_rows,
13352                    elapsed_us = elapsed_us as u64,
13353                    outcome = ?outcome,
13354                    "vdbe statement done",
13355                );
13356            }
13357
13358            if slow_query_info_enabled && elapsed.as_millis() >= SLOW_QUERY_THRESHOLD_MS {
13359                #[allow(clippy::cast_possible_truncation)]
13360                let millis = elapsed.as_millis() as u64;
13361                tracing::info!(
13362                    target: "fsqlite_vdbe::slow_query",
13363                    program_id,
13364                    opcode_count,
13365                    result_rows,
13366                    elapsed_ms = millis,
13367                    "slow vdbe statement",
13368                );
13369            }
13370        };
13371
13372        if exec_info_enabled {
13373            let span = tracing::info_span!(
13374                target: "fsqlite_vdbe",
13375                "vdbe_exec",
13376                opcode_count,
13377                program_id,
13378                result_rows,
13379                elapsed_us = elapsed_us as u64,
13380            );
13381            let _guard = span.enter();
13382            log_statement_done();
13383        } else {
13384            log_statement_done();
13385        }
13386
13387        Ok(outcome)
13388    }
13389
13390    /// Get the collected result rows.
13391    pub fn results(&self) -> &[smallvec::SmallVec<[SqliteValue; 16]>] {
13392        &self.results
13393    }
13394
13395    /// Take the result rows, consuming them.
13396    pub fn take_results(&mut self) -> Vec<smallvec::SmallVec<[SqliteValue; 16]>> {
13397        let mut results = Vec::with_capacity(self.results.capacity());
13398        std::mem::swap(&mut results, &mut self.results);
13399        results
13400    }
13401
13402    /// Take exactly one result row without replacing the backing results buffer.
13403    pub fn take_exactly_one_result_row(&mut self) -> ExactResultRowOutcome {
13404        match self.results.len() {
13405            0 => ExactResultRowOutcome::NoRows,
13406            1 => ExactResultRowOutcome::Row(Box::new(
13407                self.results
13408                    .pop()
13409                    .expect("one-row result set should contain one row"),
13410            )),
13411            _ => {
13412                self.results.clear();
13413                ExactResultRowOutcome::MultipleRows
13414            }
13415        }
13416    }
13417
13418    #[cfg(test)]
13419    fn result_buffer_capacity(&self) -> usize {
13420        self.results.capacity()
13421    }
13422
13423    // ── Helpers ─────────────────────────────────────────────────────────
13424
13425    /// Borrow a register cell without changing ownership.
13426    ///
13427    /// Opcode implementations use this for pure read paths where the register
13428    /// must remain intact after the operation.
13429    #[inline(always)]
13430    #[allow(clippy::inline_always)]
13431    fn get_reg(&self, r: i32) -> &SqliteValue {
13432        if r >= 0 && (r as usize) < self.registers.len() {
13433            &self.registers[r as usize]
13434        } else {
13435            &SqliteValue::Null
13436        }
13437    }
13438
13439    fn reg_with_offset(start: i32, offset: usize) -> Option<i32> {
13440        i32::try_from(offset)
13441            .ok()
13442            .and_then(|delta| start.checked_add(delta))
13443    }
13444
13445    fn register_range_contains(start: i32, count: usize, register: i32) -> bool {
13446        if count == 0 {
13447            return false;
13448        }
13449        let Ok(count) = i64::try_from(count) else {
13450            return true;
13451        };
13452        let start = i64::from(start);
13453        let register = i64::from(register);
13454        register >= start && register < start + count
13455    }
13456
13457    #[inline]
13458    fn invalidate_make_record_sideband_if_overwritten(&mut self, r: i32) {
13459        self.make_record_lookaside.clear_sideband_for_register(r);
13460    }
13461
13462    #[inline]
13463    #[allow(clippy::cast_sign_loss)]
13464    fn materialize_make_record_sideband(&mut self, r: i32) {
13465        if !self.make_record_lookaside.sideband_is_armed_for(r) {
13466            return;
13467        }
13468        if !(0..=65535).contains(&r) {
13469            self.make_record_lookaside.reset();
13470            return;
13471        }
13472        let idx = r as usize;
13473        if idx >= self.registers.len() {
13474            self.registers.resize(idx + 1, SqliteValue::Null);
13475        }
13476        self.clear_register_subtype(r);
13477        #[cfg(test)]
13478        FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(|counter| {
13479            counter.set(counter.get().saturating_add(1));
13480        });
13481        let buf = self.make_record_lookaside.take_buf();
13482        self.replace_register_value(idx, SqliteValue::Blob(Arc::<[u8]>::from(buf)));
13483        self.make_record_lookaside.disarm();
13484    }
13485
13486    #[inline]
13487    fn replace_register_value(&mut self, idx: usize, value: SqliteValue) {
13488        let old_value = std::mem::replace(&mut self.registers[idx], value);
13489        pool_return_reusable(old_value);
13490    }
13491
13492    /// Materialize an owned copy of a register while preserving the source.
13493    ///
13494    /// This is the helper to use when a downstream routine needs an owned
13495    /// `SqliteValue` but the register file should keep its current contents.
13496    #[inline]
13497    fn clone_reg_materialized(&mut self, r: i32) -> SqliteValue {
13498        self.materialize_make_record_sideband(r);
13499        self.get_reg(r).clone()
13500    }
13501
13502    /// Explicit hot-opcode fast lane for the main interpreter loop.
13503    ///
13504    /// We keep direct enum dispatch instead of switching to indexed
13505    /// function-pointer dispatch because indirect branches inhibit inlining
13506    /// and usually lose to the compiler's jump-table lowering for a dense
13507    /// opcode enum.
13508    #[inline(always)]
13509    async fn try_execute_hot_opcode(
13510        &mut self,
13511        op: &VdbeOp,
13512        pc: &mut usize,
13513        collect_vdbe_metrics: bool,
13514        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
13515        borrowed_bindings: Option<&[SqliteValue]>,
13516    ) -> Result<bool> {
13517        match op.opcode {
13518            Opcode::Column => {
13519                self.execute_column_hot(op).await?;
13520                *pc += 1;
13521                Ok(true)
13522            }
13523            Opcode::ColumnSubstrPrefix => {
13524                self.execute_column_substr_prefix_hot(op).await?;
13525                *pc += 1;
13526                Ok(true)
13527            }
13528            Opcode::ColumnOctetLength => {
13529                self.execute_column_octet_length_hot(op).await?;
13530                *pc += 1;
13531                Ok(true)
13532            }
13533            Opcode::ResultRow => {
13534                self.execute_result_row_hot(op, collect_vdbe_metrics, row_handler)?;
13535                *pc += 1;
13536                Ok(true)
13537            }
13538            Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
13539                self.execute_comparison_jump_hot(op, pc)?;
13540                Ok(true)
13541            }
13542            Opcode::MakeRecord => {
13543                self.execute_make_record_hot(op, collect_vdbe_metrics);
13544                *pc += 1;
13545                Ok(true)
13546            }
13547            // Hot-path additions: Integer/Goto/Null are among the most frequent
13548            // opcodes in INSERT/UPDATE/DELETE programs. Handling them here avoids
13549            // falling through to the 190-arm match statement (~5-10ns saved per
13550            // occurrence from better branch prediction and smaller code footprint).
13551            Opcode::Integer => {
13552                self.set_reg_int(op.p2, i64::from(op.p1));
13553                *pc += 1;
13554                Ok(true)
13555            }
13556            Opcode::Int64 => {
13557                let val = match &op.p4 {
13558                    P4::Int64(v) => *v,
13559                    _ => 0,
13560                };
13561                self.set_reg_int(op.p2, val);
13562                *pc += 1;
13563                Ok(true)
13564            }
13565            Opcode::Variable => {
13566                let idx = usize::try_from(op.p1)
13567                    .ok()
13568                    .and_then(|one_based| one_based.checked_sub(1));
13569                let bound = idx.and_then(|idx| {
13570                    borrowed_bindings
13571                        .and_then(|bindings| bindings.get(idx))
13572                        .or_else(|| self.bindings.get(idx))
13573                });
13574                // Integer parameters — the dominant prepared-statement bind
13575                // type — update a pre-sized register in place via set_reg_int,
13576                // skipping the clone + replace_register_value that the general
13577                // path pays (mirrors copy_single_reg's Copy/SCopy fast lane).
13578                if let Some(SqliteValue::Integer(v)) = bound {
13579                    let v = *v;
13580                    self.set_reg_int(op.p2, v);
13581                } else {
13582                    let value = bound.cloned().unwrap_or(SqliteValue::Null);
13583                    self.set_reg_fast(op.p2, value);
13584                }
13585                *pc += 1;
13586                Ok(true)
13587            }
13588            Opcode::Goto => {
13589                *pc = op.p2 as usize;
13590                Ok(true)
13591            }
13592            Opcode::Noop => {
13593                *pc += 1;
13594                Ok(true)
13595            }
13596            Opcode::Null => {
13597                // p2 = target register, p3 = end register (if range fill)
13598                let target = op.p2;
13599                if op.p3 > 0 {
13600                    for reg in target..=op.p3 {
13601                        self.set_reg_null(reg);
13602                    }
13603                } else {
13604                    self.set_reg_null(target);
13605                }
13606                *pc += 1;
13607                Ok(true)
13608            }
13609            Opcode::SoftNull => {
13610                self.set_reg_null(op.p1);
13611                *pc += 1;
13612                Ok(true)
13613            }
13614            // AddImm is the canonical counter/accumulator increment, emitted
13615            // in row counters, aggregate folding, recursive-CTE step counters,
13616            // and conformance-test hot loops. Nearly every execution finds an
13617            // Integer already in the target register, so the work is a read +
13618            // `wrapping_add` + integer write — promoting it out of the 190-arm
13619            // main match shortens the per-op instruction window meaningfully.
13620            Opcode::AddImm => {
13621                let val = self
13622                    .get_reg(op.p1)
13623                    .to_integer()
13624                    .wrapping_add(i64::from(op.p2));
13625                self.set_reg_int(op.p1, val);
13626                *pc += 1;
13627                Ok(true)
13628            }
13629            Opcode::Add => {
13630                let a = self.get_reg(op.p2);
13631                let b = self.get_reg(op.p1);
13632                let result = a.sql_add(b);
13633                self.set_reg_arith_result(op.p3, result);
13634                *pc += 1;
13635                Ok(true)
13636            }
13637            Opcode::Subtract => {
13638                let a = self.get_reg(op.p2);
13639                let b = self.get_reg(op.p1);
13640                let result = a.sql_sub(b);
13641                self.set_reg_arith_result(op.p3, result);
13642                *pc += 1;
13643                Ok(true)
13644            }
13645            Opcode::Multiply => {
13646                let a = self.get_reg(op.p2);
13647                let b = self.get_reg(op.p1);
13648                let result = a.sql_mul(b);
13649                self.set_reg_arith_result(op.p3, result);
13650                *pc += 1;
13651                Ok(true)
13652            }
13653            Opcode::Divide => {
13654                let divisor = self.get_reg(op.p1);
13655                let dividend = self.get_reg(op.p2);
13656                let result = sql_div(dividend, divisor);
13657                self.set_reg_arith_result(op.p3, result);
13658                *pc += 1;
13659                Ok(true)
13660            }
13661            Opcode::Remainder => {
13662                let divisor = self.get_reg(op.p1);
13663                let dividend = self.get_reg(op.p2);
13664                let result = sql_rem(dividend, divisor);
13665                self.set_reg_fast(op.p3, result);
13666                *pc += 1;
13667                Ok(true)
13668            }
13669            Opcode::BitAnd => {
13670                let a = self.get_reg(op.p1);
13671                let b = self.get_reg(op.p2);
13672                if a.is_null() || b.is_null() {
13673                    self.set_reg_fast(op.p3, SqliteValue::Null);
13674                } else {
13675                    self.set_reg_int(op.p3, a.to_integer() & b.to_integer());
13676                }
13677                *pc += 1;
13678                Ok(true)
13679            }
13680            Opcode::BitOr => {
13681                let a = self.get_reg(op.p1);
13682                let b = self.get_reg(op.p2);
13683                if a.is_null() || b.is_null() {
13684                    self.set_reg_fast(op.p3, SqliteValue::Null);
13685                } else {
13686                    self.set_reg_int(op.p3, a.to_integer() | b.to_integer());
13687                }
13688                *pc += 1;
13689                Ok(true)
13690            }
13691            Opcode::BitNot => {
13692                let a = self.get_reg(op.p1);
13693                if a.is_null() {
13694                    self.set_reg_fast(op.p2, SqliteValue::Null);
13695                } else {
13696                    self.set_reg_int(op.p2, !a.to_integer());
13697                }
13698                *pc += 1;
13699                Ok(true)
13700            }
13701            Opcode::ShiftLeft => {
13702                let a = self.get_reg(op.p1);
13703                let b = self.get_reg(op.p2);
13704                if a.is_null() || b.is_null() {
13705                    self.set_reg_fast(op.p3, SqliteValue::Null);
13706                } else {
13707                    let result = sql_shift_left(b.to_integer(), a.to_integer());
13708                    self.set_reg_fast(op.p3, result);
13709                }
13710                *pc += 1;
13711                Ok(true)
13712            }
13713            Opcode::ShiftRight => {
13714                let a = self.get_reg(op.p1);
13715                let b = self.get_reg(op.p2);
13716                if a.is_null() || b.is_null() {
13717                    self.set_reg_fast(op.p3, SqliteValue::Null);
13718                } else {
13719                    let result = sql_shift_right(b.to_integer(), a.to_integer());
13720                    self.set_reg_fast(op.p3, result);
13721                }
13722                *pc += 1;
13723                Ok(true)
13724            }
13725            // Copy is the most frequently emitted register-motion opcode in
13726            // codegen (71 sites), driving SELECT projection, expression
13727            // propagation, and materialization staging. p3 is almost always 0
13728            // (single-register copy); the body is `clone + set_reg_fast`.
13729            // Keeping the loop form preserves semantics for the rare range
13730            // copies while leaving the single-reg case a straight line.
13731            Opcode::Copy => {
13732                if op.p3 == 0 {
13733                    self.copy_single_reg(op.p1, op.p2);
13734                } else {
13735                    self.copy_reg_range(op.p1, op.p2, op.p3);
13736                }
13737                *pc += 1;
13738                Ok(true)
13739            }
13740            // SCopy is the shallow single-register copy. A MakeRecord sideband
13741            // is still the source register's logical value, so use the same
13742            // sideband-safe transfer body as single-register Copy.
13743            Opcode::SCopy => {
13744                self.copy_single_reg(op.p1, op.p2);
13745                *pc += 1;
13746                Ok(true)
13747            }
13748            Opcode::IntCopy => {
13749                let val = self.get_reg(op.p1).to_integer();
13750                self.set_reg_int(op.p2, val);
13751                *pc += 1;
13752                Ok(true)
13753            }
13754            // Move is emitted for register handoff where the source range is
13755            // drained. The common p3=1 case skips the range helper's loop and
13756            // overlap checks; multi-register moves still delegate to
13757            // `move_reg_range` so overlap handling stays identical to the
13758            // main-match arm.
13759            Opcode::Move => {
13760                if op.p3 == 1 {
13761                    let value = self.take_reg(op.p1);
13762                    self.set_reg_fast(op.p2, value);
13763                } else {
13764                    let count = usize::try_from(op.p3).unwrap_or(0);
13765                    self.move_reg_range(op.p1, op.p2, count);
13766                }
13767                *pc += 1;
13768                Ok(true)
13769            }
13770            // DecrJumpZero is the canonical LIMIT counter: fires once per
13771            // emitted result row. Body reads a register as integer,
13772            // decrements if positive, writes the new value back, and jumps
13773            // to p2 when the counter reaches zero. Every live DML/OLTP
13774            // query with a LIMIT clause hits this on every row, so even a
13775            // few-ns per-op dispatch saving compounds. Mirrors the
13776            // existing main-match arm verbatim.
13777            Opcode::DecrJumpZero => {
13778                let mut val = self.get_reg(op.p1).to_integer();
13779                if val > 0 {
13780                    val -= 1;
13781                    self.set_reg_int(op.p1, val);
13782                    if val == 0 {
13783                        #[allow(clippy::cast_sign_loss)]
13784                        {
13785                            *pc = op.p2 as usize;
13786                        }
13787                    } else {
13788                        *pc += 1;
13789                    }
13790                } else {
13791                    *pc += 1;
13792                }
13793                Ok(true)
13794            }
13795            // IfPos is the canonical OFFSET counter: fires once per
13796            // skipped row in any LIMIT/OFFSET-bounded query (25 codegen
13797            // sites). Body reads a register as integer; if positive,
13798            // subtracts p3 and jumps to p2; otherwise falls through.
13799            // Mirror-structure to DecrJumpZero — same dispatch-shortening
13800            // logic applies. Body mirrors the existing main-match arm
13801            // verbatim.
13802            Opcode::IfPos => {
13803                let val = self.get_reg(op.p1).to_integer();
13804                if val > 0 {
13805                    let decremented = val - i64::from(op.p3);
13806                    self.set_reg_int(op.p1, decremented);
13807                    #[allow(clippy::cast_sign_loss)]
13808                    {
13809                        *pc = op.p2 as usize;
13810                    }
13811                } else {
13812                    *pc += 1;
13813                }
13814                Ok(true)
13815            }
13816            // IfNotZero backs countdown loops such as recursive query
13817            // step limits: read integer register p1, decrement when
13818            // non-zero, and jump to p2 until it reaches zero. Same
13819            // small-body branch-counter shape as IfPos/DecrJumpZero.
13820            Opcode::IfNotZero => {
13821                let val = self.get_reg(op.p1).to_integer();
13822                if val != 0 {
13823                    self.set_reg_int(op.p1, val.wrapping_sub(1));
13824                    #[allow(clippy::cast_sign_loss)]
13825                    {
13826                        *pc = op.p2 as usize;
13827                    }
13828                } else {
13829                    *pc += 1;
13830                }
13831                Ok(true)
13832            }
13833            // IsNull is the highest-frequency unpromoted opcode in
13834            // codegen (87 emit sites — more than Goto/Column/Copy/Null
13835            // individually). It backs every NOT NULL constraint check,
13836            // every IS NULL / IS NOT NULL WHERE clause, and the
13837            // null-guarding boilerplate around aggregate folding,
13838            // CASE/COALESCE evaluation, and JOIN match probing. Body
13839            // is a single `is_null` read + branch — the smallest
13840            // possible body shape, where dispatch routing cost
13841            // dominates work cost. Mirrors the existing main-match
13842            // arm verbatim.
13843            Opcode::IsNull => {
13844                if self.get_reg(op.p1).is_null() {
13845                    #[allow(clippy::cast_sign_loss)]
13846                    {
13847                        *pc = op.p2 as usize;
13848                    }
13849                } else {
13850                    *pc += 1;
13851                }
13852                Ok(true)
13853            }
13854            // IfNot is the canonical falsy-branch jump: emitted ~48
13855            // production sites driving CASE/COALESCE WHEN-fallthrough,
13856            // AND-short-circuit, LIMIT-zero detection, HAVING-skip,
13857            // and the second arm of every truthiness probe.  Same
13858            // family as the already-promoted IsNull/IfPos/DecrJumpZero
13859            // — small body, high call frequency, dispatch routing
13860            // cost dominates work cost.  Body mirrors the existing
13861            // main-match arm verbatim, including the C-SQLite
13862            // null-as-jump-iff-p3 semantics.
13863            Opcode::IfNot => {
13864                let val = self.get_reg(op.p1);
13865                let should_jump = if val.is_null() {
13866                    op.p3 != 0
13867                } else {
13868                    !vdbe_real_is_truthy(val)
13869                };
13870                if should_jump {
13871                    #[allow(clippy::cast_sign_loss)]
13872                    {
13873                        *pc = op.p2 as usize;
13874                    }
13875                } else {
13876                    *pc += 1;
13877                }
13878                Ok(true)
13879            }
13880            Opcode::IsTrue => {
13881                let val = self.get_reg(op.p1);
13882                let p4_val = match &op.p4 {
13883                    P4::Int(n) => *n,
13884                    _ => 0,
13885                };
13886                if val.is_null() {
13887                    self.set_reg_int(op.p2, i64::from(op.p3 ^ p4_val));
13888                } else {
13889                    let v = i32::from(vdbe_real_is_truthy(val));
13890                    self.set_reg_int(op.p2, i64::from((v ^ p4_val) & 1));
13891                }
13892                *pc += 1;
13893                Ok(true)
13894            }
13895            Opcode::ZeroOrNull => {
13896                if self.get_reg(op.p1).is_null() || self.get_reg(op.p3).is_null() {
13897                    self.set_reg_fast(op.p2, SqliteValue::Null);
13898                } else {
13899                    self.set_reg_int(op.p2, 0);
13900                }
13901                *pc += 1;
13902                Ok(true)
13903            }
13904            // Not is a compact expression-evaluation opcode: read p1, apply
13905            // SQLite truthiness, and write a boolean/null result into p2. Keep
13906            // the body byte-equivalent to the main-match arm while avoiding
13907            // the large dispatch table for repeated boolean projections.
13908            Opcode::Not => {
13909                let val = self.get_reg(op.p1);
13910                if val.is_null() {
13911                    self.set_reg_fast(op.p2, SqliteValue::Null);
13912                } else {
13913                    self.set_reg_int(op.p2, i64::from(!vdbe_real_is_truthy(val)));
13914                }
13915                *pc += 1;
13916                Ok(true)
13917            }
13918            // Rowid extracts the rowid of the currently-positioned row on
13919            // cursor p1 into register p2.  Emitted at ~51 codegen sites
13920            // across fsqlite-vdbe/-planner/-core, driving every JOIN
13921            // match-probe rowid lookup, every UPDATE/DELETE row-key read,
13922            // every INSERT-returning-rowid path, every rowid-as-column
13923            // projection, and the by-rowid aggregate fast paths.  Body
13924            // mirrors the existing main-match arm verbatim — single
13925            // `cursor_rowid` call (one HashMap probe in the common
13926            // storage-cursor path, one cursor.rowid call) plus a register
13927            // write.  Same family as the already-promoted IsNull/IfPos
13928            // /IfNot/SCopy: small body, high call frequency, dispatch
13929            // routing cost dominates work cost.
13930            Opcode::Rowid => {
13931                let cursor_id = op.p1;
13932                let target = op.p2;
13933                let val = self.cursor_rowid(cursor_id).await?;
13934                self.set_reg_fast(target, val);
13935                *pc += 1;
13936                Ok(true)
13937            }
13938            // IdxRowid extracts the trailing rowid field from the index-key
13939            // record at cursor p1's current position into register p2.  Body
13940            // is byte-identical to Rowid above — both arms route through
13941            // `cursor_rowid`, which dispatches uniformly across storage
13942            // cursors (table & index B-trees), legacy in-memory cursors, and
13943            // virtual-table cursors.  Emitted ~13 production sites in
13944            // fsqlite-vdbe + fsqlite-planner codegen, firing once per matched
13945            // index row in every index-driven JOIN, IN-list lookup, covering-
13946            // index scan converting index entries to table rowids for the
13947            // subsequent SeekRowid probe, and any DELETE/UPDATE walking an
13948            // index to find row keys.  Same family as the just-promoted
13949            // Rowid: small body (HashMap probe + cursor.rowid call), high
13950            // call frequency along JOIN inner loops, dispatch routing cost
13951            // is a meaningful fraction of per-op cost.
13952            Opcode::IdxRowid => {
13953                let cursor_id = op.p1;
13954                let target = op.p2;
13955                let val = self.cursor_rowid(cursor_id).await?;
13956                self.set_reg_fast(target, val);
13957                *pc += 1;
13958                Ok(true)
13959            }
13960            // bd-perf (V2.1): Fused NewRowid + MakeRecord + Insert for
13961            // sequential append. Combines 3 opcodes into 1 dispatch.
13962            // P1=cursor, P2=first_reg, P3=num_cols, P5=insert_flags.
13963            Opcode::FusedAppendInsert => {
13964                let cursor_id = op.p1;
13965                let first_reg = op.p2;
13966                let num_cols = usize::try_from(op.p3).unwrap_or(0);
13967                let concurrent_allocator = self.concurrent_rowid_allocator.clone();
13968                let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
13969                let previous_last_insert_rowid = self.last_insert_rowid;
13970                let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
13971
13972                if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
13973                    if sc.writable {
13974                        let root_page = sc.root_page;
13975                        let autoinc_max = sc.autoincrement_high_water;
13976                        let rowid_mode = sc.rowid_mode;
13977                        // 1. Allocate rowid (same logic as NewRowid).
13978                        let rowid = if let Some(allocator) = concurrent_allocator.as_ref() {
13979                            Self::allocate_concurrent_storage_rowid(
13980                                allocator,
13981                                concurrent_schema_epoch,
13982                                root_page,
13983                                rowid_mode,
13984                                autoinc_max,
13985                                sc,
13986                                "rowid overflow in FusedAppendInsert",
13987                            )
13988                            .await?
13989                        } else {
13990                            Self::allocate_serialized_storage_rowid(
13991                                sc,
13992                                autoinc_max,
13993                                "rowid overflow in FusedAppendInsert",
13994                            )
13995                            .await?
13996                        };
13997
13998                        // 2. Serialize record from registers into sideband buf.
13999                        // The peephole preserves MakeRecord's p4 metadata; keep
14000                        // this fused path byte-equivalent to OP_MakeRecord so IPK
14001                        // placeholder columns and fixed record headers do not
14002                        // silently fall back to the generic encoder.
14003                        let mut rec_buf = self.make_record_lookaside.take_buf();
14004                        self.serialize_record_from_register_range(
14005                            first_reg,
14006                            num_cols,
14007                            &op.p4,
14008                            &mut rec_buf,
14009                        );
14010
14011                        // 3. Append to B-tree (prechecked absent — sequential rowid)
14012                        let append_result =
14013                            if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
14014                                let result = sc
14015                                    .cursor
14016                                    .table_append_after_last_position(&sc.cx, rowid, &rec_buf)
14017                                    .await;
14018                                if result.is_ok() {
14019                                    sc.last_successful_insert_rowid = Some(rowid);
14020                                }
14021                                result
14022                            } else {
14023                                Err(FrankenError::internal(
14024                                    "cursor disappeared in FusedAppendInsert",
14025                                ))
14026                            };
14027
14028                        // Return buffer for reuse
14029                        rec_buf.clear();
14030                        self.make_record_lookaside.replace_buf(rec_buf);
14031                        append_result?;
14032
14033                        // 4. Bookkeeping (same as Insert opcode)
14034                        self.changes += 1;
14035                        self.last_insert_rowid = rowid;
14036                        self.last_insert_rowid_valid = true;
14037                        self.last_insert_cursor_id = Some(cursor_id);
14038                        self.set_conflict_skip_idx(false);
14039                        self.set_pending_insert_rollback(Some(PendingInsertRollback {
14040                            cursor_id,
14041                            rowid,
14042                            previous_last_insert_rowid,
14043                            previous_last_insert_rowid_valid,
14044                            update_restore: None,
14045                        }));
14046                        self.pending_next_after_delete.remove(&cursor_id);
14047                        // CRITICAL FIX: Mark table dirty so MemDB fast paths
14048                        // know the data is stale and fall back to pager reads.
14049                        // Without this, subsequent COUNT(*)/LIKE queries via
14050                        // MemDB would return stale results.
14051                        self.mark_storage_root_page_dirty(root_page);
14052                    } else {
14053                        return Err(FrankenError::internal(
14054                            "FusedAppendInsert: cursor is not writable",
14055                        ));
14056                    }
14057                } else if let Some(root_page) = self.cursors.get(&cursor_id).map(|c| c.root_page) {
14058                    // TEMP tables deliberately use the direct MemDatabase
14059                    // cursor backend. Preserve the fused opcode's semantics
14060                    // there instead of requiring a pager-backed cursor.
14061                    let rowid = self.db.as_mut().map_or(1, |db| db.alloc_rowid(root_page));
14062                    let mut rec_buf = self.make_record_lookaside.take_buf();
14063                    self.serialize_record_from_register_range(
14064                        first_reg,
14065                        num_cols,
14066                        &op.p4,
14067                        &mut rec_buf,
14068                    );
14069                    let values = parse_record(&rec_buf).ok_or_else(|| {
14070                        FrankenError::internal("malformed SQLite record in TEMP FusedAppendInsert")
14071                    })?;
14072                    rec_buf.clear();
14073                    self.make_record_lookaside.replace_buf(rec_buf);
14074
14075                    let unique_conflicts = self
14076                        .db
14077                        .as_ref()
14078                        .and_then(|db| db.get_table(root_page))
14079                        .map(|table| table.find_unique_conflicts(&values))
14080                        .unwrap_or_default();
14081                    if !unique_conflicts.is_empty() {
14082                        return Err(FrankenError::UniqueViolation {
14083                            columns: "TEMP table unique constraint".to_owned(),
14084                        });
14085                    }
14086                    let db = self.db.as_mut().ok_or_else(|| {
14087                        FrankenError::internal(
14088                            "FusedAppendInsert: TEMP MemDatabase is not attached",
14089                        )
14090                    })?;
14091                    db.upsert_row(root_page, rowid, values);
14092
14093                    self.changes += 1;
14094                    self.last_insert_rowid = rowid;
14095                    self.last_insert_rowid_valid = true;
14096                    self.last_insert_cursor_id = Some(cursor_id);
14097                    self.set_conflict_skip_idx(false);
14098                    self.set_pending_insert_rollback(Some(PendingInsertRollback {
14099                        cursor_id,
14100                        rowid,
14101                        previous_last_insert_rowid,
14102                        previous_last_insert_rowid_valid,
14103                        update_restore: None,
14104                    }));
14105                    self.pending_next_after_delete.remove(&cursor_id);
14106                } else {
14107                    return Err(FrankenError::internal(format!(
14108                        "FusedAppendInsert: no cursor for id {cursor_id}"
14109                    )));
14110                }
14111                *pc += 1;
14112                Ok(true)
14113            }
14114            // IMPL-13: Fused `Integer(p1=lit, p2=reg) + ResultRow(p1=reg, p2=1)`.
14115            // Emits a single-column result row whose only value is the literal
14116            // integer `p1`, then clears the source register to mirror the
14117            // drain semantics of `ResultRow` (`take_reg_range`).
14118            //
14119            // Correctness: byte-equivalent to the unfused sequence. The
14120            // codegen-side peephole pass is responsible for verifying that
14121            // the `ResultRow` consumes exactly the register written by the
14122            // preceding `Integer` and emits exactly one column.
14123            Opcode::FusedLiteralResultRow => {
14124                let lit = i64::from(op.p1);
14125                let reg = op.p2;
14126                // Preserve Integer's write-then-ResultRow-drain side effects:
14127                // the register ends up cleared after `take_reg_range`.
14128                self.set_reg_int(reg, lit);
14129
14130                let should_retain_row = self.collect_result_rows
14131                    && match self.max_collected_result_rows {
14132                        Some(limit) => self.results.len() < limit,
14133                        None => true,
14134                    };
14135                if row_handler.is_some() || should_retain_row {
14136                    let materialize_start = collect_vdbe_metrics.then(Instant::now);
14137                    // Single-column row: drain the register just like
14138                    // `take_reg_range(reg, 1)` would.
14139                    let mut row: smallvec::SmallVec<[SqliteValue; 16]> =
14140                        smallvec::SmallVec::with_capacity(1);
14141                    row.push(self.take_reg(reg));
14142                    if collect_vdbe_metrics {
14143                        if let Some(materialize_start) = materialize_start {
14144                            FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14145                                u64::try_from(materialize_start.elapsed().as_nanos())
14146                                    .unwrap_or(u64::MAX),
14147                                AtomicOrdering::Relaxed,
14148                            );
14149                        }
14150                        record_result_row_metrics(&row);
14151                    }
14152                    if let Some(handler) = row_handler.as_mut() {
14153                        (*handler)(row)?;
14154                    } else {
14155                        self.results.push(row);
14156                    }
14157                } else {
14158                    // No retention: still drain the register to match the
14159                    // unfused ResultRow's `discard_reg_range` behavior.
14160                    let _ = self.take_reg(reg);
14161                }
14162                *pc += 1;
14163                Ok(true)
14164            }
14165            Opcode::String8 => {
14166                match &op.p4 {
14167                    P4::Str(s) => self.write_text_to_reg(op.p2, s),
14168                    _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
14169                }
14170                *pc += 1;
14171                Ok(true)
14172            }
14173            Opcode::String => {
14174                match &op.p4 {
14175                    P4::Str(s) => self.write_text_to_reg(op.p2, s),
14176                    _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
14177                }
14178                *pc += 1;
14179                Ok(true)
14180            }
14181            Opcode::Real => {
14182                let val = match &op.p4 {
14183                    P4::Real(v) => *v,
14184                    _ => 0.0,
14185                };
14186                self.set_reg_real(op.p2, val);
14187                *pc += 1;
14188                Ok(true)
14189            }
14190            Opcode::If => {
14191                let val = self.get_reg(op.p1);
14192                let should_jump = if val.is_null() {
14193                    op.p3 != 0
14194                } else {
14195                    vdbe_real_is_truthy(val)
14196                };
14197                if should_jump {
14198                    #[allow(clippy::cast_sign_loss)]
14199                    {
14200                        *pc = op.p2 as usize;
14201                    }
14202                } else {
14203                    *pc += 1;
14204                }
14205                Ok(true)
14206            }
14207            Opcode::Next | Opcode::SorterNext => {
14208                self.execute_next_hot(op, pc).await?;
14209                Ok(true)
14210            }
14211            _ => Ok(false),
14212        }
14213    }
14214
14215    #[inline(always)]
14216    async fn execute_next_hot(&mut self, op: &VdbeOp, pc: &mut usize) -> Result<()> {
14217        let cursor_id = op.p1;
14218        let has_next = if !self.pending_next_after_delete.is_empty()
14219            && self.pending_next_after_delete.remove(&cursor_id)
14220        {
14221            if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
14222                !cursor.cursor.eof()
14223            } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
14224                if cursor.is_pseudo {
14225                    false
14226                } else if let Some(pos) = cursor.position {
14227                    if let Some(table) = self
14228                        .db
14229                        .as_ref()
14230                        .and_then(|db| db.get_table(cursor.root_page))
14231                    {
14232                        if pos < table.rows.len() {
14233                            true
14234                        } else {
14235                            cursor.position = None;
14236                            false
14237                        }
14238                    } else {
14239                        false
14240                    }
14241                } else {
14242                    false
14243                }
14244            } else {
14245                false
14246            }
14247        } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
14248            cursor.cursor.next(&cursor.cx).await?
14249        } else if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
14250            if let Some(pos) = sorter.position {
14251                let next = pos + 1;
14252                if next < sorter.rows.len() {
14253                    sorter.position = Some(next);
14254                    true
14255                } else {
14256                    sorter.position = None;
14257                    false
14258                }
14259            } else {
14260                false
14261            }
14262        } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
14263            if cursor.is_pseudo {
14264                false
14265            } else if let Some(db) = self.db.as_ref() {
14266                if let Some(table) = db.get_table(cursor.root_page) {
14267                    if let Some(pos) = cursor.position {
14268                        let next = pos + 1;
14269                        if next < table.rows.len() {
14270                            cursor.position = Some(next);
14271                            true
14272                        } else {
14273                            cursor.position = None;
14274                            false
14275                        }
14276                    } else {
14277                        false
14278                    }
14279                } else {
14280                    false
14281                }
14282            } else {
14283                false
14284            }
14285        } else {
14286            false
14287        };
14288        if has_next {
14289            #[allow(clippy::cast_sign_loss)]
14290            {
14291                *pc = op.p2 as usize;
14292            }
14293        } else {
14294            *pc += 1;
14295        }
14296        Ok(())
14297    }
14298
14299    #[inline(always)]
14300    async fn execute_column_hot(&mut self, op: &VdbeOp) -> Result<()> {
14301        let cursor_id = op.p1;
14302        let col_idx = op.p2 as usize;
14303        let target = op.p3;
14304        if !self
14305            .column_to_reg_direct(cursor_id, col_idx, target)
14306            .await?
14307        {
14308            let val = self.cursor_column(cursor_id, col_idx).await?;
14309            self.set_reg_fast(target, val);
14310        }
14311        Ok(())
14312    }
14313
14314    #[inline(always)]
14315    async fn execute_column_substr_prefix_hot(&mut self, op: &VdbeOp) -> Result<()> {
14316        let Ok(col_idx) = usize::try_from(op.p2) else {
14317            self.set_reg_fast(op.p3, SqliteValue::Null);
14318            return Ok(());
14319        };
14320        let P4::Int(prefix_len) = &op.p4 else {
14321            self.set_reg_fast(op.p3, SqliteValue::Null);
14322            return Ok(());
14323        };
14324        let Ok(prefix_len) = usize::try_from(*prefix_len) else {
14325            self.set_reg_fast(op.p3, SqliteValue::Null);
14326            return Ok(());
14327        };
14328
14329        if let Some(value) = self
14330            .column_substr_prefix_direct(op.p1, col_idx, prefix_len)
14331            .await?
14332        {
14333            self.set_reg_fast(op.p3, value);
14334            return Ok(());
14335        }
14336
14337        let value = self.cursor_column(op.p1, col_idx).await?;
14338        self.set_reg_fast(op.p3, sqlite_substr_prefix_value(&value, prefix_len));
14339        Ok(())
14340    }
14341
14342    #[inline(always)]
14343    async fn execute_column_octet_length_hot(&mut self, op: &VdbeOp) -> Result<()> {
14344        let Ok(col_idx) = usize::try_from(op.p2) else {
14345            self.set_reg_fast(op.p3, SqliteValue::Null);
14346            return Ok(());
14347        };
14348        if let Some(value) = self.column_octet_length_direct(op.p1, col_idx).await? {
14349            self.set_reg_fast(op.p3, value);
14350            return Ok(());
14351        }
14352        let value = self.cursor_column(op.p1, col_idx).await?;
14353        self.set_reg_fast(op.p3, sqlite_octet_length_value(&value));
14354        Ok(())
14355    }
14356
14357    #[inline(always)]
14358    fn execute_result_row_hot(
14359        &mut self,
14360        op: &VdbeOp,
14361        collect_vdbe_metrics: bool,
14362        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14363    ) -> Result<()> {
14364        let count = usize::try_from(op.p2).unwrap_or(0);
14365        let should_retain_row = self.collect_result_rows
14366            && match self.max_collected_result_rows {
14367                Some(limit) => self.results.len() < limit,
14368                None => true,
14369            };
14370        if row_handler.is_some() || should_retain_row {
14371            let materialize_start = collect_vdbe_metrics.then(Instant::now);
14372            let row = self.take_reg_range(op.p1, count);
14373            if collect_vdbe_metrics {
14374                if let Some(materialize_start) = materialize_start {
14375                    FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14376                        u64::try_from(materialize_start.elapsed().as_nanos()).unwrap_or(u64::MAX),
14377                        AtomicOrdering::Relaxed,
14378                    );
14379                }
14380                record_result_row_metrics(&row);
14381            }
14382            if let Some(handler) = row_handler.as_mut() {
14383                (*handler)(row)?;
14384            } else {
14385                self.results.push(row);
14386            }
14387        } else {
14388            self.discard_reg_range(op.p1, count);
14389        }
14390        Ok(())
14391    }
14392
14393    fn emit_compiled_result_row(
14394        &mut self,
14395        values: &[SqliteValue],
14396        collect_vdbe_metrics: bool,
14397        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14398    ) -> Result<()> {
14399        let should_retain_row = self.collect_result_rows
14400            && match self.max_collected_result_rows {
14401                Some(limit) => self.results.len() < limit,
14402                None => true,
14403            };
14404        if row_handler.is_some() || should_retain_row {
14405            let materialize_start = collect_vdbe_metrics.then(Instant::now);
14406            let row: smallvec::SmallVec<[SqliteValue; 16]> = values.iter().cloned().collect();
14407            if collect_vdbe_metrics {
14408                if let Some(materialize_start) = materialize_start {
14409                    FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14410                        u64::try_from(materialize_start.elapsed().as_nanos()).unwrap_or(u64::MAX),
14411                        AtomicOrdering::Relaxed,
14412                    );
14413                }
14414                record_result_row_metrics(&row);
14415            }
14416            if let Some(handler) = row_handler.as_mut() {
14417                (*handler)(row)?;
14418            } else {
14419                self.results.push(row);
14420            }
14421        }
14422        Ok(())
14423    }
14424
14425    fn compiled_column_values(
14426        &self,
14427        value_sources: &[InsertValueSource],
14428        borrowed_bindings: Option<&[SqliteValue]>,
14429        collect_vdbe_metrics: bool,
14430        affinity: Option<&str>,
14431    ) -> smallvec::SmallVec<[SqliteValue; 16]> {
14432        let binding_source = borrowed_bindings.unwrap_or(self.bindings.as_slice());
14433        let mut values = value_sources
14434            .iter()
14435            .map(|source| match source {
14436                InsertValueSource::Binding(binding_index) => binding_source
14437                    .get(*binding_index)
14438                    .cloned()
14439                    .unwrap_or(SqliteValue::Null),
14440                InsertValueSource::Constant(value) => value.clone(),
14441            })
14442            .collect::<smallvec::SmallVec<[_; 16]>>();
14443
14444        if let Some(affinity) = affinity {
14445            for (value, ch) in values.iter_mut().zip(affinity.chars()) {
14446                let before = value.clone();
14447                let coerced = value.clone().apply_affinity(char_to_affinity(ch));
14448                if collect_vdbe_metrics {
14449                    record_type_coercion(&before, &coerced);
14450                }
14451                *value = coerced;
14452            }
14453        }
14454
14455        values
14456    }
14457
14458    fn apply_make_record_null_placeholders(values: &mut [SqliteValue], record_p4: &P4) {
14459        match record_p4 {
14460            P4::PrecomputedHeader(header) if header.column_count() == values.len() => {
14461                for (value, slot) in values.iter_mut().zip(&header.slots) {
14462                    if slot.kind == PrecomputedSerialTypeKind::NullPlaceholder {
14463                        *value = SqliteValue::Null;
14464                    }
14465                }
14466            }
14467            P4::Affinity(affinity) => {
14468                for (value, affinity) in values.iter_mut().zip(affinity.bytes()) {
14469                    if affinity == b'X' {
14470                        *value = SqliteValue::Null;
14471                    }
14472                }
14473            }
14474            _ => {}
14475        }
14476    }
14477
14478    fn execute_compiled_constant_result_row(
14479        &mut self,
14480        template: &ConstantResultRowTemplate,
14481        collect_vdbe_metrics: bool,
14482        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14483    ) -> Result<ExecOutcome> {
14484        self.emit_compiled_result_row(&template.values, collect_vdbe_metrics, row_handler)?;
14485        Ok(ExecOutcome::Done)
14486    }
14487
14488    async fn execute_compiled_simple_insert(
14489        &mut self,
14490        template: &SimpleInsertTemplate,
14491        borrowed_bindings: Option<&[SqliteValue]>,
14492        collect_vdbe_metrics: bool,
14493    ) -> Result<ExecOutcome> {
14494        if (template.insert_flags & 0x0F) != 2 {
14495            return Err(FrankenError::internal(
14496                "compiled simple INSERT only supports OE_Abort semantics",
14497            ));
14498        }
14499
14500        if !self
14501            .open_storage_cursor(template.cursor_id, template.root_page, true)
14502            .await
14503        {
14504            return Err(FrankenError::internal(format!(
14505                "compiled simple INSERT could not open writable cursor {} on root {}",
14506                template.cursor_id, template.root_page
14507            )));
14508        }
14509
14510        let mut values = self.compiled_column_values(
14511            &template.value_sources,
14512            borrowed_bindings,
14513            collect_vdbe_metrics,
14514            template.affinity.as_deref(),
14515        );
14516        Self::apply_make_record_null_placeholders(&mut values, &template.record_p4);
14517        let concurrent_allocator = self.concurrent_rowid_allocator.clone();
14518        let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
14519        let previous_last_insert_rowid = self.last_insert_rowid;
14520        let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
14521
14522        let rowid = {
14523            let sc = self
14524                .storage_cursors
14525                .get_mut(&template.cursor_id)
14526                .ok_or_else(|| FrankenError::internal("compiled simple INSERT lost its cursor"))?;
14527            if !sc.writable {
14528                return Err(FrankenError::internal(
14529                    "compiled simple INSERT cursor is not writable",
14530                ));
14531            }
14532            let autoinc_max = sc.autoincrement_high_water;
14533            let rowid_mode = sc.rowid_mode;
14534            let rowid = if let Some(allocator) = concurrent_allocator.as_ref() {
14535                Self::allocate_concurrent_storage_rowid(
14536                    allocator,
14537                    concurrent_schema_epoch,
14538                    template.root_page,
14539                    rowid_mode,
14540                    autoinc_max,
14541                    sc,
14542                    "rowid overflow in compiled simple INSERT",
14543                )
14544                .await?
14545            } else {
14546                Self::allocate_serialized_storage_rowid(
14547                    sc,
14548                    autoinc_max,
14549                    "rowid overflow in compiled simple INSERT",
14550                )
14551                .await?
14552            };
14553            rowid
14554        };
14555        let record_plan =
14556            build_compiled_record_write_plan(values.as_slice(), &template.record_builder);
14557        let payload_len = record_plan.exact_size();
14558        let appended_directly = if let Some(sc) = self.storage_cursors.get_mut(&template.cursor_id)
14559        {
14560            let result = sc
14561                .cursor
14562                .table_append_after_last_position_with_writer(
14563                    &sc.cx,
14564                    rowid,
14565                    payload_len,
14566                    move |dst| {
14567                        record_plan.write_into_slice(dst).map_err(|()| {
14568                            FrankenError::internal(
14569                                "compiled simple INSERT direct record serialization size mismatch",
14570                            )
14571                        })
14572                    },
14573                )
14574                .await?;
14575            if result {
14576                sc.last_successful_insert_rowid = Some(rowid);
14577            }
14578            result
14579        } else {
14580            return Err(FrankenError::internal(
14581                "compiled simple INSERT lost its cursor",
14582            ));
14583        };
14584        if !appended_directly {
14585            let mut payload_buf = self.make_record_lookaside.take_buf();
14586            serialize_compiled_record_into_vec(
14587                values.as_slice(),
14588                &template.record_builder,
14589                &mut payload_buf,
14590            );
14591            let append_result = if let Some(sc) = self.storage_cursors.get_mut(&template.cursor_id)
14592            {
14593                let result = sc
14594                    .cursor
14595                    .table_append_after_last_position(&sc.cx, rowid, &payload_buf)
14596                    .await;
14597                if result.is_ok() {
14598                    sc.last_successful_insert_rowid = Some(rowid);
14599                }
14600                result
14601            } else {
14602                Err(FrankenError::internal(
14603                    "compiled simple INSERT lost its cursor",
14604                ))
14605            };
14606            payload_buf.clear();
14607            self.make_record_lookaside.replace_buf(payload_buf);
14608            append_result?;
14609        }
14610
14611        self.changes += 1;
14612        self.last_insert_rowid = rowid;
14613        self.last_insert_rowid_valid = true;
14614        self.last_insert_cursor_id = Some(template.cursor_id);
14615        self.set_conflict_skip_idx(false);
14616        self.set_pending_insert_rollback(Some(PendingInsertRollback {
14617            cursor_id: template.cursor_id,
14618            rowid,
14619            previous_last_insert_rowid,
14620            previous_last_insert_rowid_valid,
14621            update_restore: None,
14622        }));
14623        self.clear_pending_idx_entries();
14624        self.pending_next_after_delete.remove(&template.cursor_id);
14625        self.mark_storage_root_page_dirty(template.root_page);
14626
14627        Ok(ExecOutcome::Done)
14628    }
14629
14630    async fn execute_compiled_rowid_lookup_select(
14631        &mut self,
14632        template: &RowidLookupSelectTemplate,
14633        borrowed_bindings: Option<&[SqliteValue]>,
14634        collect_vdbe_metrics: bool,
14635        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14636    ) -> Result<ExecOutcome> {
14637        if !self
14638            .open_storage_cursor(template.cursor_id, template.root_page, false)
14639            .await
14640        {
14641            return Err(FrankenError::internal(format!(
14642                "compiled rowid-lookup SELECT could not open cursor {} on root {}",
14643                template.cursor_id, template.root_page
14644            )));
14645        }
14646
14647        let rowid_val = match &template.rowid_source {
14648            InsertValueSource::Binding(idx) => {
14649                let bindings = borrowed_bindings.ok_or_else(|| {
14650                    FrankenError::internal("compiled rowid-lookup SELECT: missing bindings")
14651                })?;
14652                bindings
14653                    .get(*idx)
14654                    .cloned()
14655                    .unwrap_or(SqliteValue::Null)
14656                    .to_integer()
14657            }
14658            InsertValueSource::Constant(v) => v.to_integer(),
14659        };
14660
14661        let found = {
14662            let sc = self
14663                .storage_cursors
14664                .get_mut(&template.cursor_id)
14665                .ok_or_else(|| {
14666                    FrankenError::internal("compiled rowid-lookup SELECT lost its cursor")
14667                })?;
14668            sc.cursor.table_move_to(&sc.cx, rowid_val).await?.is_found()
14669        };
14670
14671        if found {
14672            let mut values = Vec::with_capacity(template.column_indices.len());
14673            for &col_idx in &template.column_indices {
14674                let val = self
14675                    .cursor_column(template.cursor_id, col_idx as usize)
14676                    .await?;
14677                values.push(val);
14678            }
14679            self.emit_compiled_result_row(&values, collect_vdbe_metrics, row_handler)?;
14680        }
14681
14682        self.storage_cursors.remove(&template.cursor_id);
14683        Ok(ExecOutcome::Done)
14684    }
14685
14686    async fn execute_compiled_full_scan_select(
14687        &mut self,
14688        template: &FullScanSelectTemplate,
14689        collect_vdbe_metrics: bool,
14690        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14691    ) -> Result<ExecOutcome> {
14692        if !self
14693            .open_storage_cursor(template.cursor_id, template.root_page, false)
14694            .await
14695        {
14696            return Err(FrankenError::internal(format!(
14697                "compiled full-scan SELECT could not open cursor {} on root {}",
14698                template.cursor_id, template.root_page
14699            )));
14700        }
14701
14702        let has_rows = {
14703            let sc = self
14704                .storage_cursors
14705                .get_mut(&template.cursor_id)
14706                .ok_or_else(|| {
14707                    FrankenError::internal("compiled full-scan SELECT lost its cursor")
14708                })?;
14709            sc.cursor.first(&sc.cx).await?
14710        };
14711
14712        if has_rows {
14713            loop {
14714                let mut values = Vec::with_capacity(template.column_indices.len());
14715                for &col_idx in &template.column_indices {
14716                    let val = self
14717                        .cursor_column(template.cursor_id, col_idx as usize)
14718                        .await?;
14719                    values.push(val);
14720                }
14721                self.emit_compiled_result_row(&values, collect_vdbe_metrics, row_handler)?;
14722
14723                let more = {
14724                    let sc = self
14725                        .storage_cursors
14726                        .get_mut(&template.cursor_id)
14727                        .ok_or_else(|| {
14728                            FrankenError::internal("compiled full-scan SELECT lost cursor mid-scan")
14729                        })?;
14730                    sc.cursor.next(&sc.cx).await?
14731                };
14732                if !more {
14733                    break;
14734                }
14735            }
14736        }
14737
14738        self.storage_cursors.remove(&template.cursor_id);
14739        Ok(ExecOutcome::Done)
14740    }
14741
14742    async fn execute_compiled_program(
14743        &mut self,
14744        compiled_program: &CompiledProgram,
14745        borrowed_bindings: Option<&[SqliteValue]>,
14746        collect_vdbe_metrics: bool,
14747        row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14748    ) -> Result<ExecOutcome> {
14749        match compiled_program {
14750            CompiledProgram::ConstantResultRow(template) => self
14751                .execute_compiled_constant_result_row(template, collect_vdbe_metrics, row_handler),
14752            CompiledProgram::SimpleInsert(template) => {
14753                self.execute_compiled_simple_insert(
14754                    template,
14755                    borrowed_bindings,
14756                    collect_vdbe_metrics,
14757                )
14758                .await
14759            }
14760            CompiledProgram::RowidLookupSelect(template) => {
14761                self.execute_compiled_rowid_lookup_select(
14762                    template,
14763                    borrowed_bindings,
14764                    collect_vdbe_metrics,
14765                    row_handler,
14766                )
14767                .await
14768            }
14769            CompiledProgram::FullScanSelect(template) => {
14770                self.execute_compiled_full_scan_select(template, collect_vdbe_metrics, row_handler)
14771                    .await
14772            }
14773        }
14774    }
14775
14776    #[inline(always)]
14777    fn execute_comparison_jump_hot(&mut self, op: &VdbeOp, pc: &mut usize) -> Result<()> {
14778        let lhs = self.get_reg(op.p3);
14779        let rhs = self.get_reg(op.p1);
14780        let store_p2 = (op.p5 & 0x20) != 0; // SQLITE_STOREP2
14781
14782        if lhs.is_null() || rhs.is_null() {
14783            let null_eq = (op.p5 & 0x80) != 0;
14784            if null_eq {
14785                let both_null = lhs.is_null() && rhs.is_null();
14786                let should_jump = match op.opcode {
14787                    Opcode::Eq => both_null,
14788                    Opcode::Ne => !both_null,
14789                    _ => false,
14790                };
14791                if store_p2 {
14792                    self.set_reg_int(op.p2, i64::from(should_jump));
14793                    *pc += 1;
14794                } else if should_jump {
14795                    *pc = op.p2 as usize;
14796                } else {
14797                    *pc += 1;
14798                }
14799            } else if store_p2 {
14800                self.set_reg_fast(op.p2, SqliteValue::Null);
14801                *pc += 1;
14802            } else if (op.p5 & 0x10) != 0 {
14803                *pc = op.p2 as usize;
14804            } else {
14805                *pc += 1;
14806            }
14807            return Ok(());
14808        }
14809
14810        let cmp = if let Some(cmp) = fast_compare_same_storage_class(lhs, rhs, &op.p4, op.p5) {
14811            cmp
14812        } else {
14813            let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, op.p5);
14814            if let P4::Collation(ref coll_name) = op.p4 {
14815                let coll = self.lock_collation();
14816                collate_compare(&cmp_lhs, &cmp_rhs, coll_name, &coll)
14817            } else {
14818                cmp_lhs.partial_cmp(&cmp_rhs)
14819            }
14820        };
14821
14822        let should_jump = matches!(
14823            (op.opcode, cmp),
14824            (Opcode::Eq, Some(Ordering::Equal))
14825                | (Opcode::Lt, Some(Ordering::Less))
14826                | (Opcode::Le, Some(Ordering::Less | Ordering::Equal))
14827                | (Opcode::Gt, Some(Ordering::Greater))
14828                | (Opcode::Ge, Some(Ordering::Greater | Ordering::Equal))
14829        ) || matches!((op.opcode, cmp), (Opcode::Ne, Some(ord)) if ord != Ordering::Equal);
14830
14831        if store_p2 {
14832            if cmp.is_none() {
14833                self.set_reg_fast(op.p2, SqliteValue::Null);
14834            } else {
14835                self.set_reg_int(op.p2, i64::from(should_jump));
14836            }
14837            *pc += 1;
14838        } else if should_jump {
14839            *pc = op.p2 as usize;
14840        } else {
14841            *pc += 1;
14842        }
14843        Ok(())
14844    }
14845
14846    #[inline(always)]
14847    fn serialize_record_from_register_range(
14848        &self,
14849        first_reg: i32,
14850        n_cols: usize,
14851        p4: &P4,
14852        rec_buf: &mut Vec<u8>,
14853    ) {
14854        match p4 {
14855            P4::PrecomputedHeader(header) if header.column_count() == n_cols => {
14856                let null_placeholder = SqliteValue::Null;
14857                let make_iter = || {
14858                    header
14859                        .slots
14860                        .iter()
14861                        .enumerate()
14862                        .map(|(i, slot)| match slot.kind {
14863                            PrecomputedSerialTypeKind::NullPlaceholder => &null_placeholder,
14864                            _ => {
14865                                #[allow(clippy::cast_possible_wrap)]
14866                                let reg = first_reg + i as i32;
14867                                self.get_reg(reg)
14868                            }
14869                        })
14870                };
14871                let used_integer_fast_path =
14872                    n_cols >= 4 && simd_serialize_integer_record(make_iter(), rec_buf);
14873                if !used_integer_fast_path
14874                    && !serialize_record_iter_with_precomputed_header_into(
14875                        make_iter(),
14876                        header,
14877                        rec_buf,
14878                    )
14879                {
14880                    fsqlite_types::record::serialize_record_iter_into(make_iter(), rec_buf);
14881                }
14882            }
14883            // The opcode's p1..p1+p2-1 register range is the source of truth
14884            // for OP_MakeRecord. If cached header metadata drifts from that
14885            // shape, fall back to the generic path instead of serializing the
14886            // wrong number of columns.
14887            P4::PrecomputedHeader(_) => {
14888                let iter = (0..n_cols).map(move |i| {
14889                    #[allow(clippy::cast_possible_wrap)]
14890                    let reg = first_reg + i as i32;
14891                    self.get_reg(reg)
14892                });
14893                if n_cols >= 4 && simd_serialize_integer_record(iter.clone(), rec_buf) {
14894                    // Integer-only row used the SIMD/scalar fixed-width fast path.
14895                } else {
14896                    fsqlite_types::record::serialize_record_iter_into(iter, rec_buf);
14897                }
14898            }
14899            P4::Affinity(aff) => {
14900                let null_placeholder = SqliteValue::Null;
14901                let affinity = aff.as_bytes();
14902                let make_iter = || {
14903                    (0..n_cols).map(|i| {
14904                        if affinity.get(i) == Some(&b'X') {
14905                            &null_placeholder
14906                        } else {
14907                            #[allow(clippy::cast_possible_wrap)]
14908                            let reg = first_reg + i as i32;
14909                            self.get_reg(reg)
14910                        }
14911                    })
14912                };
14913                if n_cols >= 4 && simd_serialize_integer_record(make_iter(), rec_buf) {
14914                    // Integer-only row used the SIMD/scalar fixed-width fast path.
14915                } else {
14916                    fsqlite_types::record::serialize_record_iter_into(make_iter(), rec_buf);
14917                }
14918            }
14919            _ => {
14920                let iter = (0..n_cols).map(move |i| {
14921                    #[allow(clippy::cast_possible_wrap)]
14922                    let reg = first_reg + i as i32;
14923                    self.get_reg(reg)
14924                });
14925                if n_cols >= 4 && simd_serialize_integer_record(iter.clone(), rec_buf) {
14926                    // Integer-only row used the SIMD/scalar fixed-width fast path.
14927                } else {
14928                    fsqlite_types::record::serialize_record_iter_into(iter, rec_buf);
14929                }
14930            }
14931        }
14932    }
14933
14934    #[inline(always)]
14935    fn execute_make_record_hot(&mut self, op: &VdbeOp, collect_vdbe_metrics: bool) {
14936        let target = op.p3;
14937        let n_cols = usize::try_from(op.p2).unwrap_or(0);
14938        if let Some(armed_reg) = self.make_record_lookaside.armed_register()
14939            && (armed_reg != target || Self::register_range_contains(op.p1, n_cols, armed_reg))
14940        {
14941            self.materialize_make_record_sideband(armed_reg);
14942        }
14943        let mut rec_buf = self.make_record_lookaside.take_buf();
14944        self.serialize_record_from_register_range(op.p1, n_cols, &op.p4, &mut rec_buf);
14945        if collect_vdbe_metrics {
14946            FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
14947            FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL.fetch_add(
14948                u64::try_from(rec_buf.len()).unwrap_or(u64::MAX),
14949                AtomicOrdering::Relaxed,
14950            );
14951        }
14952        // Set the register to a Null sentinel first so any stale sideband
14953        // state attached to this register is cleared before arming the new
14954        // sideband payload below.
14955        self.set_reg(target, SqliteValue::Null);
14956        // bd-perf: Sideband buffer — keep record bytes in the statement lookaside
14957        // and let Insert/IdxInsert read from there directly, avoiding the
14958        // Arc<[u8]> heap allocation + memcpy per row. The sideband register
14959        // number tells Insert which register to intercept.
14960        self.make_record_lookaside.replace_buf(rec_buf); // keep bytes (don't clear!)
14961        self.make_record_lookaside.arm_sideband(target);
14962    }
14963
14964    #[cold]
14965    fn execute_set_snapshot_cold(&mut self, op: &VdbeOp) -> Result<()> {
14966        let cursor_id = op.p1;
14967        let target = match &op.p4 {
14968            P4::TimeTravelCommitSeq(seq) => TimeTravelMarker::CommitSeq(*seq),
14969            P4::TimeTravelTimestamp(ts) => TimeTravelMarker::Timestamp(ts.clone()),
14970            _ => {
14971                return Err(FrankenError::Internal(
14972                    "SetSnapshot: invalid P4 (expected time-travel target)".to_owned(),
14973                ));
14974            }
14975        };
14976        self.time_travel_cursors.insert(cursor_id, target.clone());
14977
14978        let version_store = self.version_store.as_ref().ok_or_else(|| {
14979            FrankenError::Internal(
14980                "SetSnapshot: VersionStore not available for time-travel".to_owned(),
14981            )
14982        })?;
14983        let commit_log = self.time_travel_commit_log.as_ref().ok_or_else(|| {
14984            FrankenError::Internal(
14985                "SetSnapshot: CommitLog not available for time-travel".to_owned(),
14986            )
14987        })?;
14988        let gc_horizon = self.time_travel_gc_horizon.ok_or_else(|| {
14989            FrankenError::Internal(
14990                "SetSnapshot: GC horizon not available for time-travel".to_owned(),
14991            )
14992        })?;
14993
14994        let has_txn_cursor = self
14995            .storage_cursors
14996            .get(&cursor_id)
14997            .is_some_and(|sc| matches!(&sc.cursor, CursorBackend::Txn(_)));
14998        if !has_txn_cursor {
14999            return Err(FrankenError::Internal(format!(
15000                "SetSnapshot: cursor {cursor_id} is not a transactional cursor"
15001            )));
15002        }
15003
15004        let time_travel_target = match &target {
15005            TimeTravelMarker::CommitSeq(seq) => {
15006                TimeTravelTarget::CommitSequence(CommitSeq::new(*seq))
15007            }
15008            TimeTravelMarker::Timestamp(ts) => {
15009                return Err(FrankenError::Internal(format!(
15010                    "SetSnapshot: timestamp-based time-travel not yet supported (datetime parser not wired); timestamp='{ts}'"
15011                )));
15012            }
15013        };
15014
15015        let schema_epoch = SchemaEpoch::new(u64::from(self.schema_cookie));
15016        let commit_log = commit_log
15017            .lock()
15018            .unwrap_or_else(std::sync::PoisonError::into_inner);
15019        let tt_snapshot =
15020            create_time_travel_snapshot(time_travel_target, &commit_log, gc_horizon, schema_epoch)
15021                .map_err(|err| {
15022                    FrankenError::Internal(format!("time-travel snapshot error: {err}"))
15023                })?;
15024
15025        let old_sc = self.storage_cursors.remove(&cursor_id).ok_or_else(|| {
15026            FrankenError::Internal(format!("SetSnapshot: cursor {} not found", cursor_id))
15027        })?;
15028        let root_page = self.cursor_root_pages.get(&cursor_id).copied().unwrap_or(1);
15029        let root_pgno = PageNumber::new(root_page as u32).unwrap_or(PageNumber::ONE);
15030        let inner_page_io = if let Some(ref page_io) = self.txn_page_io {
15031            page_io.clone()
15032        } else {
15033            return Err(FrankenError::Internal(
15034                "SetSnapshot: no transaction page I/O available".to_owned(),
15035            ));
15036        };
15037
15038        let commit_seq = tt_snapshot.target_commit_seq().get();
15039        let tt_page_io = TimeTravelPageIo {
15040            inner: inner_page_io,
15041            version_store: Arc::clone(version_store),
15042            snapshot: tt_snapshot,
15043        };
15044        let is_table_btree = old_sc.cursor.is_table_btree();
15045        let index_desc_flags = if is_table_btree {
15046            Vec::new()
15047        } else {
15048            self.index_desc_flags_for_root(root_page)
15049        };
15050        let page_layout = BtreeCursorPageLayout {
15051            usable_size: old_sc.cursor.usable_size(),
15052            page_size: old_sc.cursor.page_size(),
15053        };
15054        let mut new_cursor = BtCursor::new_with_index_desc(
15055            tt_page_io,
15056            root_pgno,
15057            page_layout.usable_size,
15058            is_table_btree,
15059            index_desc_flags,
15060        );
15061        if !is_table_btree {
15062            new_cursor.set_index_collation_context(
15063                self.index_collations_for_root(root_page),
15064                Arc::clone(&self.collation_registry),
15065            );
15066        }
15067        configure_btree_cursor_page_size(&mut new_cursor, page_layout);
15068        self.storage_cursors.insert(
15069            cursor_id,
15070            StorageCursor {
15071                cursor: CursorBackend::TimeTravel(new_cursor),
15072                cx: old_sc.cx,
15073                writable: false,
15074                root_page,
15075                rowid_mode: old_sc.rowid_mode,
15076                autoincrement_high_water: old_sc.autoincrement_high_water,
15077                last_alloc_rowid: 0,
15078                payload_buf: Vec::new(),
15079                target_vals_buf: Vec::new(),
15080                cur_vals_buf: Vec::new(),
15081                row_decode: RowDecodeScratch::default(),
15082                last_position_stamp: None,
15083                cached_rowid: None,
15084                payload_includes_rowid_alias: None,
15085                last_successful_insert_rowid: None,
15086                last_rightmost_unique_index_prefix: None,
15087                last_rightmost_unique_index_position: None,
15088                ipk_col_idx: old_sc.ipk_col_idx,
15089                table_column_count: old_sc.table_column_count,
15090                first_not_null_non_ipk_col: old_sc.first_not_null_non_ipk_col,
15091            },
15092        );
15093        tracing::info!(
15094            cursor_id,
15095            commit_seq,
15096            "SetSnapshot: upgraded cursor to time-travel backend"
15097        );
15098        Ok(())
15099    }
15100
15101    /// Collect a borrowed register range into owned values without clearing it.
15102    ///
15103    /// Prefer this pattern for function arguments and compare-style opcodes
15104    /// that need a stable owned snapshot but should not consume the registers.
15105    fn collect_reg_range(
15106        &mut self,
15107        start: i32,
15108        count: usize,
15109    ) -> smallvec::SmallVec<[SqliteValue; 16]> {
15110        let mut row = smallvec::SmallVec::with_capacity(count);
15111        for offset in 0..count {
15112            let val = Self::reg_with_offset(start, offset)
15113                .map_or_else(|| SqliteValue::Null, |reg| self.clone_reg_materialized(reg));
15114            row.push(val);
15115        }
15116        row
15117    }
15118
15119    /// Like `collect_reg_range` but *moves* values out of the register file,
15120    /// leaving Null in the source registers. This avoids deep-cloning Text and
15121    /// Blob values on the hot ResultRow path where the registers are about to
15122    /// be overwritten by the next iteration anyway.
15123    #[inline]
15124    fn take_reg_range(
15125        &mut self,
15126        start: i32,
15127        count: usize,
15128    ) -> smallvec::SmallVec<[SqliteValue; 16]> {
15129        let mut row = smallvec::SmallVec::with_capacity(count);
15130        for offset in 0..count {
15131            let val = Self::reg_with_offset(start, offset)
15132                .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15133            row.push(val);
15134        }
15135        row
15136    }
15137
15138    #[inline]
15139    fn positive_register_ranges_overlap(left_start: i32, right_start: i32, count: usize) -> bool {
15140        let Ok(count) = i64::try_from(count) else {
15141            return true;
15142        };
15143        let left_start = i64::from(left_start);
15144        let right_start = i64::from(right_start);
15145        let left_end = left_start + count;
15146        let right_end = right_start + count;
15147        left_start < right_end && right_start < left_end
15148    }
15149
15150    #[inline]
15151    fn move_reg_range(&mut self, src_start: i32, dst_start: i32, count: usize) {
15152        if count == 0 {
15153            return;
15154        }
15155
15156        let can_stream_directly = count == 1
15157            || (src_start > 0
15158                && dst_start > 0
15159                && !Self::positive_register_ranges_overlap(src_start, dst_start, count));
15160
15161        if can_stream_directly {
15162            for offset in 0..count {
15163                let value = Self::reg_with_offset(src_start, offset)
15164                    .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15165                if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15166                    self.set_reg_fast(dst, value);
15167                }
15168            }
15169            return;
15170        }
15171
15172        let mut temp: smallvec::SmallVec<[SqliteValue; 16]> =
15173            smallvec::SmallVec::with_capacity(count);
15174        for offset in 0..count {
15175            let value = Self::reg_with_offset(src_start, offset)
15176                .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15177            temp.push(value);
15178        }
15179
15180        for (offset, value) in temp.into_iter().enumerate() {
15181            if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15182                self.set_reg_fast(dst, value);
15183            }
15184        }
15185    }
15186
15187    /// Copy one register while avoiding an intermediate enum for the common
15188    /// integer-to-pre-sized-register case.
15189    #[inline(always)]
15190    #[allow(clippy::inline_always)]
15191    fn copy_single_reg(&mut self, src: i32, dst: i32) {
15192        self.materialize_make_record_sideband(src);
15193
15194        let dst_is_pre_sized = usize::try_from(dst)
15195            .is_ok_and(|idx| u16::try_from(idx).is_ok() && idx < self.registers.len());
15196        if dst_is_pre_sized && let SqliteValue::Integer(value) = self.get_reg(src) {
15197            let value = *value;
15198            self.set_reg_int(dst, value);
15199            return;
15200        }
15201
15202        let value = self.get_reg(src).clone();
15203        self.set_reg_fast(dst, value);
15204    }
15205
15206    #[inline]
15207    fn copy_reg_range(&mut self, src_start: i32, dst_start: i32, additional: i32) {
15208        let Ok(additional) = usize::try_from(additional) else {
15209            return;
15210        };
15211        let Some(count) = additional.checked_add(1) else {
15212            return;
15213        };
15214
15215        if count == 1 {
15216            self.copy_single_reg(src_start, dst_start);
15217            return;
15218        }
15219
15220        let values = self.collect_reg_range(src_start, count);
15221        for (offset, value) in values.into_iter().enumerate() {
15222            if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15223                self.set_reg_fast(dst, value);
15224            }
15225        }
15226    }
15227
15228    #[inline]
15229    fn discard_reg_range(&mut self, start: i32, count: usize) {
15230        for offset in 0..count {
15231            if let Some(reg) = Self::reg_with_offset(start, offset) {
15232                let _ = self.take_reg(reg);
15233            }
15234        }
15235    }
15236
15237    fn agg_step_with_args(
15238        cold_state: &mut Option<Box<ColdVdbeState>>,
15239        statement_cold_state: &mut StatementColdState,
15240        step: AggStepCall<'_>,
15241    ) -> Result<()> {
15242        statement_cold_state.insert(StatementColdState::AGGREGATES);
15243        let ctx = cold_state
15244            .get_or_insert_with(|| Box::new(ColdVdbeState::new()))
15245            .aggregates
15246            .entry_or_insert_with(step.accum_reg, || {
15247                let state = step.func.initial_state();
15248                AggregateContext {
15249                    func: step.func.clone(),
15250                    state,
15251                    distinct_seen: if step.is_distinct {
15252                        Some(std::collections::HashSet::new())
15253                    } else {
15254                        None
15255                    },
15256                }
15257            });
15258
15259        if !Arc::ptr_eq(&ctx.func, step.func) {
15260            return Err(FrankenError::Internal(
15261                "AggStep accumulator reused for a different aggregate".to_owned(),
15262            ));
15263        }
15264
15265        // For DISTINCT aggregates, skip if we've already seen these args.
15266        // NULL values are always skipped for DISTINCT (SQL semantics).
15267        let should_step = if let Some(ref mut seen) = ctx.distinct_seen {
15268            if step.args.iter().any(|a| matches!(a, SqliteValue::Null)) {
15269                false
15270            } else {
15271                seen.insert(distinct_key_collated(step.args, step.agg_collation))
15272            }
15273        } else {
15274            true
15275        };
15276
15277        observe_execution_cancellation(step.execution_cx)?;
15278        if should_step {
15279            if let Some(collation) = step.agg_collation
15280                && (step.func_name.eq_ignore_ascii_case("min")
15281                    || step.func_name.eq_ignore_ascii_case("max"))
15282                && !step.args.is_empty()
15283                && !step.args[0].is_null()
15284            {
15285                agg_step_min_max_collated(
15286                    &mut ctx.state,
15287                    &step.args[0],
15288                    step.func_name.eq_ignore_ascii_case("max"),
15289                    collation,
15290                );
15291            } else {
15292                ctx.func.step(&mut ctx.state, step.args)?;
15293            }
15294        }
15295        observe_execution_cancellation(step.execution_cx)
15296    }
15297
15298    #[allow(dead_code)]
15299    fn collect_reg_range_refs(&self, start: i32, count: usize) -> Vec<&SqliteValue> {
15300        let mut row = Vec::with_capacity(count);
15301        for offset in 0..count {
15302            let val = Self::reg_with_offset(start, offset)
15303                .map_or(&SqliteValue::Null, |reg| self.get_reg(reg));
15304            row.push(val);
15305        }
15306        row
15307    }
15308
15309    #[inline(always)]
15310    #[allow(clippy::inline_always)]
15311    /// Consume a single register cell and clear its slot back to NULL.
15312    ///
15313    /// Use this when the opcode is transferring ownership out of the register
15314    /// file and the source register should no longer hold the previous value.
15315    fn take_reg(&mut self, r: i32) -> SqliteValue {
15316        if r >= 0 && (r as usize) < self.registers.len() {
15317            self.materialize_make_record_sideband(r);
15318            self.clear_register_subtype(r);
15319            std::mem::replace(&mut self.registers[r as usize], SqliteValue::Null)
15320        } else {
15321            SqliteValue::Null
15322        }
15323    }
15324
15325    /// General register write path for opcodes that do not need the hottest
15326    /// possible store sequence.
15327    ///
15328    /// All write helpers invalidate `MakeRecord` sideband state and subtype
15329    /// metadata so callers do not have to remember that bookkeeping.
15330    #[inline]
15331    #[allow(clippy::cast_sign_loss)]
15332    fn set_reg(&mut self, r: i32, val: SqliteValue) {
15333        if !(0..=65535).contains(&r) {
15334            // Drop out-of-bounds register writes to prevent OOM.
15335            // SQLite defines a max register limit (SQLITE_MAX_COLUMN + some overhead).
15336            return;
15337        }
15338        let idx = r as usize;
15339        if idx >= self.registers.len() {
15340            self.registers.resize(idx + 1, SqliteValue::Null);
15341        }
15342        self.invalidate_make_record_sideband_if_overwritten(r);
15343        // Register writes replace the logical value, so any prior subtype
15344        // metadata must be discarded as well.  Guard with is_empty() to
15345        // avoid a HashMap probe on every register write — subtypes are
15346        // rare (only JSON/pointer types).
15347        self.clear_register_subtype(r);
15348        let normalized = match val {
15349            SqliteValue::Float(f) if f.is_nan() => SqliteValue::Null,
15350            other => other,
15351        };
15352        self.replace_register_value(idx, normalized);
15353    }
15354
15355    /// Fast-path register write with NaN -> Null normalization.
15356    /// Auto-resizes the register file when necessary (handles both
15357    /// builder-allocated programs and hand-crafted test programs).
15358    ///
15359    /// Use this for hot opcode write paths that still need the same register
15360    /// invalidation semantics as `set_reg`.
15361    #[inline(always)]
15362    #[allow(clippy::inline_always)]
15363    #[allow(clippy::cast_sign_loss)]
15364    fn set_reg_fast(&mut self, r: i32, val: SqliteValue) {
15365        if !(0..=65535).contains(&r) {
15366            return;
15367        }
15368        let idx = r as usize;
15369        if idx >= self.registers.len() {
15370            self.registers.resize(idx + 1, SqliteValue::Null);
15371        }
15372        self.invalidate_make_record_sideband_if_overwritten(r);
15373        self.clear_register_subtype(r);
15374        let normalized = match val {
15375            SqliteValue::Float(f) if f.is_nan() => SqliteValue::Null,
15376            other => other,
15377        };
15378        self.replace_register_value(idx, normalized);
15379    }
15380
15381    /// Write an arithmetic result, updating an already-`Integer` register in
15382    /// place via `set_reg_int` when the result is an `Integer`. sql_add/sub/mul
15383    /// and sql_div return `Integer` only for an exact integer value (overflow
15384    /// promotes to `Float`; divide-by-zero yields `Null`), so this is
15385    /// byte-identical to `set_reg_fast` while skipping the
15386    /// `replace_register_value` buffer swap on the common integer-into-integer
15387    /// case. `Float`/`Null` results take the general path.
15388    #[inline(always)]
15389    #[allow(clippy::inline_always)]
15390    fn set_reg_arith_result(&mut self, r: i32, value: SqliteValue) {
15391        if let SqliteValue::Integer(v) = value {
15392            self.set_reg_int(r, v);
15393        } else {
15394            self.set_reg_fast(r, value);
15395        }
15396    }
15397
15398    /// Null-specialized register write used by null-writing opcodes.
15399    ///
15400    /// Logical-write bookkeeping must run even when the register is already
15401    /// NULL, but replacing NULL with NULL only feeds a non-reusable value
15402    /// through the buffer-return path.
15403    #[inline(always)]
15404    #[allow(clippy::inline_always, clippy::cast_sign_loss)]
15405    fn set_reg_null(&mut self, r: i32) {
15406        if !(0..=65535).contains(&r) {
15407            return;
15408        }
15409        let idx = r as usize;
15410        if idx >= self.registers.len() {
15411            self.registers.resize(idx + 1, SqliteValue::Null);
15412        }
15413        self.invalidate_make_record_sideband_if_overwritten(r);
15414        self.clear_register_subtype(r);
15415        if !self.registers[idx].is_null() {
15416            self.replace_register_value(idx, SqliteValue::Null);
15417        }
15418    }
15419
15420    /// Fastest possible register write for Integer values.
15421    /// Skips: bounds check (registers pre-sized in execute()),
15422    /// NaN normalization (integers can't be NaN).
15423    /// Only safe when the register file has been pre-sized.
15424    #[allow(clippy::inline_always)]
15425    #[inline(always)]
15426    #[allow(clippy::cast_sign_loss)]
15427    fn set_reg_int(&mut self, r: i32, val: i64) {
15428        let idx = r as usize;
15429        // The register file is pre-sized in execute() to program.register_count(),
15430        // so this should never need to resize. Guard with debug_assert only.
15431        debug_assert!(
15432            idx < self.registers.len(),
15433            "register {r} out of pre-sized bounds"
15434        );
15435        if idx < self.registers.len() {
15436            self.invalidate_make_record_sideband_if_overwritten(r);
15437            self.clear_register_subtype(r);
15438            if let SqliteValue::Integer(current) = &mut self.registers[idx] {
15439                *current = val;
15440            } else {
15441                self.replace_register_value(idx, SqliteValue::Integer(val));
15442            }
15443        }
15444    }
15445
15446    /// Float-specialized register write with NaN -> Null normalization.
15447    ///
15448    /// Repeated real constants can update the existing payload directly, while
15449    /// cross-variant writes retain the buffer-return behavior of replacement.
15450    #[inline(always)]
15451    #[allow(clippy::inline_always, clippy::cast_sign_loss)]
15452    fn set_reg_real(&mut self, r: i32, val: f64) {
15453        if !(0..=65535).contains(&r) {
15454            return;
15455        }
15456        let idx = r as usize;
15457        if idx >= self.registers.len() {
15458            self.registers.resize(idx + 1, SqliteValue::Null);
15459        }
15460        self.invalidate_make_record_sideband_if_overwritten(r);
15461        self.clear_register_subtype(r);
15462        if val.is_nan() {
15463            self.replace_register_value(idx, SqliteValue::Null);
15464        } else if let SqliteValue::Float(current) = &mut self.registers[idx] {
15465            *current = val;
15466        } else {
15467            self.replace_register_value(idx, SqliteValue::Float(val));
15468        }
15469    }
15470
15471    /// Write a text string to a register, reusing the existing `String`
15472    /// buffer's capacity when the register already holds a `Text` value.
15473    /// Avoids the allocate-then-free cycle for repeated text writes to the
15474    /// same register (common in scan loops with string constant columns).
15475    #[inline]
15476    #[allow(clippy::cast_sign_loss)]
15477    fn write_text_to_reg(&mut self, r: i32, text: &str) {
15478        if !(0..=65535).contains(&r) {
15479            return;
15480        }
15481        let idx = r as usize;
15482        if idx >= self.registers.len() {
15483            self.registers.resize(idx + 1, SqliteValue::Null);
15484        }
15485        self.invalidate_make_record_sideband_if_overwritten(r);
15486        self.clear_register_subtype(r);
15487        if let SqliteValue::Text(existing) = &mut self.registers[idx] {
15488            existing.overwrite(text);
15489        } else {
15490            self.replace_register_value(idx, SqliteValue::Text(SmallText::new(text)));
15491        }
15492    }
15493
15494    /// Write a blob to a register.
15495    ///
15496    /// Reuses the existing `Arc<[u8]>` allocation when the register owns a
15497    /// same-length blob exclusively; otherwise allocates a fresh `Arc`.
15498    #[inline]
15499    #[allow(clippy::cast_sign_loss)]
15500    fn write_blob_to_reg(&mut self, r: i32, blob: &[u8]) {
15501        if !(0..=65535).contains(&r) {
15502            return;
15503        }
15504        let idx = r as usize;
15505        if idx >= self.registers.len() {
15506            self.registers.resize(idx + 1, SqliteValue::Null);
15507        }
15508        self.invalidate_make_record_sideband_if_overwritten(r);
15509        self.clear_register_subtype(r);
15510        if let SqliteValue::Blob(existing) = &mut self.registers[idx]
15511            && existing.len() == blob.len()
15512            && let Some(existing_bytes) = Arc::get_mut(existing)
15513        {
15514            existing_bytes.copy_from_slice(blob);
15515            return;
15516        }
15517        self.replace_register_value(idx, SqliteValue::Blob(Arc::from(blob)));
15518    }
15519
15520    /// Return `SUBSTR(column, 1, prefix_len)` directly from raw storage bytes
15521    /// when the column's storage class makes that equivalent to scalar `substr`.
15522    #[allow(clippy::too_many_lines, clippy::cast_sign_loss)]
15523    async fn column_substr_prefix_direct(
15524        &mut self,
15525        cursor_id: i32,
15526        col_idx: usize,
15527        prefix_len: usize,
15528    ) -> Result<Option<SqliteValue>> {
15529        let collect_vdbe_metrics = self.collect_vdbe_metrics;
15530        let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15531            return Ok(None);
15532        };
15533        if cursor.cursor.eof() {
15534            return Ok(Some(SqliteValue::Null));
15535        }
15536
15537        ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15538
15539        let ipk_col_idx = cursor.ipk_col_idx;
15540        let payload_includes = if let Some(ipk) = ipk_col_idx {
15541            if let Some(cached) = cursor.payload_includes_rowid_alias {
15542                cached
15543            } else {
15544                let includes = payload_includes_rowid_alias_without_rowid(
15545                    &cursor.row_decode,
15546                    ipk,
15547                    cursor.table_column_count,
15548                    cursor.first_not_null_non_ipk_col,
15549                );
15550                cursor.payload_includes_rowid_alias = Some(includes);
15551                includes
15552            }
15553        } else {
15554            false
15555        };
15556
15557        let payload_idx = if let Some(ipk) = ipk_col_idx {
15558            if col_idx == ipk {
15559                return Ok(None);
15560            }
15561            if col_idx > ipk && !payload_includes {
15562                col_idx - 1
15563            } else {
15564                col_idx
15565            }
15566        } else {
15567            col_idx
15568        };
15569
15570        let Some(col) = cursor.row_decode.column_offset(payload_idx).copied() else {
15571            return Ok(None);
15572        };
15573        let Some(col_end) = column_payload_end(&col) else {
15574            return Err(FrankenError::DatabaseCorrupt {
15575                detail: format!("malformed column {payload_idx} payload length"),
15576            });
15577        };
15578        ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics).await?;
15579        let start =
15580            usize::try_from(col.body_offset).map_err(|_| FrankenError::DatabaseCorrupt {
15581                detail: format!("malformed column {payload_idx} payload offset"),
15582            })?;
15583        let bytes = cursor.payload_buf.get(start..col_end).ok_or_else(|| {
15584            FrankenError::DatabaseCorrupt {
15585                detail: format!("column {payload_idx} payload exceeds row image"),
15586            }
15587        })?;
15588
15589        let value = match classify_serial_type(col.serial_type) {
15590            SerialTypeClass::Null => Some(SqliteValue::Null),
15591            SerialTypeClass::Text => {
15592                if !bytes.is_ascii() {
15593                    return Ok(None);
15594                }
15595                let end = prefix_len.min(bytes.len());
15596                let text = std::str::from_utf8(&bytes[..end])
15597                    .expect("ASCII storage text prefix must be valid UTF-8");
15598                Some(SqliteValue::Text(SmallText::new(text)))
15599            }
15600            SerialTypeClass::Blob => {
15601                let end = prefix_len.min(bytes.len());
15602                Some(SqliteValue::Blob(Arc::from(&bytes[..end])))
15603            }
15604            SerialTypeClass::Integer
15605            | SerialTypeClass::Float
15606            | SerialTypeClass::Zero
15607            | SerialTypeClass::One
15608            | SerialTypeClass::Reserved => None,
15609        };
15610
15611        if collect_vdbe_metrics && let Some(value) = value.as_ref() {
15612            FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15613            record_decoded_value_metrics(value);
15614        }
15615        Ok(value)
15616    }
15617
15618    /// Return `octet_length(column)` from record-header metadata when the
15619    /// storage class encodes a byte length directly.
15620    ///
15621    /// This path deliberately requests only enough payload to parse the record
15622    /// header. In particular, it must not expand an overflow TEXT/BLOB merely
15623    /// to decide whether a higher layer is willing to materialize that value.
15624    async fn column_octet_length_direct(
15625        &mut self,
15626        cursor_id: i32,
15627        col_idx: usize,
15628    ) -> Result<Option<SqliteValue>> {
15629        let collect_vdbe_metrics = self.collect_vdbe_metrics;
15630        let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15631            return Ok(None);
15632        };
15633        if cursor.cursor.eof() {
15634            return Ok(Some(SqliteValue::Null));
15635        }
15636
15637        ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15638
15639        let ipk_col_idx = cursor.ipk_col_idx;
15640        let payload_includes = if let Some(ipk) = ipk_col_idx {
15641            if let Some(cached) = cursor.payload_includes_rowid_alias {
15642                cached
15643            } else {
15644                let includes = payload_includes_rowid_alias_without_rowid(
15645                    &cursor.row_decode,
15646                    ipk,
15647                    cursor.table_column_count,
15648                    cursor.first_not_null_non_ipk_col,
15649                );
15650                cursor.payload_includes_rowid_alias = Some(includes);
15651                includes
15652            }
15653        } else {
15654            false
15655        };
15656
15657        let payload_idx = if let Some(ipk) = ipk_col_idx {
15658            if col_idx == ipk {
15659                return Ok(None);
15660            }
15661            if col_idx > ipk && !payload_includes {
15662                col_idx - 1
15663            } else {
15664                col_idx
15665            }
15666        } else {
15667            col_idx
15668        };
15669
15670        let Some(col) = cursor.row_decode.column_offset(payload_idx) else {
15671            return Ok(None);
15672        };
15673        let value = match classify_serial_type(col.serial_type) {
15674            SerialTypeClass::Null | SerialTypeClass::Reserved => Some(SqliteValue::Null),
15675            SerialTypeClass::Text | SerialTypeClass::Blob => {
15676                Some(SqliteValue::Integer(i64::from(col.value_len)))
15677            }
15678            SerialTypeClass::Integer
15679            | SerialTypeClass::Float
15680            | SerialTypeClass::Zero
15681            | SerialTypeClass::One => None,
15682        };
15683
15684        if collect_vdbe_metrics && let Some(value) = value.as_ref() {
15685            FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15686            record_decoded_value_metrics(value);
15687        }
15688        Ok(value)
15689    }
15690
15691    /// Zero-clone column-to-register write for storage cursors.
15692    ///
15693    /// Inlines the storage-cursor branch of [`cursor_column`] but writes
15694    /// the column value *directly* into the target register, reusing
15695    /// existing `Text`/`Blob` buffer capacity via disjoint struct field
15696    /// borrows (`self.storage_cursors` vs `self.registers`).
15697    ///
15698    /// Returns `Ok(true)` when the column read was handled,
15699    /// `Ok(false)` when the cursor is not a storage cursor (caller must
15700    /// fall back to `cursor_column`).
15701    #[allow(clippy::too_many_lines, clippy::cast_sign_loss)]
15702    async fn column_to_reg_direct(
15703        &mut self,
15704        cursor_id: i32,
15705        col_idx: usize,
15706        target: i32,
15707    ) -> Result<bool> {
15708        let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15709            return Ok(false);
15710        };
15711        if cursor.cursor.eof() {
15712            self.set_reg(target, SqliteValue::Null);
15713            return Ok(true);
15714        }
15715
15716        let mut refresh_state =
15717            ensure_storage_cursor_row_layout(cursor, 0, self.collect_vdbe_metrics).await?;
15718
15719        // ── Resolve IPK alias and payload column index ────────────
15720        let ipk_col_idx = cursor.ipk_col_idx;
15721        let payload_includes = if let Some(ipk) = ipk_col_idx {
15722            if let Some(cached) = cursor.payload_includes_rowid_alias {
15723                cached
15724            } else {
15725                let includes = payload_includes_rowid_alias_without_rowid(
15726                    &cursor.row_decode,
15727                    ipk,
15728                    cursor.table_column_count,
15729                    cursor.first_not_null_non_ipk_col,
15730                );
15731                cursor.payload_includes_rowid_alias = Some(includes);
15732                includes
15733            }
15734        } else {
15735            false
15736        };
15737
15738        let payload_idx = if let Some(ipk) = ipk_col_idx {
15739            if col_idx == ipk {
15740                let rowid = storage_cursor_cached_rowid(cursor).await?;
15741                self.set_reg_fast(target, SqliteValue::Integer(rowid));
15742                return Ok(true);
15743            }
15744            if col_idx > ipk && !payload_includes {
15745                col_idx - 1
15746            } else {
15747                col_idx
15748            }
15749        } else {
15750            col_idx
15751        };
15752
15753        // ── Lazy decode + zero-clone register write ────────────────
15754        let collect_vdbe_metrics = self.collect_vdbe_metrics;
15755
15756        if payload_idx < cursor.row_decode.column_count() {
15757            if let Some(col) = cursor.row_decode.column_offset(payload_idx).copied()
15758                && let Some(col_end) = column_payload_end(&col)
15759            {
15760                let col_refresh =
15761                    ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics).await?;
15762                refresh_state.refreshed |= col_refresh.refreshed;
15763                refresh_state.eager_values_ready |= col_refresh.eager_values_ready;
15764            }
15765            // Already decoded? Write from cache to register with buffer reuse.
15766            let cached_value_ready = cursor.row_decode.cached_value_ready(payload_idx);
15767            if cached_value_ready {
15768                if let Some(cached) = cursor.row_decode.cached_value(payload_idx) {
15769                    if refresh_state.refreshed && refresh_state.eager_values_ready {
15770                        note_decode_cache_miss(collect_vdbe_metrics);
15771                    } else {
15772                        note_decode_cache_hit(collect_vdbe_metrics);
15773                    }
15774                    if collect_vdbe_metrics {
15775                        FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15776                        record_decoded_value_metrics(cached);
15777                    }
15778                    // KEY OPTIMIZATION: write from &cached (borrows
15779                    // self.storage_cursors) directly into self.registers
15780                    // (disjoint struct field) — zero clone for matching types.
15781                    let reg_idx = target as usize;
15782                    if (0..=65535).contains(&target) {
15783                        if reg_idx >= self.registers.len() {
15784                            self.registers.resize(reg_idx + 1, SqliteValue::Null);
15785                        }
15786                        self.make_record_lookaside
15787                            .clear_sideband_for_register(target);
15788                        if let Some(cold_state) = self.cold_state.as_mut()
15789                            && cold_state.has_subtypes
15790                        {
15791                            cold_state.register_subtypes.remove(&target);
15792                            if cold_state.register_subtypes.is_empty() {
15793                                cold_state.has_subtypes = false;
15794                            }
15795                        }
15796                        match cached {
15797                            // NaN → Null normalization (matches set_reg behavior).
15798                            SqliteValue::Float(f) if f.is_nan() => {
15799                                self.registers[reg_idx] = SqliteValue::Null;
15800                            }
15801                            val => {
15802                                self.registers[reg_idx] = val.clone();
15803                            }
15804                        }
15805                    }
15806                    return Ok(true);
15807                }
15808            }
15809
15810            // Not yet decoded: decode from raw payload.
15811            // bd-db300.4.4.2 K1: pass the previous row's cached value as a hint.
15812            // If raw bytes match, reuse the existing Arc (skip malloc+memcpy).
15813            note_decode_cache_miss(collect_vdbe_metrics);
15814            let hint = cursor.row_decode.cached_value(payload_idx);
15815            let val = fsqlite_types::record::decode_column_from_offset_reuse(
15816                &cursor.payload_buf,
15817                cursor
15818                    .row_decode
15819                    .column_offset(payload_idx)
15820                    .expect("payload_idx checked against column_count"),
15821                hint,
15822                collect_vdbe_metrics,
15823            )
15824            .ok_or_else(|| FrankenError::DatabaseCorrupt {
15825                detail: format!("failed to decode column {payload_idx} from cursor payload"),
15826            })?;
15827
15828            if collect_vdbe_metrics {
15829                FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15830                record_decoded_value_metrics(&val);
15831            }
15832
15833            // Cache the decoded value with buffer reuse.
15834            cursor.row_decode.cache_decoded(payload_idx, val.clone());
15835
15836            // Write freshly decoded value to register (owned, no clone needed).
15837            self.set_reg_fast(target, val);
15838            return Ok(true);
15839        }
15840
15841        // ── Column beyond record width: ALTER TABLE ADD COLUMN defaults ─
15842        if let Some(&rp) = self.cursor_root_pages.get(&cursor_id) {
15843            if let Some(defaults) = self.column_defaults_by_root_page.get(&rp) {
15844                if let Some(Some(default_val)) = defaults.get(col_idx) {
15845                    if collect_vdbe_metrics {
15846                        FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15847                        record_decoded_value_metrics(default_val);
15848                    }
15849                    self.set_reg(target, default_val.clone());
15850                    return Ok(true);
15851                }
15852            }
15853        }
15854        if collect_vdbe_metrics {
15855            FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15856            record_decoded_value_metrics(&SqliteValue::Null);
15857        }
15858        self.set_reg(target, SqliteValue::Null);
15859        Ok(true)
15860    }
15861
15862    /// Read a column value from the cursor's current row.
15863    ///
15864    /// Uses **lazy column decode**: when the cursor moves to a new position,
15865    /// only the record header is parsed (serial types + byte offsets).
15866    /// Individual column values are decoded on first access and cached in
15867    /// `row_decode.values` for subsequent reads at the same position.
15868    ///
15869    /// For records with >64 columns the full eager-decode path is used
15870    /// because `row_decode.decoded_mask` uses a single `u64`.
15871    async fn cursor_column(&mut self, cursor_id: i32, col_idx: usize) -> Result<SqliteValue> {
15872        let collect_vdbe_metrics = self.collect_vdbe_metrics;
15873        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
15874            if cursor.cursor.eof() {
15875                return Ok(SqliteValue::Null);
15876            }
15877            let mut refresh_state =
15878                ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15879
15880            let ipk_col_idx = cursor.ipk_col_idx;
15881            let payload_includes_rowid_alias = if let Some(ipk_col_idx) = ipk_col_idx {
15882                if let Some(cached) = cursor.payload_includes_rowid_alias {
15883                    cached
15884                } else {
15885                    let includes = payload_includes_rowid_alias_without_rowid(
15886                        &cursor.row_decode,
15887                        ipk_col_idx,
15888                        cursor.table_column_count,
15889                        cursor.first_not_null_non_ipk_col,
15890                    );
15891                    cursor.payload_includes_rowid_alias = Some(includes);
15892                    includes
15893                }
15894            } else {
15895                false
15896            };
15897            let payload_idx = if let Some(ipk) = ipk_col_idx {
15898                if col_idx == ipk {
15899                    let rowid = storage_cursor_cached_rowid(cursor).await?;
15900                    return Ok(SqliteValue::Integer(rowid));
15901                }
15902                if col_idx > ipk && !payload_includes_rowid_alias {
15903                    col_idx - 1
15904                } else {
15905                    col_idx
15906                }
15907            } else {
15908                col_idx
15909            };
15910
15911            // ── Lazy column decode: decode on demand ─────────────────
15912            if payload_idx < cursor.row_decode.column_count() {
15913                if let Some(col) = cursor.row_decode.column_offset(payload_idx).copied()
15914                    && let Some(col_end) = column_payload_end(&col)
15915                {
15916                    let col_refresh =
15917                        ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics)
15918                            .await?;
15919                    refresh_state.refreshed |= col_refresh.refreshed;
15920                    refresh_state.eager_values_ready |= col_refresh.eager_values_ready;
15921                }
15922                // Check if already decoded via bitmask.
15923                let cached_value_ready = cursor.row_decode.cached_value_ready(payload_idx);
15924                if cached_value_ready {
15925                    // Already materialized — return cached value.
15926                    if let Some(val) = cursor.row_decode.cached_value(payload_idx).cloned() {
15927                        if refresh_state.refreshed && refresh_state.eager_values_ready {
15928                            note_decode_cache_miss(collect_vdbe_metrics);
15929                        } else {
15930                            note_decode_cache_hit(collect_vdbe_metrics);
15931                        }
15932                        if collect_vdbe_metrics {
15933                            FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15934                            record_decoded_value_metrics(&val);
15935                        }
15936                        return Ok(val);
15937                    }
15938                }
15939                // Decode just this column from the offset table + raw payload.
15940                // bd-db300.4.4.2 K1: reuse hint from previous row's cached value.
15941                note_decode_cache_miss(collect_vdbe_metrics);
15942                let hint = cursor.row_decode.cached_value(payload_idx);
15943                let val = fsqlite_types::record::decode_column_from_offset_reuse(
15944                    &cursor.payload_buf,
15945                    cursor
15946                        .row_decode
15947                        .column_offset(payload_idx)
15948                        .expect("payload_idx checked against column_count"),
15949                    hint,
15950                    collect_vdbe_metrics,
15951                )
15952                .ok_or_else(|| FrankenError::DatabaseCorrupt {
15953                    detail: format!("failed to decode column {payload_idx} from cursor payload"),
15954                })?;
15955                if collect_vdbe_metrics {
15956                    FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15957                    record_decoded_value_metrics(&val);
15958                }
15959                // Cache the decoded value for repeated column reads while the
15960                // cursor stays on the same physical row image.
15961                cursor.row_decode.cache_decoded(payload_idx, val.clone());
15962                return Ok(val);
15963            }
15964            // Column beyond record width — check ALTER TABLE ADD COLUMN defaults.
15965            if let Some(&root_page) = self.cursor_root_pages.get(&cursor_id) {
15966                if let Some(defaults) = self.column_defaults_by_root_page.get(&root_page) {
15967                    if let Some(Some(default_val)) = defaults.get(col_idx) {
15968                        if collect_vdbe_metrics {
15969                            FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15970                            record_decoded_value_metrics(default_val);
15971                        }
15972                        return Ok(default_val.clone());
15973                    }
15974                }
15975            }
15976            if collect_vdbe_metrics {
15977                FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15978                record_decoded_value_metrics(&SqliteValue::Null);
15979            }
15980            return Ok(SqliteValue::Null);
15981        }
15982
15983        // Extract pseudo cursor info without holding a mutable borrow
15984        // on self.cursors, to avoid conflict with self.get_reg().
15985        if let Some(cursor) = self.cursors.get(&cursor_id) {
15986            if cursor.is_pseudo {
15987                if let Some(row) = &cursor.pseudo_row {
15988                    let value = row.get(col_idx).cloned().unwrap_or(SqliteValue::Null);
15989                    if collect_vdbe_metrics {
15990                        record_decoded_value_metrics(&value);
15991                    }
15992                    return Ok(value);
15993                }
15994                if let Some(reg) = cursor.pseudo_reg {
15995                    let blob = self.get_reg(reg).clone();
15996
15997                    let use_cache = if let Some(cursor) = self.cursors.get(&cursor_id) {
15998                        if let Some((cached_blob, _)) = &cursor.cached_pseudo_row {
15999                            cached_blob == &blob
16000                        } else {
16001                            false
16002                        }
16003                    } else {
16004                        false
16005                    };
16006
16007                    if !use_cache {
16008                        if let Some(cursor) = self.cursors.get(&cursor_id)
16009                            && let Some((cached_blob, _)) = &cursor.cached_pseudo_row
16010                            && cached_blob != &blob
16011                        {
16012                            note_decode_cache_invalidation(
16013                                collect_vdbe_metrics,
16014                                DecodeCacheInvalidationReason::PseudoRowChange,
16015                            );
16016                        }
16017                        note_decode_cache_miss(collect_vdbe_metrics);
16018                        if let Ok(values) = decode_record_with_metrics(&blob, collect_vdbe_metrics)
16019                        {
16020                            if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
16021                                cursor.cached_pseudo_row = Some((blob, values));
16022                            }
16023                        } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
16024                            cursor.cached_pseudo_row = None;
16025                        }
16026                    }
16027
16028                    if let Some(cursor) = self.cursors.get(&cursor_id) {
16029                        if let Some((_, values)) = &cursor.cached_pseudo_row {
16030                            if use_cache {
16031                                note_decode_cache_hit(collect_vdbe_metrics);
16032                            }
16033                            let value = values.get(col_idx).cloned().unwrap_or(SqliteValue::Null);
16034                            if collect_vdbe_metrics {
16035                                record_decoded_value_metrics(&value);
16036                            }
16037                            return Ok(value);
16038                        }
16039                    }
16040                }
16041                return Ok(SqliteValue::Null);
16042            }
16043            if let Some(pos) = cursor.position
16044                && let Some(db) = self.db.as_ref()
16045                && let Some(table) = db.get_table(cursor.root_page)
16046                && let Some(row) = table.rows.get(pos)
16047            {
16048                let value = row
16049                    .values
16050                    .get(col_idx)
16051                    .cloned()
16052                    .unwrap_or(SqliteValue::Null);
16053                if collect_vdbe_metrics {
16054                    record_decoded_value_metrics(&value);
16055                }
16056                return Ok(value);
16057            }
16058        }
16059
16060        // Sorter cursor: read column directly from the sorted row.
16061        if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
16062            if let Some(pos) = sorter.position {
16063                if let Some(value) = sorter
16064                    .rows
16065                    .get(pos)
16066                    .and_then(|row| row.values.get(col_idx))
16067                    .cloned()
16068                {
16069                    note_decode_cache_hit(collect_vdbe_metrics);
16070                    if collect_vdbe_metrics {
16071                        record_decoded_value_metrics(&value);
16072                    }
16073                    return Ok(value);
16074                }
16075                let refresh_state = ensure_sorter_row_cache(sorter, collect_vdbe_metrics, pos)?;
16076                let rows = &sorter.rows;
16077                let cached_row_decode = &mut sorter.cached_row_decode;
16078                let row = rows.get(pos).ok_or_else(|| FrankenError::DatabaseCorrupt {
16079                    detail: format!("missing sorter row at position {pos}"),
16080                })?;
16081                let value = if col_idx < cached_row_decode.column_count() {
16082                    let cached_value_ready = cached_row_decode.cached_value_ready(col_idx);
16083                    if cached_value_ready {
16084                        if refresh_state.refreshed && refresh_state.eager_values_ready {
16085                            note_decode_cache_miss(collect_vdbe_metrics);
16086                        } else {
16087                            note_decode_cache_hit(collect_vdbe_metrics);
16088                        }
16089                        cached_row_decode
16090                            .cached_value(col_idx)
16091                            .cloned()
16092                            .unwrap_or(SqliteValue::Null)
16093                    } else if let Some(value) = fsqlite_types::record::decode_column_from_offset(
16094                        &row.blob,
16095                        cached_row_decode
16096                            .column_offset(col_idx)
16097                            .expect("col_idx checked against column_count"),
16098                        collect_vdbe_metrics,
16099                    ) {
16100                        note_decode_cache_miss(collect_vdbe_metrics);
16101                        cached_row_decode.cache_decoded(col_idx, value.clone());
16102                        value
16103                    } else {
16104                        return Err(FrankenError::DatabaseCorrupt {
16105                            detail: format!(
16106                                "malformed sorter record while reading column {col_idx}"
16107                            ),
16108                        });
16109                    }
16110                } else {
16111                    SqliteValue::Null
16112                };
16113                if collect_vdbe_metrics {
16114                    record_decoded_value_metrics(&value);
16115                }
16116                return Ok(value);
16117            }
16118        }
16119
16120        Ok(SqliteValue::Null)
16121    }
16122
16123    /// Get the rowid from the cursor's current row.
16124    async fn cursor_rowid(&mut self, cursor_id: i32) -> Result<SqliteValue> {
16125        if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
16126            if cursor.cursor.eof() {
16127                return Ok(SqliteValue::Null);
16128            }
16129            return Ok(SqliteValue::Integer(
16130                storage_cursor_cached_rowid(cursor).await?,
16131            ));
16132        }
16133
16134        if let Some(state) = self
16135            .cold_state()
16136            .and_then(|cold_state| cold_state.vtab_cursors.get(&cursor_id))
16137        {
16138            if state.null_row || state.cursor.eof() {
16139                return Ok(SqliteValue::Null);
16140            }
16141            return Ok(SqliteValue::Integer(state.cursor.rowid()?));
16142        }
16143
16144        if let Some(cursor) = self.cursors.get(&cursor_id)
16145            && let Some(pos) = cursor.position
16146            && let Some(db) = self.db.as_ref()
16147            && let Some(table) = db.get_table(cursor.root_page)
16148            && let Some(row) = table.rows.get(pos)
16149        {
16150            return Ok(SqliteValue::Integer(row.rowid));
16151        }
16152        Ok(SqliteValue::Null)
16153    }
16154
16155    #[allow(clippy::cast_sign_loss)]
16156    async fn open_storage_cursor(
16157        &mut self,
16158        cursor_id: i32,
16159        root_page: i32,
16160        writable: bool,
16161    ) -> bool {
16162        let _page_size_u32 = self.page_size.get();
16163        // bd-1xrs: storage_cursors_enabled check removed.
16164        // StorageCursor is now the ONLY cursor path.
16165        let trace_id = self.execution_cx.trace_id();
16166        let certifying_mode = self.reject_mem_fallback;
16167        let mode = if self.reject_mem_fallback {
16168            "parity_cert"
16169        } else {
16170            "fallback_allowed"
16171        };
16172
16173        let Some(root_pgno) = PageNumber::new(root_page as u32) else {
16174            Self::log_open_storage_cursor_fallback_decision(
16175                trace_id,
16176                certifying_mode,
16177                cursor_id,
16178                root_page,
16179                writable,
16180                "none",
16181                "invalid_page_number",
16182                "refused",
16183                "invalid root page number",
16184            );
16185            tracing::debug!(
16186                cursor_id,
16187                root_page,
16188                writable,
16189                mode,
16190                backend_kind = "none",
16191                decision_reason = "invalid_page_number",
16192                "open_storage_cursor: invalid root page number"
16193            );
16194            return false;
16195        };
16196
16197        let has_txn = self.txn_page_io.is_some();
16198        let txn_cx = self.derive_execution_cx();
16199        let (rowid_mode, autoincrement_high_water) = self.storage_cursor_runtime_meta(root_page);
16200        if self.cursor_root_pages.get(&cursor_id).copied() == Some(root_page)
16201            && let Some(existing) = self.storage_cursors.get_mut(&cursor_id)
16202        {
16203            let backend_matches = if has_txn {
16204                existing.cursor.is_txn()
16205            } else {
16206                existing.cursor.is_mem()
16207            };
16208            if backend_matches {
16209                existing.writable |= writable;
16210                existing.cx = txn_cx.clone();
16211                existing.root_page = root_page;
16212                existing.rowid_mode = rowid_mode;
16213                existing.autoincrement_high_water = autoincrement_high_water;
16214                self.cursor_root_pages.insert(cursor_id, root_page);
16215                tracing::trace!(
16216                    cursor_id,
16217                    page_id = root_page,
16218                    writable,
16219                    has_txn,
16220                    backend_kind = existing.cursor.kind_str(),
16221                    "open_storage_cursor: reused retained cursor"
16222                );
16223                return true;
16224            }
16225        }
16226        let mut mem_decision_reason = "no_pager_transaction";
16227
16228        // Phase 5C.1 (bd-35my): Route through pager when available.
16229        //
16230        // Critical safety rule:
16231        // If a pager transaction exists, writable cursors must NEVER fall back
16232        // to MemPageStore. A writable fallback can silently route writes to a
16233        // non-durable in-memory copy and create divergence/corruption under
16234        // concurrency.
16235        //
16236        // Read-only fallback remains allowed when parity-cert is disabled and
16237        // MemDatabase owns the root page (for example, materialized view /
16238        // sqlite_master snapshots that were never pager-backed).
16239        //
16240        // The only acceptable writable pager "bootstrap" case is a truly
16241        // zero-initialized root page that we can initialize in-place.
16242        // Use a labeled block so we can break out to the MemDatabase fallback path
16243        // when the pager read fails but MemDatabase has the table.
16244        'pager_block: {
16245            if let Some(ref mut page_io) = self.txn_page_io {
16246                // If the pager read itself fails, check if MemDatabase can serve
16247                // this table before failing. This handles view materialization where
16248                // MemDatabase allocates root pages beyond the pager's db_size.
16249                let page_data = match page_io.read_page(&txn_cx, root_pgno).await {
16250                    Ok(bytes) => bytes,
16251                    Err(err) => {
16252                        // Check if MemDatabase can serve this table.
16253                        let has_mem_table = self
16254                            .db
16255                            .as_ref()
16256                            .is_some_and(|db| db.get_table(root_page).is_some());
16257                        if has_mem_table && !self.reject_mem_fallback && !writable {
16258                            mem_decision_reason = "pager_read_failed_mem_fallback";
16259                            Self::log_open_storage_cursor_fallback_decision(
16260                                trace_id,
16261                                certifying_mode,
16262                                cursor_id,
16263                                root_page,
16264                                writable,
16265                                "mem",
16266                                "pager_read_failed_mem_fallback",
16267                                "vdbe_mempage_fallback_allowed",
16268                                "pager read failed and MemDatabase owns read-only root page",
16269                            );
16270                            tracing::debug!(
16271                                cursor_id,
16272                                page_id = root_page,
16273                                writable,
16274                                has_txn,
16275                                mode,
16276                                backend_kind = "mem",
16277                                decision_reason = "pager_read_failed_mem_fallback",
16278                                error = %err,
16279                                "open_storage_cursor: pager read failed, falling through to MemDatabase"
16280                            );
16281                            // Break out of pager_block to fall through to MemDatabase path.
16282                            break 'pager_block;
16283                        }
16284                        Self::log_open_storage_cursor_fallback_decision(
16285                            trace_id,
16286                            certifying_mode,
16287                            cursor_id,
16288                            root_page,
16289                            writable,
16290                            "txn",
16291                            "pager_read_failed",
16292                            "refused",
16293                            "pager read failed before storage cursor dispatch",
16294                        );
16295                        tracing::warn!(
16296                            cursor_id,
16297                            page_id = root_page,
16298                            writable,
16299                            has_txn,
16300                            mode,
16301                            backend_kind = "txn",
16302                            decision_reason = "pager_read_failed",
16303                            error = %err,
16304                            has_mem_table,
16305                            reject_mem_fallback = self.reject_mem_fallback,
16306                            "open_storage_cursor: failed to read root page from pager"
16307                        );
16308                        return false;
16309                    }
16310                };
16311                let hdr_offset = header_offset_for_page(root_pgno);
16312                let parsed_header = BtreePageHeader::parse(&page_data, hdr_offset).ok();
16313                // A page is a valid B-tree if the header parses successfully.
16314                // Legacy fallback: synthetic test pages with non-zero first byte
16315                // but unparseable header are also accepted, but we infer
16316                // is_table from the raw page-type byte rather than defaulting
16317                // to true (which was incorrect for index pages).
16318                let is_valid_btree =
16319                    parsed_header.is_some() || (!page_data.is_empty() && page_data[0] != 0x00);
16320                let is_zero_page = page_data.iter().all(|&byte| byte == 0);
16321
16322                let page_layout = btree_cursor_page_layout_for_reader_or_default(
16323                    page_io,
16324                    &txn_cx,
16325                    self.page_size,
16326                )
16327                .await;
16328
16329                if is_valid_btree {
16330                    // Real B-tree backed by pager: infer table-vs-index from the
16331                    // parsed page header when available, falling back to the raw
16332                    // page-type byte for synthetic/legacy pages.
16333                    let (is_table_btree, detected_page_type) = if let Some(header) = parsed_header {
16334                        (header.page_type.is_table(), Some(header.page_type))
16335                    } else {
16336                        // Header parse failed but page has data. Use the raw
16337                        // page-type flag byte at the header offset to infer
16338                        // table vs index — do NOT blindly default to table.
16339                        let type_byte = page_data.get(hdr_offset).copied().unwrap_or(0);
16340                        let is_table = match type_byte {
16341                            0x02 | 0x0A => false, // InteriorIndex / LeafIndex
16342                            0x05 | 0x0D => true,  // InteriorTable / LeafTable
16343                            _ => {
16344                                tracing::warn!(
16345                                    cursor_id,
16346                                    page_id = root_page,
16347                                    type_byte,
16348                                    "open_storage_cursor: unparseable header with unknown page-type byte, defaulting to table"
16349                                );
16350                                true
16351                            }
16352                        };
16353                        (is_table, None)
16354                    };
16355                    let mut cursor = BtCursor::new_with_index_desc(
16356                        page_io.clone(),
16357                        root_pgno,
16358                        page_layout.usable_size,
16359                        is_table_btree,
16360                        if is_table_btree {
16361                            Vec::new()
16362                        } else {
16363                            self.index_desc_flags_for_root(root_page)
16364                        },
16365                    );
16366                    if !is_table_btree {
16367                        cursor.set_index_collation_context(
16368                            self.index_collations_for_root(root_page),
16369                            Arc::clone(&self.collation_registry),
16370                        );
16371                    }
16372                    configure_btree_cursor_page_size(&mut cursor, page_layout);
16373                    let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16374                    let table_column_count = self
16375                        .table_column_count_by_root_page
16376                        .get(&root_page)
16377                        .copied();
16378                    let first_not_null_non_ipk_col = self
16379                        .first_not_null_non_ipk_col_by_root_page
16380                        .get(&root_page)
16381                        .copied();
16382                    self.storage_cursors.insert(
16383                        cursor_id,
16384                        StorageCursor {
16385                            cursor: CursorBackend::Txn(cursor),
16386                            cx: txn_cx,
16387                            writable,
16388                            root_page,
16389                            rowid_mode,
16390                            autoincrement_high_water,
16391                            last_alloc_rowid: 0,
16392                            last_successful_insert_rowid: None,
16393                            last_rightmost_unique_index_prefix: None,
16394                            last_rightmost_unique_index_position: None,
16395                            payload_buf: Vec::new(),
16396                            target_vals_buf: Vec::new(),
16397                            cur_vals_buf: Vec::new(),
16398                            row_decode: RowDecodeScratch::default(),
16399                            last_position_stamp: None,
16400                            cached_rowid: None,
16401                            payload_includes_rowid_alias: None,
16402                            ipk_col_idx,
16403                            table_column_count,
16404                            first_not_null_non_ipk_col,
16405                        },
16406                    );
16407                    self.cursor_root_pages.insert(cursor_id, root_page);
16408                    Self::log_open_storage_cursor_fallback_decision(
16409                        trace_id,
16410                        certifying_mode,
16411                        cursor_id,
16412                        root_page,
16413                        writable,
16414                        "txn",
16415                        "valid_btree_page",
16416                        "real_backend_dispatch",
16417                        "pager transaction opened a valid B-tree page",
16418                    );
16419                    tracing::debug!(
16420                        cursor_id,
16421                        page_id = root_page,
16422                        writable,
16423                        has_txn,
16424                        mode,
16425                        backend_kind = "txn",
16426                        decision_reason = "valid_btree_page",
16427                        detected_page_type = ?detected_page_type,
16428                        is_table_btree,
16429                        "open_storage_cursor: routed through pager transaction"
16430                    );
16431                    return true;
16432                }
16433
16434                // For writable cursors on truly zeroed pages (e.g., freshly
16435                // allocated roots), initialize an empty root page.
16436                if writable && is_zero_page {
16437                    // Infer root kind: check the index metadata map first (it
16438                    // is populated from the schema and correctly identifies all
16439                    // index root pages, including autoindexes). Fall back to
16440                    // MemDatabase only when the index map has no opinion.
16441                    // MemDatabase::get_table() returns Some for BOTH table and
16442                    // autoindex root pages (because create_table_at is called
16443                    // for both), so using it alone would misclassify autoindex
16444                    // pages as tables — causing "wrong # of entries" corruption
16445                    // when stock SQLite reads the file (issue #55).
16446                    let is_known_index =
16447                        self.index_desc_flags_by_root_page.contains_key(&root_page)
16448                            || self.index_collations_by_root_page.contains_key(&root_page);
16449                    let is_table_btree = if is_known_index {
16450                        false
16451                    } else {
16452                        self.db
16453                            .as_ref()
16454                            .is_none_or(|db| db.get_table(root_page).is_some())
16455                    };
16456                    let init_page_type = if is_table_btree {
16457                        BtreePageType::LeafTable
16458                    } else {
16459                        BtreePageType::LeafIndex
16460                    };
16461                    // Initialize empty leaf page for the inferred B-tree kind.
16462                    // The page buffer starts zeroed, so only the page-type flag
16463                    // and cell-content offset need to be populated explicitly.
16464                    let mut page = vec![0u8; page_layout.page_size as usize];
16465                    page[0] = if is_table_btree {
16466                        fsqlite_types::BTreePageType::LeafTable as u8
16467                    } else {
16468                        fsqlite_types::BTreePageType::LeafIndex as u8
16469                    };
16470                    let content_raw = if page_layout.usable_size >= 65_536 {
16471                        0u16
16472                    } else {
16473                        page_layout.usable_size as u16
16474                    };
16475                    page[5..7].copy_from_slice(&content_raw.to_be_bytes());
16476
16477                    // Write the initialized page to pager. The buffer is owned,
16478                    // exactly page-size, and dead after this call, so move it into
16479                    // the owned-passthrough write lane (`write_page_data`) instead
16480                    // of the borrowed `write_page` lane, which clones a second
16481                    // full-page image via `to_vec()` in `normalize_owned_page_data`.
16482                    if let Err(err) = page_io
16483                        .write_page_data(&txn_cx, root_pgno, PageData::from_vec(page))
16484                        .await
16485                    {
16486                        Self::log_open_storage_cursor_fallback_decision(
16487                            trace_id,
16488                            certifying_mode,
16489                            cursor_id,
16490                            root_page,
16491                            writable,
16492                            "txn",
16493                            "zero_page_init_failed",
16494                            "refused",
16495                            "failed to initialize writable root page in pager",
16496                        );
16497                        tracing::warn!(
16498                            cursor_id,
16499                            page_id = root_page,
16500                            writable,
16501                            has_txn,
16502                            mode,
16503                            backend_kind = "txn",
16504                            decision_reason = "zero_page_init_failed",
16505                            error = %err,
16506                            "open_storage_cursor: failed to initialize writable root page in pager"
16507                        );
16508                        return false;
16509                    }
16510                    let mut cursor = BtCursor::new_with_index_desc(
16511                        page_io.clone(),
16512                        root_pgno,
16513                        page_layout.usable_size,
16514                        is_table_btree,
16515                        if is_table_btree {
16516                            Vec::new()
16517                        } else {
16518                            self.index_desc_flags_for_root(root_page)
16519                        },
16520                    );
16521                    if !is_table_btree {
16522                        cursor.set_index_collation_context(
16523                            self.index_collations_for_root(root_page),
16524                            Arc::clone(&self.collation_registry),
16525                        );
16526                    }
16527                    configure_btree_cursor_page_size(&mut cursor, page_layout);
16528                    let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16529                    let table_column_count = self
16530                        .table_column_count_by_root_page
16531                        .get(&root_page)
16532                        .copied();
16533                    let first_not_null_non_ipk_col = self
16534                        .first_not_null_non_ipk_col_by_root_page
16535                        .get(&root_page)
16536                        .copied();
16537                    self.storage_cursors.insert(
16538                        cursor_id,
16539                        StorageCursor {
16540                            cursor: CursorBackend::Txn(cursor),
16541                            cx: txn_cx,
16542                            writable,
16543                            root_page,
16544                            rowid_mode,
16545                            autoincrement_high_water,
16546                            last_alloc_rowid: 0,
16547                            last_successful_insert_rowid: None,
16548                            last_rightmost_unique_index_prefix: None,
16549                            last_rightmost_unique_index_position: None,
16550                            payload_buf: Vec::new(),
16551                            target_vals_buf: Vec::new(),
16552                            cur_vals_buf: Vec::new(),
16553                            row_decode: RowDecodeScratch::default(),
16554                            last_position_stamp: None,
16555                            cached_rowid: None,
16556                            payload_includes_rowid_alias: None,
16557                            ipk_col_idx,
16558                            table_column_count,
16559                            first_not_null_non_ipk_col,
16560                        },
16561                    );
16562                    self.cursor_root_pages.insert(cursor_id, root_page);
16563                    Self::log_open_storage_cursor_fallback_decision(
16564                        trace_id,
16565                        certifying_mode,
16566                        cursor_id,
16567                        root_page,
16568                        writable,
16569                        "txn",
16570                        "zero_page_initialized",
16571                        "real_backend_dispatch",
16572                        "initialized empty root page through pager transaction",
16573                    );
16574                    tracing::debug!(
16575                        cursor_id,
16576                        page_id = root_page,
16577                        writable,
16578                        has_txn,
16579                        mode,
16580                        backend_kind = "txn",
16581                        decision_reason = "zero_page_initialized",
16582                        initialized_page_type = ?init_page_type,
16583                        is_table_btree,
16584                        "open_storage_cursor: initialized empty root page via pager"
16585                    );
16586                    return true;
16587                }
16588
16589                // If the page is zero/invalid but MemDatabase has this table
16590                // (e.g., materialized sqlite_master virtual table), read-only
16591                // cursors may fall through to the MemDatabase path below when
16592                // parity-cert is disabled. Writable cursors must still refuse.
16593                let has_mem_table = self
16594                    .db
16595                    .as_ref()
16596                    .is_some_and(|db| db.get_table(root_page).is_some());
16597                if !has_mem_table || writable {
16598                    // No MemDatabase fallback available — refuse to open.
16599                    Self::log_open_storage_cursor_fallback_decision(
16600                        trace_id,
16601                        certifying_mode,
16602                        cursor_id,
16603                        root_page,
16604                        writable,
16605                        "none",
16606                        "invalid_page_no_mem_fallback",
16607                        "refused",
16608                        "invalid transaction-backed root page has no allowed MemDatabase fallback",
16609                    );
16610                    tracing::warn!(
16611                        cursor_id,
16612                        page_id = root_page,
16613                        writable,
16614                        has_txn,
16615                        mode,
16616                        backend_kind = "none",
16617                        decision_reason = "invalid_page_no_mem_fallback",
16618                        first_byte = page_data.first().copied().unwrap_or_default(),
16619                        is_zero_page,
16620                        has_mem_table,
16621                        "open_storage_cursor: refusing on invalid transaction-backed root page"
16622                    );
16623                    return false;
16624                }
16625                // else: fall through to MemDatabase path
16626                mem_decision_reason = "txn_page_invalid_mem_fallback";
16627                Self::log_open_storage_cursor_fallback_decision(
16628                    trace_id,
16629                    certifying_mode,
16630                    cursor_id,
16631                    root_page,
16632                    writable,
16633                    "mem",
16634                    "txn_page_invalid_mem_fallback",
16635                    "vdbe_mempage_fallback_allowed",
16636                    "invalid pager page can be served by read-only MemDatabase table",
16637                );
16638                tracing::debug!(
16639                    cursor_id,
16640                    page_id = root_page,
16641                    writable,
16642                    has_txn,
16643                    mode,
16644                    backend_kind = "mem",
16645                    decision_reason = "txn_page_invalid_mem_fallback",
16646                    "open_storage_cursor: pager page invalid, falling through to MemDatabase"
16647                );
16648            } // end if let Some(ref mut page_io)
16649        } // end 'pager_block
16650
16651        // bd-2ttd8.1: Parity-certification mode — reject MemPageStore fallback.
16652        if self.reject_mem_fallback {
16653            Self::log_open_storage_cursor_fallback_decision(
16654                trace_id,
16655                certifying_mode,
16656                cursor_id,
16657                root_page,
16658                writable,
16659                "mem",
16660                "parity_cert_rejection",
16661                "vdbe_mempage_fallback_rejected",
16662                "MemPageStore fallback rejected in parity-cert mode",
16663            );
16664            tracing::warn!(
16665                cursor_id,
16666                page_id = root_page,
16667                writable,
16668                has_txn,
16669                mode,
16670                backend_kind = "mem",
16671                decision_reason = "parity_cert_rejection",
16672                "open_storage_cursor: MemPageStore fallback rejected in parity-cert mode"
16673            );
16674            return false;
16675        }
16676
16677        // Fallback: build a transient B-tree snapshot (Phase 4 path used by
16678        // tests without a real pager). Both read and write cursors can operate
16679        // on empty tables (INSERT needs to work on new tables).
16680        let is_table_btree = self
16681            .db
16682            .as_ref()
16683            .is_none_or(|db| db.get_table(root_page).is_some());
16684        let transient_root_pgno = transient_mem_root_pgno(root_pgno);
16685        let store = if is_table_btree {
16686            MemPageStore::with_empty_table(transient_root_pgno, self.page_size.get())
16687        } else {
16688            MemPageStore::with_empty_index(transient_root_pgno, self.page_size.get())
16689        };
16690        let cx = self.derive_execution_cx();
16691        let mut cursor = BtCursor::new_with_index_desc(
16692            store,
16693            transient_root_pgno,
16694            self.page_size.get(),
16695            is_table_btree,
16696            if is_table_btree {
16697                Vec::new()
16698            } else {
16699                self.index_desc_flags_for_root(root_page)
16700            },
16701        );
16702        if !is_table_btree {
16703            cursor.set_index_collation_context(
16704                self.index_collations_for_root(root_page),
16705                Arc::clone(&self.collation_registry),
16706            );
16707        }
16708        // Populate cursor from MemDatabase if available.
16709        if is_table_btree
16710            && let Some(table) = self.db.as_ref().and_then(|db| db.get_table(root_page))
16711        {
16712            for row in &table.rows {
16713                let payload = encode_record(&row.values);
16714                if cursor.table_insert(&cx, row.rowid, &payload).await.is_err() {
16715                    return false;
16716                }
16717            }
16718        }
16719
16720        let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16721        let table_column_count = self
16722            .table_column_count_by_root_page
16723            .get(&root_page)
16724            .copied();
16725        let first_not_null_non_ipk_col = self
16726            .first_not_null_non_ipk_col_by_root_page
16727            .get(&root_page)
16728            .copied();
16729        self.storage_cursors.insert(
16730            cursor_id,
16731            StorageCursor {
16732                cursor: CursorBackend::Mem(cursor),
16733                cx,
16734                writable,
16735                root_page,
16736                rowid_mode,
16737                autoincrement_high_water,
16738                last_alloc_rowid: 0,
16739                last_successful_insert_rowid: None,
16740                last_rightmost_unique_index_prefix: None,
16741                last_rightmost_unique_index_position: None,
16742                payload_buf: Vec::new(),
16743                target_vals_buf: Vec::new(),
16744                cur_vals_buf: Vec::new(),
16745                row_decode: RowDecodeScratch::default(),
16746                last_position_stamp: None,
16747                cached_rowid: None,
16748                payload_includes_rowid_alias: None,
16749                ipk_col_idx,
16750                table_column_count,
16751                first_not_null_non_ipk_col,
16752            },
16753        );
16754        self.cursor_root_pages.insert(cursor_id, root_page);
16755        Self::log_open_storage_cursor_fallback_decision(
16756            trace_id,
16757            certifying_mode,
16758            cursor_id,
16759            root_page,
16760            writable,
16761            "mem",
16762            mem_decision_reason,
16763            "vdbe_mempage_fallback_allowed",
16764            "routed through transient MemPageStore fallback",
16765        );
16766        tracing::debug!(
16767            cursor_id,
16768            page_id = root_page,
16769            writable,
16770            has_txn,
16771            mode,
16772            backend_kind = "mem",
16773            decision_reason = mem_decision_reason,
16774            is_table_btree,
16775            "open_storage_cursor: routed through MemPageStore fallback"
16776        );
16777        true
16778    }
16779
16780    fn trace_opcode(&self, pc: usize, op: &VdbeOp) {
16781        if !self.trace_opcodes || !tracing::enabled!(tracing::Level::TRACE) {
16782            return;
16783        }
16784        let spans = opcode_register_spans(op);
16785        tracing::trace!(
16786            target: "fsqlite_vdbe::opcode",
16787            logging_standard = VDBE_TRACE_LOGGING_STANDARD,
16788            pc,
16789            opcode = %op.opcode.name(),
16790            p1 = op.p1,
16791            p2 = op.p2,
16792            p3 = op.p3,
16793            p5 = op.p5,
16794            read_start = spans.read_start,
16795            read_len = spans.read_len,
16796            write_start = spans.write_start,
16797            write_len = spans.write_len,
16798            "executing vdbe opcode",
16799        );
16800    }
16801}
16802
16803// ── SQLite record encoding ──────────────────────────────────────────────
16804//
16805// SQLite `OP_MakeRecord` produces a record in the on-disk record format
16806// (header + body). Using the same format internally avoids later translation
16807// when wiring VDBE cursors to the real B-tree layer.
16808
16809/// SQLite affinity constants (from §3.2 of datatype3.html).
16810/// Encoded in the lower bits of comparison opcode p5 (masked by 0x47).
16811const SQLITE_AFF_MASK: u16 = 0x47;
16812const SQLITE_AFF_TEXT: u16 = 0x42; // 'B'
16813const SQLITE_AFF_NUMERIC: u16 = 0x43; // 'C'
16814
16815/// C SQLite OP_If/OP_IfNot truthiness: uses `sqlite3VdbeRealValue() != 0.0`,
16816/// which means 0.1, 0.5, -0.1 etc. are all truthy (unlike integer truncation).
16817fn vdbe_real_is_truthy(val: &SqliteValue) -> bool {
16818    match val {
16819        SqliteValue::Null => false,
16820        SqliteValue::Integer(n) => *n != 0,
16821        SqliteValue::Float(f) => *f != 0.0,
16822        SqliteValue::Text(_) | SqliteValue::Blob(_) => {
16823            let i = val.to_integer();
16824            if i != 0 {
16825                return true;
16826            }
16827            val.to_float() != 0.0
16828        }
16829    }
16830}
16831
16832/// Apply SQLite comparison affinity coercion (§3.2 of datatype3.html).
16833///
16834/// Comparison affinity is applied independently to each operand. TEXT affinity
16835/// stringifies numeric values, while a numeric-class affinity attempts to
16836/// convert every TEXT value to numeric. When p5 is 0 or carries BLOB affinity
16837/// (0x41), no coercion is performed — values compare using their native storage
16838/// classes (NULL < numeric < text < blob).
16839fn coerce_for_comparison<'a>(
16840    lhs: &'a SqliteValue,
16841    rhs: &'a SqliteValue,
16842    p5: u16,
16843) -> (
16844    std::borrow::Cow<'a, SqliteValue>,
16845    std::borrow::Cow<'a, SqliteValue>,
16846) {
16847    use std::borrow::Cow;
16848
16849    let affinity = p5 & SQLITE_AFF_MASK;
16850
16851    // TEXT affinity (0x42): convert numeric operands to text for comparison.
16852    if affinity == SQLITE_AFF_TEXT {
16853        let coerce_to_text = |v: &SqliteValue| -> Option<SqliteValue> {
16854            match v {
16855                SqliteValue::Integer(_) | SqliteValue::Float(_) => {
16856                    Some(SqliteValue::Text(v.to_text().into()))
16857                }
16858                _ => None,
16859            }
16860        };
16861        let new_lhs = coerce_to_text(lhs);
16862        let new_rhs = coerce_to_text(rhs);
16863        return (
16864            new_lhs.map_or_else(|| Cow::Borrowed(lhs), Cow::Owned),
16865            new_rhs.map_or_else(|| Cow::Borrowed(rhs), Cow::Owned),
16866        );
16867    }
16868
16869    // Numeric affinity (>= 0x43): independently attempt text→numeric on both
16870    // operands. Whether the opposite runtime value is already numeric must not
16871    // affect application of the opcode's declared comparison affinity.
16872    if affinity >= SQLITE_AFF_NUMERIC {
16873        let coerce_to_numeric = |value: &SqliteValue| match value {
16874            SqliteValue::Text(text) => try_coerce_text_to_numeric_cmp(text),
16875            _ => None,
16876        };
16877        let new_lhs = coerce_to_numeric(lhs);
16878        let new_rhs = coerce_to_numeric(rhs);
16879        return (
16880            new_lhs.map_or_else(|| Cow::Borrowed(lhs), Cow::Owned),
16881            new_rhs.map_or_else(|| Cow::Borrowed(rhs), Cow::Owned),
16882        );
16883    }
16884
16885    (Cow::Borrowed(lhs), Cow::Borrowed(rhs))
16886}
16887
16888fn values_equal_without_collation(lhs: &SqliteValue, rhs: &SqliteValue) -> bool {
16889    if lhs.is_null() || rhs.is_null() {
16890        return false;
16891    }
16892    if let (SqliteValue::Integer(a), SqliteValue::Integer(b)) = (lhs, rhs) {
16893        return a == b;
16894    }
16895    let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, 0);
16896    cmp_lhs.partial_cmp(&cmp_rhs) == Some(Ordering::Equal)
16897}
16898
16899/// Try to parse a text string as a numeric value for comparison coercion.
16900fn try_coerce_text_to_numeric_cmp(s: &str) -> Option<SqliteValue> {
16901    let trimmed = s.trim();
16902    if trimmed.is_empty() {
16903        return None;
16904    }
16905    // Try integer first.
16906    if let Ok(i) = trimmed.parse::<i64>() {
16907        return Some(SqliteValue::Integer(i));
16908    }
16909    // Try float.
16910    if let Ok(f) = trimmed.parse::<f64>() {
16911        if !f.is_finite() {
16912            let lower = trimmed.to_ascii_lowercase();
16913            if lower.contains("inf") || lower.contains("nan") {
16914                return None;
16915            }
16916        }
16917        return Some(SqliteValue::Float(f));
16918    }
16919    None
16920}
16921
16922fn collate_compare(
16923    lhs: &SqliteValue,
16924    rhs: &SqliteValue,
16925    coll_name: &str,
16926    collation_registry: &CollationRegistry,
16927) -> Option<std::cmp::Ordering> {
16928    match (lhs, rhs) {
16929        (SqliteValue::Text(l), SqliteValue::Text(r)) => Some(compare_text_with_collation(
16930            l.as_bytes_direct(),
16931            r.as_bytes_direct(),
16932            coll_name,
16933            collation_registry,
16934        )),
16935        _ => lhs.partial_cmp(rhs),
16936    }
16937}
16938
16939fn compare_text_with_collation(
16940    left: &[u8],
16941    right: &[u8],
16942    coll_name: &str,
16943    collation_registry: &CollationRegistry,
16944) -> Ordering {
16945    collation_registry
16946        .find(coll_name)
16947        .map(|collation| collation.compare(left, right))
16948        .unwrap_or_else(|| left.cmp(right))
16949}
16950
16951fn builtin_collation_compare_text(
16952    left: &SmallText,
16953    right: &SmallText,
16954    coll_name: &str,
16955) -> Option<Ordering> {
16956    let left = left.as_bytes_direct();
16957    let right = right.as_bytes_direct();
16958    if coll_name.eq_ignore_ascii_case("BINARY") {
16959        return Some(left.cmp(right));
16960    }
16961    if coll_name.eq_ignore_ascii_case("NOCASE") {
16962        return Some(compare_ascii_nocase_bytes(left, right));
16963    }
16964    if coll_name.eq_ignore_ascii_case("RTRIM") {
16965        return Some(trim_rtrim_collation_text(left).cmp(trim_rtrim_collation_text(right)));
16966    }
16967    None
16968}
16969
16970fn cmp_sqlite_values_collated(a: &SqliteValue, b: &SqliteValue, coll: &str) -> Ordering {
16971    match (a, b) {
16972        (SqliteValue::Text(l), SqliteValue::Text(r)) => builtin_collation_compare_text(l, r, coll)
16973            .unwrap_or_else(|| l.as_bytes_direct().cmp(r.as_bytes_direct())),
16974        _ => a.partial_cmp(b).unwrap_or(Ordering::Equal),
16975    }
16976}
16977
16978fn agg_step_min_max_collated(
16979    state: &mut Box<dyn Any + Send>,
16980    candidate: &SqliteValue,
16981    is_max: bool,
16982    coll: &str,
16983) {
16984    let current: &mut Option<SqliteValue> = state
16985        .downcast_mut()
16986        .expect("MIN/MAX aggregate state must be Option<SqliteValue>");
16987    match current {
16988        None => *current = Some(candidate.clone()),
16989        &mut Some(ref cur) => {
16990            let ord = cmp_sqlite_values_collated(candidate, cur, coll);
16991            if (is_max && ord == Ordering::Greater) || (!is_max && ord == Ordering::Less) {
16992                *current = Some(candidate.clone());
16993            }
16994        }
16995    }
16996}
16997
16998fn compare_ascii_nocase_bytes(left: &[u8], right: &[u8]) -> Ordering {
16999    let shared_len = left.len().min(right.len());
17000    for idx in 0..shared_len {
17001        let l = left[idx].to_ascii_lowercase();
17002        let r = right[idx].to_ascii_lowercase();
17003        match l.cmp(&r) {
17004            Ordering::Equal => {}
17005            non_equal => return non_equal,
17006        }
17007    }
17008    left.len().cmp(&right.len())
17009}
17010
17011fn fast_compare_same_storage_class(
17012    lhs: &SqliteValue,
17013    rhs: &SqliteValue,
17014    p4: &P4,
17015    p5: u16,
17016) -> Option<Option<Ordering>> {
17017    let affinity = p5 & SQLITE_AFF_MASK;
17018    match (lhs, rhs) {
17019        (SqliteValue::Integer(a), SqliteValue::Integer(b)) if affinity != SQLITE_AFF_TEXT => {
17020            Some(Some(a.cmp(b)))
17021        }
17022        (SqliteValue::Float(a), SqliteValue::Float(b)) if affinity != SQLITE_AFF_TEXT => {
17023            Some(a.partial_cmp(b))
17024        }
17025        (SqliteValue::Text(a), SqliteValue::Text(b)) if affinity < SQLITE_AFF_NUMERIC => match p4 {
17026            P4::Collation(coll_name) => builtin_collation_compare_text(a, b, coll_name).map(Some),
17027            _ => Some(Some(a.as_bytes_direct().cmp(b.as_bytes_direct()))),
17028        },
17029        (SqliteValue::Blob(a), SqliteValue::Blob(b)) if !matches!(p4, P4::Collation(_)) => {
17030            Some(Some(a.as_ref().cmp(b.as_ref())))
17031        }
17032        _ => None,
17033    }
17034}
17035
17036fn extract_collation_names_owned(p4: &P4) -> Vec<Option<String>> {
17037    match p4 {
17038        P4::Collation(name) => vec![Some(name.clone())],
17039        P4::Str(spec) => spec
17040            .split([',', '|', '\0'])
17041            .map(str::trim)
17042            .filter(|entry| !entry.is_empty())
17043            .map(|s| Some(s.to_owned()))
17044            .collect(),
17045        _ => Vec::new(),
17046    }
17047}
17048
17049fn compare_collation_for_field_from_p4(p4: &P4, field_idx: usize) -> Option<&str> {
17050    match p4 {
17051        P4::Collation(name) => Some(name.as_str()),
17052        P4::Str(spec) => {
17053            let mut idx = 0;
17054            for entry in spec.split([',', '|', '\0']) {
17055                let trimmed = entry.trim();
17056                if trimmed.is_empty() {
17057                    continue;
17058                }
17059                if idx == field_idx {
17060                    return Some(trimmed);
17061                }
17062                idx += 1;
17063            }
17064            if idx == 1 && field_idx > 0 {
17065                for entry in spec.split([',', '|', '\0']) {
17066                    let trimmed = entry.trim();
17067                    if !trimmed.is_empty() {
17068                        return Some(trimmed);
17069                    }
17070                }
17071            }
17072            None
17073        }
17074        _ => None,
17075    }
17076}
17077
17078/// For REPLACE conflict resolution: re-seek the index cursor, find the
17079/// conflicting entry (which has matching indexed columns but a different
17080/// rowid), delete it from the index, and return its rowid so the caller
17081/// can delete the old table row.
17082fn try_decode_storage_cursor_target_index_record(
17083    cursor: &mut StorageCursor,
17084    key_bytes: &[u8],
17085) -> bool {
17086    cursor.target_vals_buf.clear();
17087    fsqlite_types::record::parse_record_into(key_bytes, &mut cursor.target_vals_buf).is_some()
17088}
17089
17090fn decode_storage_cursor_target_index_record_strict(
17091    cursor: &mut StorageCursor,
17092    key_bytes: &[u8],
17093    malformed_detail: &'static str,
17094) -> Result<()> {
17095    try_decode_storage_cursor_target_index_record(cursor, key_bytes)
17096        .then_some(())
17097        .ok_or_else(|| FrankenError::internal(malformed_detail))
17098}
17099
17100async fn try_decode_storage_cursor_current_index_record(
17101    cursor_id: i32,
17102    cursor: &mut StorageCursor,
17103) -> Result<bool> {
17104    refresh_storage_cursor_first_key_state(cursor, false);
17105    cursor.payload_buf.clear();
17106    cursor
17107        .cursor
17108        .payload_into(&cursor.cx, &mut cursor.payload_buf)
17109        .await?;
17110    cursor.cur_vals_buf.clear();
17111    let decoded =
17112        fsqlite_types::record::parse_record_into(&cursor.payload_buf, &mut cursor.cur_vals_buf)
17113            .is_some();
17114    cursor.cached_rowid = if decoded {
17115        cursor.cur_vals_buf.last().and_then(SqliteValue::as_integer)
17116    } else {
17117        None
17118    };
17119    tracing::trace!(
17120        cursor_id,
17121        decoded,
17122        payload_len = cursor.payload_buf.len(),
17123        decoded_value_count = cursor.cur_vals_buf.len(),
17124        scratch_payload_capacity = cursor.payload_buf.capacity(),
17125        scratch_value_capacity = cursor.cur_vals_buf.capacity(),
17126        scratch_model = "per_cursor",
17127        decode_path = "storage_cursor_index_probe",
17128        "decoded storage cursor index probe into reusable scratch"
17129    );
17130    Ok(decoded)
17131}
17132
17133async fn decode_storage_cursor_current_index_record_strict(
17134    cursor_id: i32,
17135    cursor: &mut StorageCursor,
17136    malformed_detail: &'static str,
17137) -> Result<()> {
17138    try_decode_storage_cursor_current_index_record(cursor_id, cursor)
17139        .await?
17140        .then_some(())
17141        .ok_or_else(|| FrankenError::internal(malformed_detail))
17142}
17143
17144async fn storage_cursor_no_conflict_prefix_match(
17145    cursor_id: i32,
17146    cursor: &mut StorageCursor,
17147    key_bytes: &[u8],
17148) -> Result<bool> {
17149    if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17150        return Ok(false);
17151    }
17152    cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
17153    if cursor.cursor.eof() {
17154        return Ok(false);
17155    }
17156    if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17157        return Ok(false);
17158    }
17159    let prefix_len = cursor.target_vals_buf.len();
17160    Ok(cursor.cur_vals_buf.len() >= prefix_len
17161        && cursor.cur_vals_buf[..prefix_len] == cursor.target_vals_buf[..])
17162}
17163
17164async fn storage_cursor_no_conflict_prefix_match_collated(
17165    cursor_id: i32,
17166    cursor: &mut StorageCursor,
17167    key_bytes: &[u8],
17168    desc_flags: &[bool],
17169    collations: &[Option<String>],
17170    collation_registry: &CollationRegistry,
17171) -> Result<bool> {
17172    if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17173        return Ok(false);
17174    }
17175    let prefix_len = cursor.target_vals_buf.len();
17176    if !cursor.cursor.first(&cursor.cx).await? {
17177        return Ok(false);
17178    }
17179    if prefix_len == 0 {
17180        return Ok(true);
17181    }
17182
17183    loop {
17184        if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17185            return Ok(false);
17186        }
17187        if cursor.cur_vals_buf.len() >= prefix_len
17188            && compare_index_prefix_keys(
17189                &cursor.cur_vals_buf,
17190                &cursor.target_vals_buf,
17191                prefix_len,
17192                desc_flags,
17193                collations,
17194                collation_registry,
17195            ) == Ordering::Equal
17196        {
17197            return Ok(true);
17198        }
17199        if !cursor.cursor.next(&cursor.cx).await? {
17200            break;
17201        }
17202    }
17203
17204    Ok(false)
17205}
17206
17207async fn storage_cursor_exact_match_collated(
17208    cursor_id: i32,
17209    cursor: &mut StorageCursor,
17210    key_bytes: &[u8],
17211    desc_flags: &[bool],
17212    collations: &[Option<String>],
17213    collation_registry: &CollationRegistry,
17214) -> Result<bool> {
17215    if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17216        return Ok(false);
17217    }
17218    let key_len = cursor.target_vals_buf.len();
17219    if !cursor.cursor.first(&cursor.cx).await? {
17220        return Ok(false);
17221    }
17222
17223    loop {
17224        if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17225            return Ok(false);
17226        }
17227        if cursor.cur_vals_buf.len() == key_len
17228            && compare_index_prefix_keys(
17229                &cursor.cur_vals_buf,
17230                &cursor.target_vals_buf,
17231                key_len,
17232                desc_flags,
17233                collations,
17234                collation_registry,
17235            ) == Ordering::Equal
17236        {
17237            return Ok(true);
17238        }
17239        if !cursor.cursor.next(&cursor.cx).await? {
17240            break;
17241        }
17242    }
17243
17244    Ok(false)
17245}
17246
17247async fn storage_cursor_current_first_index_key_equals(
17248    cursor: &mut StorageCursor,
17249    probe_value: &SqliteValue,
17250    collect_vdbe_metrics: bool,
17251    malformed_detail: &'static str,
17252) -> Result<bool> {
17253    refresh_storage_cursor_first_key_state(cursor, collect_vdbe_metrics);
17254    storage_cursor_current_first_index_key_equals_at_current_row(
17255        cursor,
17256        probe_value,
17257        collect_vdbe_metrics,
17258        malformed_detail,
17259    )
17260    .await
17261}
17262
17263async fn storage_cursor_current_first_index_key_equals_at_current_row(
17264    cursor: &mut StorageCursor,
17265    probe_value: &SqliteValue,
17266    collect_vdbe_metrics: bool,
17267    malformed_detail: &'static str,
17268) -> Result<bool> {
17269    if cursor.row_decode.cached_value_ready(0) {
17270        note_decode_cache_hit(collect_vdbe_metrics);
17271        let cached = cursor
17272            .row_decode
17273            .cached_value(0)
17274            .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17275        if collect_vdbe_metrics {
17276            record_decoded_value_metrics(cached);
17277        }
17278        return Ok(storage_cursor_first_index_key_equals_probe(
17279            cursor,
17280            cached,
17281            probe_value,
17282        ));
17283    }
17284
17285    note_decode_cache_miss(collect_vdbe_metrics);
17286    if let SqliteValue::Integer(probe_int) = probe_value {
17287        match storage_cursor_probe_first_index_key_integer(cursor, *probe_int, malformed_detail)
17288            .await?
17289        {
17290            FirstIndexKeyIntegerProbe::Match => return Ok(true),
17291            FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17292                cursor
17293                    .row_decode
17294                    .cache_decoded(0, SqliteValue::Integer(current_value));
17295                return Ok(false);
17296            }
17297            FirstIndexKeyIntegerProbe::NeedsGenericCompare(fallback_col) => {
17298                return storage_cursor_current_first_index_key_equals_generic_fallback(
17299                    cursor,
17300                    probe_value,
17301                    collect_vdbe_metrics,
17302                    malformed_detail,
17303                    fallback_col,
17304                );
17305            }
17306        }
17307    }
17308
17309    let col = storage_cursor_first_index_key_column_offset(cursor, malformed_detail).await?;
17310    storage_cursor_current_first_index_key_equals_generic_fallback(
17311        cursor,
17312        probe_value,
17313        collect_vdbe_metrics,
17314        malformed_detail,
17315        col,
17316    )
17317}
17318
17319async fn storage_cursor_current_first_index_key_compare(
17320    cursor: &mut StorageCursor,
17321    probe_value: &SqliteValue,
17322    collect_vdbe_metrics: bool,
17323    malformed_detail: &'static str,
17324    desc: bool,
17325    collation: Option<&str>,
17326    collation_registry: &CollationRegistry,
17327) -> Result<Ordering> {
17328    refresh_storage_cursor_first_key_state(cursor, collect_vdbe_metrics);
17329    storage_cursor_current_first_index_key_compare_at_current_row(
17330        cursor,
17331        probe_value,
17332        collect_vdbe_metrics,
17333        malformed_detail,
17334        desc,
17335        collation,
17336        collation_registry,
17337    )
17338    .await
17339}
17340
17341async fn storage_cursor_current_first_index_key_compare_at_current_row(
17342    cursor: &mut StorageCursor,
17343    probe_value: &SqliteValue,
17344    collect_vdbe_metrics: bool,
17345    malformed_detail: &'static str,
17346    desc: bool,
17347    collation: Option<&str>,
17348    collation_registry: &CollationRegistry,
17349) -> Result<Ordering> {
17350    let compare_ctx = FirstIndexKeyCompareContext {
17351        desc,
17352        collation,
17353        collation_registry,
17354    };
17355    if cursor.row_decode.cached_value_ready(0) {
17356        note_decode_cache_hit(collect_vdbe_metrics);
17357        let cached = cursor
17358            .row_decode
17359            .cached_value(0)
17360            .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17361        if collect_vdbe_metrics {
17362            record_decoded_value_metrics(cached);
17363        }
17364        return Ok(compare_first_index_key_values(
17365            cached,
17366            probe_value,
17367            compare_ctx,
17368        ));
17369    }
17370
17371    note_decode_cache_miss(collect_vdbe_metrics);
17372    if let SqliteValue::Integer(probe_int) = probe_value {
17373        match storage_cursor_probe_first_index_key_integer(cursor, *probe_int, malformed_detail)
17374            .await?
17375        {
17376            FirstIndexKeyIntegerProbe::Match => return Ok(Ordering::Equal),
17377            FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17378                let current_value = SqliteValue::Integer(current_value);
17379                let cmp = compare_first_index_key_values(&current_value, probe_value, compare_ctx);
17380                cursor.row_decode.cache_decoded(0, current_value);
17381                return Ok(cmp);
17382            }
17383            FirstIndexKeyIntegerProbe::NeedsGenericCompare(fallback_col) => {
17384                return storage_cursor_current_first_index_key_compare_generic_fallback(
17385                    cursor,
17386                    probe_value,
17387                    collect_vdbe_metrics,
17388                    malformed_detail,
17389                    fallback_col,
17390                    compare_ctx,
17391                );
17392            }
17393        }
17394    }
17395
17396    let col = storage_cursor_first_index_key_column_offset(cursor, malformed_detail).await?;
17397    storage_cursor_current_first_index_key_compare_generic_fallback(
17398        cursor,
17399        probe_value,
17400        collect_vdbe_metrics,
17401        malformed_detail,
17402        col,
17403        compare_ctx,
17404    )
17405}
17406
17407fn refresh_storage_cursor_first_key_state(cursor: &mut StorageCursor, collect_vdbe_metrics: bool) {
17408    let position_stamp = cursor.cursor.position_stamp();
17409    if cursor.last_position_stamp == position_stamp {
17410        return;
17411    }
17412
17413    if let Some(previous_stamp) = cursor.last_position_stamp
17414        && let Some(current_stamp) = position_stamp
17415    {
17416        note_decode_cache_invalidation(
17417            collect_vdbe_metrics,
17418            decode_cache_invalidation_reason_from_stamps(previous_stamp, current_stamp),
17419        );
17420    } else if cursor.last_position_stamp.is_some() {
17421        note_decode_cache_invalidation(
17422            collect_vdbe_metrics,
17423            DecodeCacheInvalidationReason::PositionChange,
17424        );
17425    }
17426    cursor.row_decode.invalidate();
17427    cursor.payload_buf.clear();
17428    cursor.last_position_stamp = position_stamp;
17429    cursor.cached_rowid = None;
17430    cursor.payload_includes_rowid_alias = None;
17431}
17432
17433async fn storage_cursor_cached_rowid(cursor: &mut StorageCursor) -> Result<i64> {
17434    refresh_storage_cursor_first_key_state(cursor, false);
17435    if let Some(rowid) = cursor.cached_rowid {
17436        return Ok(rowid);
17437    }
17438    let rowid = cursor.cursor.rowid(&cursor.cx).await?;
17439    cursor.cached_rowid = Some(rowid);
17440    Ok(rowid)
17441}
17442
17443async fn storage_cursor_first_index_key_column_offset(
17444    cursor: &mut StorageCursor,
17445    malformed_detail: &'static str,
17446) -> Result<ColumnOffset> {
17447    const INITIAL_PREFIX_BYTES: usize = 16;
17448
17449    let mut requested_bytes = INITIAL_PREFIX_BYTES;
17450    loop {
17451        ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
17452        let loaded = cursor.payload_buf.len();
17453
17454        let (header_size_u64, hdr_varint_len) =
17455            read_varint(&cursor.payload_buf).ok_or_else(|| FrankenError::DatabaseCorrupt {
17456                detail: malformed_detail.to_owned(),
17457            })?;
17458        let header_size =
17459            usize::try_from(header_size_u64).map_err(|_| FrankenError::DatabaseCorrupt {
17460                detail: malformed_detail.to_owned(),
17461            })?;
17462        if header_size < hdr_varint_len {
17463            return Err(FrankenError::DatabaseCorrupt {
17464                detail: malformed_detail.to_owned(),
17465            });
17466        }
17467        if header_size > loaded {
17468            if loaded < requested_bytes {
17469                return Err(FrankenError::DatabaseCorrupt {
17470                    detail: malformed_detail.to_owned(),
17471                });
17472            }
17473            requested_bytes = header_size.max(loaded.saturating_mul(2));
17474            continue;
17475        }
17476
17477        let (serial_type, _) = read_varint(&cursor.payload_buf[hdr_varint_len..header_size])
17478            .ok_or_else(|| FrankenError::DatabaseCorrupt {
17479                detail: malformed_detail.to_owned(),
17480            })?;
17481        let value_len = usize::try_from(serial_type_len(serial_type).ok_or_else(|| {
17482            FrankenError::DatabaseCorrupt {
17483                detail: malformed_detail.to_owned(),
17484            }
17485        })?)
17486        .map_err(|_| FrankenError::DatabaseCorrupt {
17487            detail: malformed_detail.to_owned(),
17488        })?;
17489        let body_offset = header_size;
17490        let col_end =
17491            body_offset
17492                .checked_add(value_len)
17493                .ok_or_else(|| FrankenError::DatabaseCorrupt {
17494                    detail: malformed_detail.to_owned(),
17495                })?;
17496        if col_end > loaded {
17497            if loaded < requested_bytes {
17498                return Err(FrankenError::DatabaseCorrupt {
17499                    detail: malformed_detail.to_owned(),
17500                });
17501            }
17502            requested_bytes = col_end.max(loaded.saturating_mul(2));
17503            continue;
17504        }
17505
17506        return Ok(ColumnOffset {
17507            serial_type,
17508            body_offset: u32::try_from(body_offset).map_err(|_| FrankenError::DatabaseCorrupt {
17509                detail: malformed_detail.to_owned(),
17510            })?,
17511            value_len: u32::try_from(value_len).map_err(|_| FrankenError::DatabaseCorrupt {
17512                detail: malformed_detail.to_owned(),
17513            })?,
17514        });
17515    }
17516}
17517
17518fn storage_cursor_current_first_index_key_equals_generic_fallback(
17519    cursor: &mut StorageCursor,
17520    probe_value: &SqliteValue,
17521    collect_vdbe_metrics: bool,
17522    malformed_detail: &'static str,
17523    col: ColumnOffset,
17524) -> Result<bool> {
17525    let hint = cursor.row_decode.cached_value(0);
17526    let value = fsqlite_types::record::decode_column_from_offset_reuse(
17527        &cursor.payload_buf,
17528        &col,
17529        hint,
17530        collect_vdbe_metrics,
17531    )
17532    .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17533    if collect_vdbe_metrics {
17534        record_decoded_value_metrics(&value);
17535    }
17536    let matches = storage_cursor_first_index_key_equals_probe(cursor, &value, probe_value);
17537    cursor.row_decode.cache_decoded(0, value);
17538    Ok(matches)
17539}
17540
17541fn storage_cursor_current_first_index_key_compare_generic_fallback(
17542    cursor: &mut StorageCursor,
17543    probe_value: &SqliteValue,
17544    collect_vdbe_metrics: bool,
17545    malformed_detail: &'static str,
17546    col: ColumnOffset,
17547    compare_ctx: FirstIndexKeyCompareContext<'_>,
17548) -> Result<Ordering> {
17549    let hint = cursor.row_decode.cached_value(0);
17550    let value = fsqlite_types::record::decode_column_from_offset_reuse(
17551        &cursor.payload_buf,
17552        &col,
17553        hint,
17554        collect_vdbe_metrics,
17555    )
17556    .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17557    if collect_vdbe_metrics {
17558        record_decoded_value_metrics(&value);
17559    }
17560    let cmp = compare_first_index_key_values(&value, probe_value, compare_ctx);
17561    cursor.row_decode.cache_decoded(0, value);
17562    Ok(cmp)
17563}
17564
17565fn storage_cursor_first_index_key_collation(cursor: &StorageCursor) -> Option<&str> {
17566    cursor
17567        .cursor
17568        .index_collations()
17569        .first()
17570        .and_then(|collation| collation.as_deref())
17571        .filter(|name| !name.eq_ignore_ascii_case("BINARY"))
17572}
17573
17574fn storage_cursor_first_index_key_equals_probe(
17575    cursor: &StorageCursor,
17576    current_value: &SqliteValue,
17577    probe_value: &SqliteValue,
17578) -> bool {
17579    let Some(collation) = storage_cursor_first_index_key_collation(cursor) else {
17580        return values_equal_without_collation(current_value, probe_value);
17581    };
17582
17583    if let (SqliteValue::Text(left), SqliteValue::Text(right)) = (current_value, probe_value)
17584        && let Some(ordering) = builtin_collation_compare_text(left, right, collation)
17585    {
17586        return ordering == Ordering::Equal;
17587    }
17588
17589    let registry = cursor.cursor.collation_registry();
17590    let guard = registry.lock().unwrap_or_else(|err| err.into_inner());
17591    cmp_values_collated(current_value, probe_value, Some(collation), &guard) == Ordering::Equal
17592}
17593
17594#[derive(Clone, Copy)]
17595struct FirstIndexKeyCompareContext<'a> {
17596    desc: bool,
17597    collation: Option<&'a str>,
17598    collation_registry: &'a CollationRegistry,
17599}
17600
17601fn compare_first_index_key_values(
17602    current_value: &SqliteValue,
17603    probe_value: &SqliteValue,
17604    compare_ctx: FirstIndexKeyCompareContext<'_>,
17605) -> Ordering {
17606    let mut cmp = cmp_values_collated(
17607        current_value,
17608        probe_value,
17609        compare_ctx.collation,
17610        compare_ctx.collation_registry,
17611    );
17612    if compare_ctx.desc {
17613        cmp = cmp.reverse();
17614    }
17615    cmp
17616}
17617
17618enum FirstIndexKeyIntegerProbe {
17619    Match,
17620    Mismatch(i64),
17621    NeedsGenericCompare(ColumnOffset),
17622}
17623
17624async fn storage_cursor_probe_first_index_key_integer(
17625    cursor: &mut StorageCursor,
17626    probe_value: i64,
17627    malformed_detail: &'static str,
17628) -> Result<FirstIndexKeyIntegerProbe> {
17629    use fsqlite_types::serial_type::{SerialTypeClass, classify_serial_type};
17630
17631    if let Some((matches, current_value)) = cursor
17632        .cursor
17633        .try_probe_current_first_index_key_integer_local(probe_value)?
17634    {
17635        return Ok(if matches {
17636            FirstIndexKeyIntegerProbe::Match
17637        } else {
17638            FirstIndexKeyIntegerProbe::Mismatch(current_value)
17639        });
17640    }
17641
17642    const INITIAL_PREFIX_BYTES: usize = 16;
17643
17644    let mut requested_bytes = INITIAL_PREFIX_BYTES;
17645    loop {
17646        ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
17647        let loaded = cursor.payload_buf.len();
17648
17649        let (header_size_u64, hdr_varint_len) =
17650            read_varint(&cursor.payload_buf).ok_or_else(|| FrankenError::DatabaseCorrupt {
17651                detail: malformed_detail.to_owned(),
17652            })?;
17653        let header_size =
17654            usize::try_from(header_size_u64).map_err(|_| FrankenError::DatabaseCorrupt {
17655                detail: malformed_detail.to_owned(),
17656            })?;
17657        if header_size < hdr_varint_len {
17658            return Err(FrankenError::DatabaseCorrupt {
17659                detail: malformed_detail.to_owned(),
17660            });
17661        }
17662        if header_size > loaded {
17663            if loaded < requested_bytes {
17664                return Err(FrankenError::DatabaseCorrupt {
17665                    detail: malformed_detail.to_owned(),
17666                });
17667            }
17668            requested_bytes = header_size.max(loaded.saturating_mul(2));
17669            continue;
17670        }
17671
17672        let (serial_type, _) = read_varint(&cursor.payload_buf[hdr_varint_len..header_size])
17673            .ok_or_else(|| FrankenError::DatabaseCorrupt {
17674                detail: malformed_detail.to_owned(),
17675            })?;
17676        let value_len = usize::try_from(serial_type_len(serial_type).ok_or_else(|| {
17677            FrankenError::DatabaseCorrupt {
17678                detail: malformed_detail.to_owned(),
17679            }
17680        })?)
17681        .map_err(|_| FrankenError::DatabaseCorrupt {
17682            detail: malformed_detail.to_owned(),
17683        })?;
17684        let body_offset = header_size;
17685        let col_end =
17686            body_offset
17687                .checked_add(value_len)
17688                .ok_or_else(|| FrankenError::DatabaseCorrupt {
17689                    detail: malformed_detail.to_owned(),
17690                })?;
17691        if col_end > loaded {
17692            if loaded < requested_bytes {
17693                return Err(FrankenError::DatabaseCorrupt {
17694                    detail: malformed_detail.to_owned(),
17695                });
17696            }
17697            requested_bytes = col_end.max(loaded.saturating_mul(2));
17698            continue;
17699        }
17700
17701        let mismatch = |current_value| FirstIndexKeyIntegerProbe::Mismatch(current_value);
17702        return Ok(match classify_serial_type(serial_type) {
17703            SerialTypeClass::Zero => {
17704                if probe_value == 0 {
17705                    FirstIndexKeyIntegerProbe::Match
17706                } else {
17707                    mismatch(0)
17708                }
17709            }
17710            SerialTypeClass::One => {
17711                if probe_value == 1 {
17712                    FirstIndexKeyIntegerProbe::Match
17713                } else {
17714                    mismatch(1)
17715                }
17716            }
17717            SerialTypeClass::Integer => {
17718                let bytes = cursor
17719                    .payload_buf
17720                    .get(body_offset..col_end)
17721                    .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17722                let current_value = decode_big_endian_signed_fast(bytes);
17723                if current_value == probe_value {
17724                    FirstIndexKeyIntegerProbe::Match
17725                } else {
17726                    mismatch(current_value)
17727                }
17728            }
17729            _ => FirstIndexKeyIntegerProbe::NeedsGenericCompare(ColumnOffset {
17730                serial_type,
17731                body_offset: u32::try_from(body_offset).map_err(|_| {
17732                    FrankenError::DatabaseCorrupt {
17733                        detail: malformed_detail.to_owned(),
17734                    }
17735                })?,
17736                value_len: u32::try_from(value_len).map_err(|_| FrankenError::DatabaseCorrupt {
17737                    detail: malformed_detail.to_owned(),
17738                })?,
17739            }),
17740        });
17741    }
17742}
17743
17744#[allow(clippy::cast_possible_wrap)]
17745fn decode_big_endian_signed_fast(bytes: &[u8]) -> i64 {
17746    match bytes.len() {
17747        0 => 0,
17748        1 => bytes[0] as i8 as i64,
17749        2 => {
17750            let mut buf = [0_u8; 2];
17751            buf.copy_from_slice(bytes);
17752            i16::from_be_bytes(buf) as i64
17753        }
17754        3 => {
17755            let mut buf = [if bytes[0] & 0x80 != 0 { 0xFF } else { 0 }; 4];
17756            buf[1..4].copy_from_slice(bytes);
17757            i32::from_be_bytes(buf) as i64
17758        }
17759        4 => {
17760            let mut buf = [0_u8; 4];
17761            buf.copy_from_slice(bytes);
17762            i32::from_be_bytes(buf) as i64
17763        }
17764        6 => {
17765            let mut buf = [if bytes[0] & 0x80 != 0 { 0xFF } else { 0 }; 8];
17766            buf[2..8].copy_from_slice(bytes);
17767            i64::from_be_bytes(buf)
17768        }
17769        8 => {
17770            let mut buf = [0_u8; 8];
17771            buf.copy_from_slice(bytes);
17772            i64::from_be_bytes(buf)
17773        }
17774        _ => {
17775            let negative = bytes.first().is_some_and(|&b| b & 0x80 != 0);
17776            let mut value: u64 = if negative { u64::MAX } else { 0 };
17777            for &b in bytes {
17778                value = (value << 8) | u64::from(b);
17779            }
17780            value as i64
17781        }
17782    }
17783}
17784
17785async fn storage_cursor_count_equal_first_key_run(
17786    cursor: &mut StorageCursor,
17787    probe_value: &SqliteValue,
17788    collect_vdbe_metrics: bool,
17789) -> Result<i64> {
17790    if let SqliteValue::Integer(probe_int) = probe_value {
17791        return storage_cursor_count_equal_first_key_run_integer_probe(
17792            cursor,
17793            *probe_int,
17794            collect_vdbe_metrics,
17795        )
17796        .await;
17797    }
17798
17799    let mut matched = 0_i64;
17800    while !cursor.cursor.eof() {
17801        if !storage_cursor_current_first_index_key_equals(
17802            cursor,
17803            probe_value,
17804            collect_vdbe_metrics,
17805            "CountIndexEqRun: malformed index entry record",
17806        )
17807        .await?
17808        {
17809            break;
17810        }
17811        matched = matched.wrapping_add(1);
17812        if !cursor.cursor.next(&cursor.cx).await? {
17813            break;
17814        }
17815    }
17816    Ok(matched)
17817}
17818
17819async fn storage_cursor_count_equal_first_key_run_integer_probe(
17820    cursor: &mut StorageCursor,
17821    probe_int: i64,
17822    collect_vdbe_metrics: bool,
17823) -> Result<i64> {
17824    let mut matched = 0_i64;
17825    let probe_value = SqliteValue::Integer(probe_int);
17826    let mut payload_position_stamp = None;
17827    while !cursor.cursor.eof() {
17828        match cursor
17829            .cursor
17830            .count_equal_first_index_key_run_integer_local_segment(&cursor.cx, probe_int)
17831            .await?
17832        {
17833            FirstIndexKeyIntegerLocalRunSegment::Matched(local_matched) => {
17834                matched = matched.wrapping_add(local_matched);
17835                continue;
17836            }
17837            FirstIndexKeyIntegerLocalRunSegment::Mismatch {
17838                matched: local_matched,
17839                current_value,
17840            } => {
17841                matched = matched.wrapping_add(local_matched);
17842                cursor.row_decode.invalidate();
17843                cursor
17844                    .row_decode
17845                    .cache_decoded(0, SqliteValue::Integer(current_value));
17846                break;
17847            }
17848            FirstIndexKeyIntegerLocalRunSegment::NeedsFallback {
17849                matched: local_matched,
17850            } => {
17851                matched = matched.wrapping_add(local_matched);
17852            }
17853        }
17854
17855        if cursor.cursor.eof() {
17856            break;
17857        }
17858
17859        let position_stamp = cursor.cursor.position_stamp();
17860        if payload_position_stamp != position_stamp {
17861            cursor.payload_buf.clear();
17862            payload_position_stamp = position_stamp;
17863        }
17864        match storage_cursor_probe_first_index_key_integer(
17865            cursor,
17866            probe_int,
17867            "CountIndexEqRun: malformed index entry record",
17868        )
17869        .await?
17870        {
17871            FirstIndexKeyIntegerProbe::Match => {}
17872            FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17873                cursor.row_decode.invalidate();
17874                cursor
17875                    .row_decode
17876                    .cache_decoded(0, SqliteValue::Integer(current_value));
17877                break;
17878            }
17879            FirstIndexKeyIntegerProbe::NeedsGenericCompare(col) => {
17880                cursor.row_decode.invalidate();
17881                if !storage_cursor_current_first_index_key_equals_generic_fallback(
17882                    cursor,
17883                    &probe_value,
17884                    collect_vdbe_metrics,
17885                    "CountIndexEqRun: malformed index entry record",
17886                    col,
17887                )? {
17888                    break;
17889                }
17890            }
17891        }
17892
17893        matched = matched.wrapping_add(1);
17894        if !cursor.cursor.next(&cursor.cx).await? {
17895            break;
17896        }
17897    }
17898    Ok(matched)
17899}
17900
17901async fn find_conflicting_rowid_in_index(
17902    sc: &mut StorageCursor,
17903    key_bytes: &[u8],
17904    n_idx_cols: usize,
17905) -> Result<Option<i64>> {
17906    // Re-seek to the position where the conflicting entry should be.
17907    sc.cursor.index_move_to(&sc.cx, key_bytes).await?;
17908
17909    sc.cur_vals_buf.clear();
17910
17911    // The new key we're trying to insert — parse its prefix for comparison.
17912    decode_storage_cursor_target_index_record_strict(
17913        sc,
17914        key_bytes,
17915        "find_conflicting_rowid: malformed new index key",
17916    )?;
17917
17918    // Check the entry at current position and previous entry for a prefix match.
17919    for attempt in 0..2 {
17920        if sc.cursor.eof() {
17921            if attempt == 0 {
17922                // Try moving to the previous entry.
17923                sc.cursor.prev(&sc.cx).await?;
17924                continue;
17925            }
17926            break;
17927        }
17928
17929        decode_storage_cursor_current_index_record_strict(
17930            -1,
17931            sc,
17932            "find_conflicting_rowid: malformed index entry record",
17933        )
17934        .await?;
17935
17936        // Check if the indexed columns (excluding the trailing rowid) match
17937        // and none of them are NULL.
17938        let mut prefix_match = true;
17939        let mut has_null = false;
17940        for i in 0..n_idx_cols {
17941            let new_val = sc.target_vals_buf.get(i);
17942            let entry_val = sc.cur_vals_buf.get(i);
17943            if matches!(new_val, Some(SqliteValue::Null) | None)
17944                || matches!(entry_val, Some(SqliteValue::Null) | None)
17945            {
17946                has_null = true;
17947                break;
17948            }
17949            if new_val != entry_val {
17950                prefix_match = false;
17951                break;
17952            }
17953        }
17954
17955        if prefix_match && !has_null {
17956            let old_rowid = index_entry_rowid_at(
17957                &sc.cur_vals_buf,
17958                n_idx_cols,
17959                "find_conflicting_rowid: index entry must end with exactly one integer rowid suffix",
17960            )?;
17961
17962            // Delete the conflicting index entry.
17963            sc.cursor.delete(&sc.cx).await?;
17964            invalidate_storage_cursor_row_cache(sc);
17965
17966            return Ok(Some(old_rowid));
17967        }
17968
17969        if attempt == 0 {
17970            sc.cursor.prev(&sc.cx).await?;
17971        }
17972    }
17973
17974    Ok(None)
17975}
17976
17977async fn find_conflicting_rowid_in_index_collated(
17978    sc: &mut StorageCursor,
17979    key_bytes: &[u8],
17980    n_idx_cols: usize,
17981    desc_flags: &[bool],
17982    collations: &[Option<String>],
17983    collation_registry: &CollationRegistry,
17984) -> Result<Option<i64>> {
17985    let target_values = parse_record(key_bytes).ok_or_else(|| {
17986        FrankenError::internal("find_conflicting_rowid_in_index_collated: malformed new index key")
17987    })?;
17988    if target_values.len() < n_idx_cols
17989        || target_values
17990            .iter()
17991            .take(n_idx_cols)
17992            .any(SqliteValue::is_null)
17993    {
17994        return Ok(None);
17995    }
17996
17997    if !sc.cursor.first(&sc.cx).await? {
17998        return Ok(None);
17999    }
18000
18001    loop {
18002        let existing_key = sc.cursor.payload(&sc.cx).await?;
18003        let existing_values = parse_record(&existing_key).ok_or_else(|| {
18004            FrankenError::internal(
18005                "find_conflicting_rowid_in_index_collated: malformed index entry record",
18006            )
18007        })?;
18008        if existing_values.len() >= n_idx_cols
18009            && existing_values
18010                .iter()
18011                .take(n_idx_cols)
18012                .all(|value| !value.is_null())
18013            && compare_index_prefix_keys(
18014                &existing_values,
18015                &target_values,
18016                n_idx_cols,
18017                desc_flags,
18018                collations,
18019                collation_registry,
18020            ) == Ordering::Equal
18021        {
18022            let rowid = index_entry_rowid_at(
18023                &existing_values,
18024                n_idx_cols,
18025                "find_conflicting_rowid_in_index_collated: index entry must end with exactly one integer rowid suffix",
18026            )?;
18027            return Ok(Some(rowid));
18028        }
18029
18030        if !sc.cursor.next(&sc.cx).await? {
18031            break;
18032        }
18033    }
18034
18035    Ok(None)
18036}
18037
18038fn index_entry_rowid_at(
18039    values: &[SqliteValue],
18040    n_idx_cols: usize,
18041    corrupt_detail: &str,
18042) -> Result<i64> {
18043    let expected_len = n_idx_cols
18044        .checked_add(1)
18045        .ok_or_else(|| FrankenError::DatabaseCorrupt {
18046            detail: corrupt_detail.to_owned(),
18047        })?;
18048    if values.len() != expected_len {
18049        return Err(FrankenError::DatabaseCorrupt {
18050            detail: corrupt_detail.to_owned(),
18051        });
18052    }
18053    values[n_idx_cols]
18054        .as_integer()
18055        .ok_or_else(|| FrankenError::DatabaseCorrupt {
18056            detail: corrupt_detail.to_owned(),
18057        })
18058}
18059
18060fn encode_record(values: &[SqliteValue]) -> Vec<u8> {
18061    serialize_record(values)
18062}
18063
18064#[allow(dead_code)]
18065fn payload_includes_rowid_alias(
18066    payload_values: &[SqliteValue],
18067    _rowid: i64,
18068    ipk_col_idx: usize,
18069    table_column_count: Option<usize>,
18070    first_not_null_non_ipk_col_idx: Option<usize>,
18071) -> bool {
18072    let payload_cols = payload_values.len();
18073    if let Some(table_cols) = table_column_count {
18074        if payload_cols == table_cols {
18075            return true;
18076        }
18077        let one_column_short = payload_cols.checked_add(1) == Some(table_cols);
18078        if one_column_short && matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null)) {
18079            return true;
18080        }
18081        if let Some(not_null_col_idx) =
18082            first_not_null_non_ipk_col_idx.filter(|idx| *idx > ipk_col_idx)
18083        {
18084            let omitted_payload_idx = not_null_col_idx - 1;
18085            let present_payload_idx = not_null_col_idx;
18086            if omitted_payload_idx < payload_cols
18087                && present_payload_idx < payload_cols
18088                && matches!(
18089                    payload_values.get(omitted_payload_idx),
18090                    Some(SqliteValue::Null)
18091                )
18092            {
18093                return true;
18094            }
18095        }
18096        // Rows can be more than one column short after repeated ALTER TABLE
18097        // ADD COLUMN operations while still carrying SQLite's NULL IPK slot.
18098        if matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null)) {
18099            return true;
18100        }
18101        return false;
18102    }
18103    if payload_cols <= ipk_col_idx {
18104        return false;
18105    }
18106
18107    matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null))
18108}
18109
18110#[allow(dead_code)]
18111fn payload_includes_rowid_alias_lazy(
18112    row_decode: &RowDecodeScratch,
18113    _record: &[u8],
18114    _rowid: i64,
18115    ipk_col_idx: usize,
18116    table_column_count: Option<usize>,
18117    first_not_null_non_ipk_col_idx: Option<usize>,
18118) -> bool {
18119    payload_includes_rowid_alias_without_rowid(
18120        row_decode,
18121        ipk_col_idx,
18122        table_column_count,
18123        first_not_null_non_ipk_col_idx,
18124    )
18125}
18126
18127/// Resolve the rowid-alias payload shape from record-header metadata alone.
18128///
18129/// This stays conservative for non-NULL IPK-position values: a shifted user
18130/// column can coincidentally equal the physical rowid, so equality is not
18131/// evidence that the IPK alias slot is present.
18132fn payload_includes_rowid_alias_without_rowid(
18133    row_decode: &RowDecodeScratch,
18134    ipk_col_idx: usize,
18135    table_column_count: Option<usize>,
18136    first_not_null_non_ipk_col_idx: Option<usize>,
18137) -> bool {
18138    use fsqlite_types::serial_type::{SerialTypeClass, classify_serial_type};
18139
18140    let payload_cols = row_decode.column_count();
18141    if let Some(table_cols) = table_column_count {
18142        if payload_cols == table_cols {
18143            return true;
18144        }
18145        let one_column_short = payload_cols.checked_add(1) == Some(table_cols);
18146        if let Some(not_null_col_idx) =
18147            first_not_null_non_ipk_col_idx.filter(|idx| *idx > ipk_col_idx)
18148        {
18149            let omitted_payload_idx = not_null_col_idx - 1;
18150            let present_payload_idx = not_null_col_idx;
18151            if omitted_payload_idx < payload_cols
18152                && present_payload_idx < payload_cols
18153                && let Some(col) = row_decode.column_offset(omitted_payload_idx)
18154                && matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null)
18155            {
18156                return true;
18157            }
18158        }
18159        if one_column_short {
18160            let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18161                return false;
18162            };
18163            return matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null);
18164        }
18165        if ipk_col_idx >= payload_cols {
18166            return false;
18167        }
18168        let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18169            return false;
18170        };
18171        return matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null);
18172    }
18173    if ipk_col_idx >= payload_cols {
18174        return false;
18175    }
18176    let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18177        return false;
18178    };
18179    matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null)
18180}
18181
18182fn column_payload_end(col: &ColumnOffset) -> Option<usize> {
18183    let start = usize::try_from(col.body_offset).ok()?;
18184    let len = usize::try_from(col.value_len).ok()?;
18185    start.checked_add(len)
18186}
18187
18188fn row_decode_payload_end(row_decode: &RowDecodeScratch) -> Option<usize> {
18189    let mut end = 0_usize;
18190    for idx in 0..row_decode.column_count() {
18191        end = end.max(column_payload_end(row_decode.column_offset(idx)?)?);
18192    }
18193    Some(end)
18194}
18195
18196#[derive(Debug, Clone, Copy, Default)]
18197struct DecodeCacheRefreshState {
18198    refreshed: bool,
18199    eager_values_ready: bool,
18200}
18201
18202fn decode_cache_invalidation_reason_from_stamps(
18203    previous: CursorPositionStamp,
18204    current: CursorPositionStamp,
18205) -> DecodeCacheInvalidationReason {
18206    if previous.same_logical_position(current) {
18207        DecodeCacheInvalidationReason::WriteMutation
18208    } else {
18209        DecodeCacheInvalidationReason::PositionChange
18210    }
18211}
18212
18213async fn ensure_storage_cursor_payload_prefix(
18214    cursor: &mut StorageCursor,
18215    min_payload_bytes: usize,
18216) -> Result<()> {
18217    if cursor.payload_buf.len() >= min_payload_bytes {
18218        return Ok(());
18219    }
18220    cursor
18221        .cursor
18222        .payload_prefix_into(&cursor.cx, min_payload_bytes, &mut cursor.payload_buf)
18223        .await
18224}
18225
18226async fn ensure_storage_cursor_row_layout(
18227    cursor: &mut StorageCursor,
18228    min_payload_bytes: usize,
18229    collect_vdbe_metrics: bool,
18230) -> Result<DecodeCacheRefreshState> {
18231    let position_stamp = cursor.cursor.position_stamp();
18232    let mut refreshed = false;
18233    if cursor.last_position_stamp != position_stamp {
18234        if let Some(previous_stamp) = cursor.last_position_stamp
18235            && let Some(current_stamp) = position_stamp
18236        {
18237            note_decode_cache_invalidation(
18238                collect_vdbe_metrics,
18239                decode_cache_invalidation_reason_from_stamps(previous_stamp, current_stamp),
18240            );
18241        } else if cursor.last_position_stamp.is_some() {
18242            note_decode_cache_invalidation(
18243                collect_vdbe_metrics,
18244                DecodeCacheInvalidationReason::PositionChange,
18245            );
18246        }
18247        cursor.row_decode.invalidate();
18248        cursor.payload_buf.clear();
18249        cursor.last_position_stamp = position_stamp;
18250        cursor.cached_rowid = None;
18251        cursor.payload_includes_rowid_alias = None;
18252        refreshed = true;
18253    }
18254
18255    if !refreshed && !cursor.row_decode.is_empty() && cursor.payload_buf.len() >= min_payload_bytes
18256    {
18257        return Ok(DecodeCacheRefreshState {
18258            refreshed: false,
18259            eager_values_ready: false,
18260        });
18261    }
18262
18263    let required_bytes = min_payload_bytes;
18264    let mut requested_bytes = min_payload_bytes.max(STORAGE_CURSOR_LAYOUT_PREFIX_BYTES);
18265
18266    loop {
18267        ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
18268
18269        if cursor.row_decode.is_empty() {
18270            if cursor
18271                .row_decode
18272                .prepare_for_record_prefix(&cursor.payload_buf)
18273                .is_none()
18274            {
18275                let loaded = cursor.payload_buf.len();
18276                if loaded < requested_bytes {
18277                    return Err(FrankenError::DatabaseCorrupt {
18278                        detail: "malformed record header or payload in cursor payload".to_owned(),
18279                    });
18280                }
18281                requested_bytes = requested_bytes
18282                    .saturating_mul(2)
18283                    .max(loaded.saturating_add(1));
18284                continue;
18285            }
18286        }
18287
18288        if cursor.payload_buf.len() < required_bytes {
18289            let loaded = cursor.payload_buf.len();
18290            if loaded < requested_bytes {
18291                return Err(FrankenError::DatabaseCorrupt {
18292                    detail: format!(
18293                        "cursor payload shorter than required prefix: need {required_bytes}, have {loaded}"
18294                    ),
18295                });
18296            }
18297            requested_bytes = required_bytes.max(loaded.saturating_mul(2));
18298            continue;
18299        }
18300
18301        let eager_values_ready = if cursor.row_decode.column_count() > 64
18302            && !cursor.row_decode.cached_value_ready(64)
18303            && row_decode_payload_end(&cursor.row_decode)
18304                .is_some_and(|record_len| cursor.payload_buf.len() >= record_len)
18305        {
18306            cursor
18307                .row_decode
18308                .prepare_for_record(&cursor.payload_buf)
18309                .ok_or_else(|| FrankenError::DatabaseCorrupt {
18310                    detail: "malformed wide record in cursor payload".to_owned(),
18311                })?
18312        } else {
18313            false
18314        };
18315
18316        return Ok(DecodeCacheRefreshState {
18317            refreshed,
18318            eager_values_ready,
18319        });
18320    }
18321}
18322
18323fn ensure_sorter_row_cache(
18324    sorter: &mut SorterCursor,
18325    collect_vdbe_metrics: bool,
18326    position: usize,
18327) -> Result<DecodeCacheRefreshState> {
18328    let (rows, cached_row_position, cached_row_decode) = (
18329        &sorter.rows,
18330        &mut sorter.cached_row_position,
18331        &mut sorter.cached_row_decode,
18332    );
18333    if *cached_row_position == Some(position) {
18334        return Ok(DecodeCacheRefreshState::default());
18335    }
18336    let row = rows
18337        .get(position)
18338        .ok_or_else(|| FrankenError::DatabaseCorrupt {
18339            detail: format!("missing sorter row at position {position}"),
18340        })?;
18341    if cached_row_position.is_some() {
18342        note_decode_cache_invalidation(
18343            collect_vdbe_metrics,
18344            DecodeCacheInvalidationReason::PositionChange,
18345        );
18346    }
18347    let eager_values_ready = cached_row_decode
18348        .prepare_for_record(&row.blob)
18349        .ok_or_else(|| FrankenError::DatabaseCorrupt {
18350            detail: "malformed sorter record header or payload".to_owned(),
18351        })?;
18352    *cached_row_position = Some(position);
18353    Ok(DecodeCacheRefreshState {
18354        refreshed: true,
18355        eager_values_ready,
18356    })
18357}
18358
18359fn invalidate_storage_cursor_row_cache_with_reason(
18360    cursor: &mut StorageCursor,
18361    collect_vdbe_metrics: bool,
18362    reason: DecodeCacheInvalidationReason,
18363) {
18364    if cursor.last_position_stamp.is_some() || !cursor.row_decode.is_empty() {
18365        note_decode_cache_invalidation(collect_vdbe_metrics, reason);
18366    }
18367    cursor.row_decode.invalidate();
18368    cursor.last_position_stamp = None;
18369    cursor.cached_rowid = None;
18370    cursor.payload_includes_rowid_alias = None;
18371}
18372
18373fn invalidate_storage_cursor_row_cache(cursor: &mut StorageCursor) {
18374    invalidate_storage_cursor_row_cache_with_reason(
18375        cursor,
18376        false,
18377        DecodeCacheInvalidationReason::WriteMutation,
18378    );
18379}
18380
18381#[allow(dead_code)]
18382fn encode_record_refs(values: &[&SqliteValue]) -> Vec<u8> {
18383    fsqlite_types::record::serialize_record_refs(values)
18384}
18385
18386/// Extract the raw bytes from a record blob value (output of `MakeRecord`).
18387fn record_blob_bytes(val: &SqliteValue) -> &[u8] {
18388    match val {
18389        SqliteValue::Blob(bytes) => bytes,
18390        _ => &[],
18391    }
18392}
18393
18394fn decode_record_with_metrics(
18395    val: &SqliteValue,
18396    collect_vdbe_metrics: bool,
18397) -> Result<Vec<SqliteValue>> {
18398    let SqliteValue::Blob(bytes) = val else {
18399        return Ok(Vec::new());
18400    };
18401    decode_record_bytes_with_metrics(bytes, collect_vdbe_metrics)
18402}
18403
18404fn decode_record_bytes_with_metrics(
18405    bytes: &[u8],
18406    collect_vdbe_metrics: bool,
18407) -> Result<Vec<SqliteValue>> {
18408    let _profile_stage = enter_vdbe_decode_profile_stage();
18409    let values = parse_record(bytes)
18410        .ok_or_else(|| FrankenError::internal("malformed SQLite record blob"))?;
18411    if collect_vdbe_metrics {
18412        FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
18413        for value in &values {
18414            record_decoded_value_metrics(value);
18415        }
18416    }
18417    Ok(values)
18418}
18419
18420#[cfg(test)]
18421fn decode_record(val: &SqliteValue) -> Result<Vec<SqliteValue>> {
18422    decode_record_with_metrics(val, vdbe_metrics_enabled())
18423}
18424
18425fn sorter_keys_equal(
18426    lhs: &[SqliteValue],
18427    rhs: &[SqliteValue],
18428    key_columns: usize,
18429    collations: &[Option<String>],
18430    collation_registry: &CollationRegistry,
18431) -> bool {
18432    compare_sorter_keys(lhs, rhs, key_columns, collations, collation_registry) == Ordering::Equal
18433}
18434
18435fn compare_sorter_keys(
18436    lhs: &[SqliteValue],
18437    rhs: &[SqliteValue],
18438    key_columns: usize,
18439    collations: &[Option<String>],
18440    collation_registry: &CollationRegistry,
18441) -> Ordering {
18442    let key_count = key_columns.max(1);
18443    for idx in 0..key_count {
18444        let Some(lhs_value) = lhs.get(idx) else {
18445            return if rhs.get(idx).is_some() {
18446                Ordering::Less
18447            } else {
18448                break;
18449            };
18450        };
18451        let Some(rhs_value) = rhs.get(idx) else {
18452            return Ordering::Greater;
18453        };
18454
18455        let coll = collations.get(idx).and_then(|c| c.as_deref());
18456        match cmp_values_collated(lhs_value, rhs_value, coll, collation_registry) {
18457            Ordering::Equal => {}
18458            non_equal => return non_equal,
18459        }
18460    }
18461    Ordering::Equal
18462}
18463
18464fn parse_non_null_index_prefix(key_bytes: &[u8], key_columns: usize) -> Option<Vec<SqliteValue>> {
18465    let fields = parse_record(key_bytes)?;
18466    if fields.len() < key_columns || fields.iter().take(key_columns).any(SqliteValue::is_null) {
18467        return None;
18468    }
18469    Some(fields.into_iter().take(key_columns).collect())
18470}
18471
18472fn compare_index_prefix_keys(
18473    lhs: &[SqliteValue],
18474    rhs: &[SqliteValue],
18475    key_columns: usize,
18476    desc_flags: &[bool],
18477    collations: &[Option<String>],
18478    collation_registry: &CollationRegistry,
18479) -> Ordering {
18480    let key_count = key_columns.max(1);
18481    for idx in 0..key_count {
18482        let Some(lhs_value) = lhs.get(idx) else {
18483            return if rhs.get(idx).is_some() {
18484                Ordering::Less
18485            } else {
18486                break;
18487            };
18488        };
18489        let Some(rhs_value) = rhs.get(idx) else {
18490            return Ordering::Greater;
18491        };
18492
18493        let coll = collations.get(idx).and_then(|c| c.as_deref());
18494        let mut ord = cmp_values_collated(lhs_value, rhs_value, coll, collation_registry);
18495        if desc_flags.get(idx).copied().unwrap_or(false) {
18496            ord = ord.reverse();
18497        }
18498        if ord != Ordering::Equal {
18499            return ord;
18500        }
18501    }
18502    Ordering::Equal
18503}
18504
18505fn idx_compare_condition_met(opcode: Opcode, cmp: Ordering) -> bool {
18506    match opcode {
18507        Opcode::IdxLE => cmp != Ordering::Greater,
18508        Opcode::IdxGT => cmp == Ordering::Greater,
18509        Opcode::IdxLT => cmp == Ordering::Less,
18510        Opcode::IdxGE => cmp != Ordering::Less,
18511        _ => unreachable!("idx_compare_condition_met called with non-Idx comparison opcode"),
18512    }
18513}
18514
18515/// Compare two `SqliteValue`s with an optional collation sequence.
18516///
18517/// Text values consult the collation registry so dynamically loaded
18518/// collations participate in ORDER BY, DISTINCT, and index probes.
18519fn cmp_values_collated(
18520    lhs: &SqliteValue,
18521    rhs: &SqliteValue,
18522    collation: Option<&str>,
18523    collation_registry: &CollationRegistry,
18524) -> Ordering {
18525    if let (Some(coll), SqliteValue::Text(lt), SqliteValue::Text(rt)) = (collation, lhs, rhs) {
18526        return compare_text_with_collation(
18527            lt.as_bytes_direct(),
18528            rt.as_bytes_direct(),
18529            coll,
18530            collation_registry,
18531        );
18532    }
18533    lhs.partial_cmp(rhs).unwrap_or(Ordering::Equal)
18534}
18535
18536fn compare_sorter_rows(
18537    lhs: &[SqliteValue],
18538    rhs: &[SqliteValue],
18539    key_columns: usize,
18540    sort_key_orders: &[SortKeyOrder],
18541    collations: &[Option<String>],
18542    collation_registry: &CollationRegistry,
18543) -> Ordering {
18544    let key_count = key_columns.max(1);
18545    for idx in 0..key_count {
18546        let order = sort_key_orders
18547            .get(idx)
18548            .copied()
18549            .unwrap_or(SortKeyOrder::Asc);
18550        let is_desc = matches!(order, SortKeyOrder::Desc | SortKeyOrder::DescNullsFirst);
18551        let nulls_last = matches!(order, SortKeyOrder::Desc | SortKeyOrder::AscNullsLast);
18552        let Some(lhs_value) = lhs.get(idx) else {
18553            return if rhs.get(idx).is_some() {
18554                if is_desc {
18555                    Ordering::Greater
18556                } else {
18557                    Ordering::Less
18558                }
18559            } else {
18560                break;
18561            };
18562        };
18563        let Some(rhs_value) = rhs.get(idx) else {
18564            return if is_desc {
18565                Ordering::Less
18566            } else {
18567                Ordering::Greater
18568            };
18569        };
18570
18571        // Handle NULLs with explicit NULLS FIRST/LAST ordering.
18572        let l_null = lhs_value.is_null();
18573        let r_null = rhs_value.is_null();
18574        if l_null || r_null {
18575            if l_null && r_null {
18576                continue;
18577            }
18578            // l_null + nulls_last → Greater (NULL at end)
18579            // l_null + !nulls_last → Less (NULL at start)
18580            // !l_null + nulls_last → Less (non-NULL before NULL)
18581            // !l_null + !nulls_last → Greater (non-NULL after NULL)
18582            return if l_null == nulls_last {
18583                Ordering::Greater
18584            } else {
18585                Ordering::Less
18586            };
18587        }
18588
18589        let coll = collations.get(idx).and_then(|c| c.as_deref());
18590        let mut ord = cmp_values_collated(lhs_value, rhs_value, coll, collation_registry);
18591        if ord == Ordering::Equal {
18592            continue;
18593        }
18594
18595        if is_desc {
18596            ord = ord.reverse();
18597        }
18598        return ord;
18599    }
18600
18601    // Rust's sort_by is stable, so equal-key rows stay in insertion order,
18602    // matching C SQLite's behavior (especially for COLLATE NOCASE DISTINCT).
18603    Ordering::Equal
18604}
18605
18606fn opcode_trace_enabled() -> bool {
18607    let env_enabled = std::env::var(VDBE_TRACE_ENV).is_ok_and(|value| {
18608        let normalized = value.trim().to_ascii_lowercase();
18609        !normalized.is_empty() && normalized != "0" && normalized != "false" && normalized != "off"
18610    });
18611    env_enabled || cfg!(test)
18612}
18613
18614// ── Arithmetic helpers ──────────────────────────────────────────────────────
18615
18616/// Mirrors C SQLite `numericType()` (SQLite VDBE:496): returns true if BOTH
18617/// operands should be treated as integers for arithmetic purposes.
18618/// Text/Blob that parse as i64 are integer-typed; Float is not.
18619fn both_integer_numeric_type(a: &SqliteValue, b: &SqliteValue) -> bool {
18620    a.is_integer_numeric_type() && b.is_integer_numeric_type()
18621}
18622
18623/// SQL division with NULL propagation and division-by-zero handling.
18624#[allow(clippy::cast_precision_loss)]
18625fn sql_div(dividend: &SqliteValue, divisor: &SqliteValue) -> SqliteValue {
18626    if dividend.is_null() || divisor.is_null() {
18627        return SqliteValue::Null;
18628    }
18629    // C SQLite numericType() coercion (SQLite VDBE:1932-1934): if both operands
18630    // are integer-typed (including text that parses as integer), use int math.
18631    let both_int = both_integer_numeric_type(dividend, divisor);
18632    if both_int {
18633        let a = dividend.to_integer();
18634        let b = divisor.to_integer();
18635        if b == 0 {
18636            SqliteValue::Null
18637        } else {
18638            match a.checked_div(b) {
18639                Some(result) => SqliteValue::Integer(result),
18640                // i64::MIN / -1 overflows; C SQLite promotes to float via
18641                // `goto fp_math` (SQLite VDBE:1916), NOT wrapping.
18642                #[allow(clippy::cast_precision_loss)]
18643                None => {
18644                    let result = a as f64 / b as f64;
18645                    if result.is_nan() {
18646                        SqliteValue::Null
18647                    } else {
18648                        SqliteValue::Float(result)
18649                    }
18650                }
18651            }
18652        }
18653    } else {
18654        let b = divisor.to_float();
18655        if b == 0.0 {
18656            SqliteValue::Null
18657        } else {
18658            let result = dividend.to_float() / b;
18659            if result.is_nan() {
18660                SqliteValue::Null
18661            } else {
18662                SqliteValue::Float(result)
18663            }
18664        }
18665    }
18666}
18667
18668/// SQL remainder with NULL propagation and division-by-zero handling.
18669///
18670/// C SQLite (SQLite VDBE:1920): when both operands are MEM_Int, result is Integer.
18671/// When either is Float/Text/Blob, fp_math path casts to integer for the
18672/// modulo but stores the result as Float (MEM_Real).
18673/// C SQLite `numericType()` coercion: text that parses as integer is treated
18674/// as integer for the both-int check (SQLite VDBE:1932-1934).
18675#[allow(clippy::cast_precision_loss)]
18676fn sql_rem(dividend: &SqliteValue, divisor: &SqliteValue) -> SqliteValue {
18677    if dividend.is_null() || divisor.is_null() {
18678        return SqliteValue::Null;
18679    }
18680    let both_int = both_integer_numeric_type(dividend, divisor);
18681    let a = dividend.to_integer();
18682    let b = divisor.to_integer();
18683    if b == 0 {
18684        return SqliteValue::Null;
18685    }
18686    // i64::MIN % -1 = 0 mathematically.
18687    let result = a.checked_rem(b).unwrap_or_default();
18688    if both_int {
18689        SqliteValue::Integer(result)
18690    } else {
18691        SqliteValue::Float(result as f64)
18692    }
18693}
18694
18695/// SQL shift left (SQLite semantics: negative shift = shift right).
18696fn sql_shift_left(val: i64, amount: i64) -> SqliteValue {
18697    if amount < 0 {
18698        return sql_shift_right(val, amount.saturating_neg());
18699    }
18700    if amount >= 64 {
18701        return SqliteValue::Integer(0);
18702    }
18703    // amount is in [0, 63] so the cast is safe.
18704    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18705    let shift = amount as u32;
18706    SqliteValue::Integer(val << shift)
18707}
18708
18709/// SQL shift right (SQLite semantics: negative shift = shift left).
18710fn sql_shift_right(val: i64, amount: i64) -> SqliteValue {
18711    if amount < 0 {
18712        return sql_shift_left(val, amount.saturating_neg());
18713    }
18714    if amount >= 64 {
18715        return SqliteValue::Integer(if val < 0 { -1 } else { 0 });
18716    }
18717    // amount is in [0, 63] so the cast is safe.
18718    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18719    let shift = amount as u32;
18720    SqliteValue::Integer(val >> shift)
18721}
18722
18723/// Three-valued SQL AND.
18724fn sql_and(a: &SqliteValue, b: &SqliteValue) -> SqliteValue {
18725    // C SQLite compiles AND/OR using OP_If/OP_IfNot which use
18726    // sqlite3VdbeRealValue() != 0.0, so 0.5 is truthy.
18727    let a_val = if a.is_null() {
18728        None
18729    } else {
18730        Some(vdbe_real_is_truthy(a))
18731    };
18732    let b_val = if b.is_null() {
18733        None
18734    } else {
18735        Some(vdbe_real_is_truthy(b))
18736    };
18737
18738    match (a_val, b_val) {
18739        (Some(false), _) | (_, Some(false)) => SqliteValue::Integer(0),
18740        (Some(true), Some(true)) => SqliteValue::Integer(1),
18741        _ => SqliteValue::Null,
18742    }
18743}
18744
18745/// Three-valued SQL OR.
18746fn sql_or(a: &SqliteValue, b: &SqliteValue) -> SqliteValue {
18747    let a_val = if a.is_null() {
18748        None
18749    } else {
18750        Some(vdbe_real_is_truthy(a))
18751    };
18752    let b_val = if b.is_null() {
18753        None
18754    } else {
18755        Some(vdbe_real_is_truthy(b))
18756    };
18757
18758    match (a_val, b_val) {
18759        (Some(true), _) | (_, Some(true)) => SqliteValue::Integer(1),
18760        (Some(false), Some(false)) => SqliteValue::Integer(0),
18761        _ => SqliteValue::Null,
18762    }
18763}
18764
18765/// Scan the leading numeric prefix from a byte slice.
18766///
18767/// Recognises `[+-]? [0-9]* ('.' [0-9]*)? ([eE] [+-]? [0-9]+)?`.
18768/// Returns the byte offset where the prefix ends (0 if no prefix).
18769fn scan_numeric_prefix(bytes: &[u8]) -> usize {
18770    if bytes.is_empty() {
18771        return 0;
18772    }
18773    let mut i = 0;
18774    // Optional leading sign.
18775    if bytes[i] == b'+' || bytes[i] == b'-' {
18776        i += 1;
18777    }
18778    let digit_start = i;
18779    // Integer digits.
18780    while i < bytes.len() && bytes[i].is_ascii_digit() {
18781        i += 1;
18782    }
18783    // Optional decimal part.
18784    if i < bytes.len() && bytes[i] == b'.' {
18785        i += 1;
18786        while i < bytes.len() && bytes[i].is_ascii_digit() {
18787            i += 1;
18788        }
18789    }
18790    // Must have consumed at least one digit.
18791    if i == digit_start {
18792        return 0;
18793    }
18794    // Optional exponent (e.g. e+10, E-3, e5).
18795    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
18796        let exp_start = i;
18797        i += 1;
18798        if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
18799            i += 1;
18800        }
18801        if i < bytes.len() && bytes[i].is_ascii_digit() {
18802            while i < bytes.len() && bytes[i].is_ascii_digit() {
18803                i += 1;
18804            }
18805        } else {
18806            // No digits after 'e' — revert to before exponent.
18807            i = exp_start;
18808        }
18809    }
18810    i
18811}
18812
18813/// Parse the text/blob prefix used by `CAST(... AS INTEGER)`.
18814///
18815/// SQLite only consumes an optional sign followed by decimal digits here.
18816/// Decimal points and exponents terminate the parse instead of contributing to
18817/// the numeric value.
18818fn parse_cast_integer_prefix(s: &str) -> i64 {
18819    let trimmed = s.trim();
18820    if trimmed.is_empty() {
18821        return 0;
18822    }
18823
18824    let bytes = trimmed.as_bytes();
18825    let mut end = if matches!(bytes.first(), Some(b'+' | b'-')) {
18826        1
18827    } else {
18828        0
18829    };
18830    let digit_start = end;
18831    while end < bytes.len() && bytes[end].is_ascii_digit() {
18832        end += 1;
18833    }
18834    if end == digit_start {
18835        return 0;
18836    }
18837
18838    let prefix = &trimmed[..end];
18839    match prefix.parse::<i64>() {
18840        Ok(value) => value,
18841        Err(_) if prefix.starts_with('-') => i64::MIN,
18842        Err(_) => i64::MAX,
18843    }
18844}
18845
18846/// SQL CAST operation (p2 encodes target type).
18847fn sql_cast(val: SqliteValue, target: i32) -> SqliteValue {
18848    if val.is_null() {
18849        return SqliteValue::Null;
18850    }
18851    // Target type encoding matches SQLite:
18852    // 'A' (65) = BLOB, 'B' (66) = TEXT, 'C' (67) = NUMERIC,
18853    // 'D' (68) = INTEGER, 'E' (69) = REAL
18854    // But more commonly p2 is used as an affinity character.
18855    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18856    let target_byte = target as u8;
18857    // C SQLite interprets blob bytes as UTF-8 text before numeric casts.
18858    let val = match (val, target_byte) {
18859        (SqliteValue::Blob(b), b'C' | b'c' | b'D' | b'd' | b'E' | b'e') => {
18860            SqliteValue::Text(SmallText::from_arc_bytes(b))
18861        }
18862        (other, _) => other,
18863    };
18864    match target_byte {
18865        b'A' | b'a' => SqliteValue::Blob(match val {
18866            SqliteValue::Blob(b) => b,
18867            SqliteValue::Text(s) => Arc::from(s.as_bytes_direct()),
18868            other => Arc::from(other.to_text().into_bytes()),
18869        }),
18870        b'B' | b'b' => {
18871            // C SQLite changes the storage-class tag without validating or
18872            // replacing the BLOB payload. SmallText retains invalid UTF-8 in
18873            // its byte-preserving raw representation.
18874            match val {
18875                SqliteValue::Blob(b) => SqliteValue::Text(SmallText::from_arc_bytes(b)),
18876                other => SqliteValue::Text(other.to_text().into()),
18877            }
18878        }
18879        b'C' | b'c' => val.cast_to_numeric(),
18880        b'D' | b'd' => {
18881            // C SQLite integer casts from text/blob consume only the signed
18882            // integer prefix; decimal/exponent syntax is ignored.
18883            match &val {
18884                SqliteValue::Text(s) => SqliteValue::Integer(parse_cast_integer_prefix(s)),
18885                _ => SqliteValue::Integer(val.to_integer()),
18886            }
18887        }
18888        b'E' | b'e' => {
18889            // C SQLite: CAST('3.14abc' AS REAL) extracts leading numeric prefix.
18890            // C SQLite allows Inf from "1e999" but not from literal "inf" text.
18891            match &val {
18892                SqliteValue::Text(s) => {
18893                    let trimmed = s.trim();
18894                    let end = scan_numeric_prefix(trimmed.as_bytes());
18895                    // Full-string numeric match (rejects Rust's "nan"/"inf" parsing).
18896                    if end == trimmed.len() && end > 0 {
18897                        if let Ok(f) = trimmed.parse::<f64>() {
18898                            return SqliteValue::Float(f);
18899                        }
18900                    }
18901                    // Prefix match for strings with trailing non-numeric text.
18902                    if end > 0 {
18903                        if let Ok(f) = trimmed[..end].parse::<f64>() {
18904                            return SqliteValue::Float(f);
18905                        }
18906                    }
18907                    SqliteValue::Float(0.0)
18908                }
18909                _ => SqliteValue::Float(val.to_float()),
18910            }
18911        }
18912        _ => val, // unknown: no-op
18913    }
18914}
18915
18916/// Convert affinity character to `TypeAffinity`.
18917fn char_to_affinity(ch: char) -> fsqlite_types::TypeAffinity {
18918    match ch {
18919        'B' | 'b' => fsqlite_types::TypeAffinity::Text,
18920        'C' | 'c' => fsqlite_types::TypeAffinity::Numeric,
18921        'D' | 'd' => fsqlite_types::TypeAffinity::Integer,
18922        'E' | 'e' => fsqlite_types::TypeAffinity::Real,
18923        _ => fsqlite_types::TypeAffinity::Blob,
18924    }
18925}
18926
18927// ── Tests ───────────────────────────────────────────────────────────────────
18928
18929#[cfg(test)]
18930mod tests {
18931    use std::sync::Arc;
18932    use std::time::Instant;
18933
18934    use super::*;
18935    use crate::{Label, ProgramBuilder};
18936    use asupersync::runtime::{Runtime, RuntimeBuilder};
18937    use fsqlite_func::vtab::{IndexInfo, VirtualTable, VirtualTableCursor};
18938    use fsqlite_func::{FunctionRegistry, ScalarFunction, register_builtins};
18939    use fsqlite_mvcc::ConcurrentRegistry;
18940    use fsqlite_types::limits::MAX_COLUMN;
18941    use fsqlite_types::opcode::{IndexCursorMeta, Opcode, P4, VdbeOp};
18942    use fsqlite_types::record::{parse_record, parse_record_into, serialize_record_iter_into};
18943    use fsqlite_types::serial_type::{
18944        serial_type_for_blob, serial_type_for_integer, serial_type_for_text, varint_len,
18945        write_varint,
18946    };
18947    use fsqlite_types::{SmallText, Snapshot};
18948    use proptest::prelude::*;
18949    use proptest::test_runner::{Config as ProptestConfig, TestCaseError, TestRunner};
18950    use rusqlite::params_from_iter;
18951    use rusqlite::types::Value as RusqliteValue;
18952
18953    struct RecordProfileThreadOverrideGuard {
18954        previous: Option<bool>,
18955    }
18956
18957    impl RecordProfileThreadOverrideGuard {
18958        fn enabled() -> Self {
18959            let previous = fsqlite_types::record::record_profile_thread_override();
18960            fsqlite_types::record::set_record_profile_thread_override(Some(true));
18961            Self { previous }
18962        }
18963    }
18964
18965    impl Drop for RecordProfileThreadOverrideGuard {
18966        fn drop(&mut self) {
18967            fsqlite_types::record::set_record_profile_thread_override(self.previous);
18968        }
18969    }
18970
18971    struct CancelExecutionFunc {
18972        cx: Cx,
18973    }
18974
18975    fn test_runtime() -> Runtime {
18976        RuntimeBuilder::current_thread()
18977            .blocking_threads(1, 1)
18978            .build()
18979            .expect("test runtime should build")
18980    }
18981
18982    fn run_async<F: std::future::Future>(future: F) -> F::Output {
18983        test_runtime().block_on(future)
18984    }
18985
18986    impl ScalarFunction for CancelExecutionFunc {
18987        fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
18988            self.cx.cancel();
18989            Ok(SqliteValue::Null)
18990        }
18991
18992        fn num_args(&self) -> i32 {
18993            0
18994        }
18995
18996        fn name(&self) -> &str {
18997            "cancel_exec"
18998        }
18999    }
19000
19001    /// Build and execute a program, returning results.
19002    fn run_program(build: impl FnOnce(&mut ProgramBuilder)) -> Vec<Vec<SqliteValue>> {
19003        let mut b = ProgramBuilder::new();
19004        build(&mut b);
19005        let prog = b.finish().expect("program should build");
19006        let mut engine = VdbeEngine::new(prog.register_count());
19007        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
19008        assert_eq!(outcome, ExecOutcome::Done);
19009        engine
19010            .take_results()
19011            .into_iter()
19012            .map(|v| v.into_vec())
19013            .collect()
19014    }
19015
19016    #[test]
19017    fn test_schema_evaluation_context_function_policy_and_diagnostics() {
19018        let fixed_datetime = [SqliteValue::Text("2000-01-01".into())];
19019        let current_datetime = [SqliteValue::Text("now".into())];
19020
19021        for context in [
19022            SchemaEvaluationContext::Index,
19023            SchemaEvaluationContext::GeneratedColumn,
19024        ] {
19025            let error = validate_schema_function_invocation(
19026                context,
19027                "RANDOM",
19028                fsqlite_func::ScalarSchemaSafety::Never,
19029                &[],
19030            )
19031            .expect_err("indexes and generated columns must reject Never functions");
19032            let expected_owner = match context {
19033                SchemaEvaluationContext::Index => "an index",
19034                SchemaEvaluationContext::GeneratedColumn => "a generated column",
19035                SchemaEvaluationContext::CheckConstraint => unreachable!(),
19036            };
19037            assert!(
19038                error.to_string().contains(&format!(
19039                    "non-deterministic use of random() in {expected_owner}"
19040                )),
19041                "unexpected diagnostic: {error}"
19042            );
19043        }
19044
19045        validate_schema_function_invocation(
19046            SchemaEvaluationContext::CheckConstraint,
19047            "RANDOM",
19048            fsqlite_func::ScalarSchemaSafety::Never,
19049            &[],
19050        )
19051        .expect("ordinary nondeterministic functions are legal in CHECK constraints");
19052
19053        for (context, expected_owner) in [
19054            (SchemaEvaluationContext::Index, "an index"),
19055            (
19056                SchemaEvaluationContext::GeneratedColumn,
19057                "a generated column",
19058            ),
19059            (
19060                SchemaEvaluationContext::CheckConstraint,
19061                "a CHECK constraint",
19062            ),
19063        ] {
19064            validate_schema_function_invocation(
19065                context,
19066                "DATE",
19067                fsqlite_func::ScalarSchemaSafety::DateTimeConditional,
19068                &fixed_datetime,
19069            )
19070            .expect("fixed date/time arguments are safe in every schema context");
19071
19072            let error = validate_schema_function_invocation(
19073                context,
19074                "DATE",
19075                fsqlite_func::ScalarSchemaSafety::DateTimeConditional,
19076                &current_datetime,
19077            )
19078            .expect_err("current-time date() must fail in every schema context");
19079            assert!(
19080                error.to_string().contains(&format!(
19081                    "non-deterministic use of date() in {expected_owner}"
19082                )),
19083                "unexpected diagnostic: {error}"
19084            );
19085        }
19086    }
19087
19088    #[test]
19089    fn test_payload_includes_rowid_alias_does_not_false_positive_when_first_stored_value_matches_rowid()
19090     {
19091        let payload_values = vec![
19092            SqliteValue::Integer(1),
19093            SqliteValue::Text("local".into()),
19094            SqliteValue::Text("dup-session".into()),
19095        ];
19096
19097        assert!(
19098            !payload_includes_rowid_alias(&payload_values, 1, 0, Some(4), Some(1)),
19099            "omitted INTEGER PRIMARY KEY aliases must be detected by payload width, not by a coincidental value match"
19100        );
19101        assert!(
19102            !payload_includes_rowid_alias(&payload_values, 1, 0, None, Some(1)),
19103            "countless fallback must not treat a matching first stored integer as proof that the rowid alias is present"
19104        );
19105    }
19106
19107    #[test]
19108    fn test_payload_includes_rowid_alias_lazy_does_not_false_positive_when_first_stored_value_matches_rowid()
19109     {
19110        let record = serialize_record(&[
19111            SqliteValue::Integer(1),
19112            SqliteValue::Text("local".into()),
19113            SqliteValue::Text("dup-session".into()),
19114        ]);
19115        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19116        scratch
19117            .prepare_for_record(&record)
19118            .expect("payload should decode");
19119
19120        assert!(
19121            !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, Some(4), Some(1)),
19122            "lazy rowid-alias detection must not shift columns when the first stored value happens to equal the rowid"
19123        );
19124        assert!(
19125            !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, None, Some(1)),
19126            "countless lazy fallback must stay conservative instead of trusting value equality"
19127        );
19128    }
19129
19130    #[test]
19131    fn test_payload_includes_rowid_alias_lazy_accepts_explicit_null_placeholder() {
19132        let record = serialize_record(&[
19133            SqliteValue::Null,
19134            SqliteValue::Integer(1),
19135            SqliteValue::Text("local".into()),
19136        ]);
19137        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19138        scratch
19139            .prepare_for_record(&record)
19140            .expect("payload should decode");
19141
19142        assert!(payload_includes_rowid_alias_lazy(
19143            &scratch,
19144            &record,
19145            1,
19146            0,
19147            Some(3),
19148            Some(1)
19149        ));
19150        assert!(payload_includes_rowid_alias_lazy(
19151            &scratch,
19152            &record,
19153            1,
19154            0,
19155            None,
19156            Some(1)
19157        ));
19158        assert!(payload_includes_rowid_alias_lazy(
19159            &scratch,
19160            &record,
19161            1,
19162            0,
19163            Some(4),
19164            Some(1)
19165        ));
19166    }
19167
19168    #[test]
19169    fn test_payload_includes_rowid_alias_without_rowid_resolves_common_width_cases() {
19170        let record = serialize_record(&[
19171            SqliteValue::Integer(1),
19172            SqliteValue::Text("local".into()),
19173            SqliteValue::Text("dup-session".into()),
19174        ]);
19175        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19176        scratch
19177            .prepare_for_record(&record)
19178            .expect("payload should decode");
19179
19180        assert!(
19181            payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(3), Some(1)),
19182            "matching payload/table width proves the payload already carries the IPK alias"
19183        );
19184        assert!(
19185            !payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(4), Some(1)),
19186            "one-column-short integer rows must not compare against rowid because the first stored user column may match by coincidence"
19187        );
19188    }
19189
19190    #[test]
19191    fn test_payload_includes_rowid_alias_without_rowid_accepts_short_null_placeholder() {
19192        let record = serialize_record(&[
19193            SqliteValue::Null,
19194            SqliteValue::Text("local".into()),
19195            SqliteValue::Text("dup-session".into()),
19196        ]);
19197        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19198        scratch
19199            .prepare_for_record(&record)
19200            .expect("payload should decode");
19201
19202        assert!(
19203            payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(4), Some(1)),
19204            "SQLite-format rows keep a NULL IPK placeholder when later ALTER TABLE ADD COLUMN makes the payload one column short"
19205        );
19206    }
19207
19208    #[test]
19209    fn test_payload_includes_rowid_alias_without_rowid_rejects_ambiguous_integer_match() {
19210        let record = serialize_record(&[
19211            SqliteValue::Integer(2),
19212            SqliteValue::Text("local".into()),
19213            SqliteValue::Text("dup-session".into()),
19214        ]);
19215        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19216        scratch
19217            .prepare_for_record(&record)
19218            .expect("payload should decode");
19219
19220        assert!(
19221            !payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(5), Some(1)),
19222            "ambiguous integer-at-IPK rows must not be accepted by rowid equality"
19223        );
19224        assert!(
19225            !payload_includes_rowid_alias_without_rowid(&scratch, 0, None, Some(1)),
19226            "the metadata-free fallback remains conservative"
19227        );
19228    }
19229
19230    #[test]
19231    fn test_payload_includes_rowid_alias_lazy_rejects_multi_column_short_integer_match() {
19232        let record = serialize_record(&[
19233            SqliteValue::Integer(1),
19234            SqliteValue::Text("local".into()),
19235            SqliteValue::Text("dup-session".into()),
19236        ]);
19237        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19238        scratch
19239            .prepare_for_record(&record)
19240            .expect("payload should decode");
19241
19242        assert!(
19243            !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, Some(5), Some(1)),
19244            "multi-column-short rows must not treat a shifted user integer equal to rowid as an IPK placeholder"
19245        );
19246    }
19247
19248    #[test]
19249    fn test_payload_includes_rowid_alias_detects_short_alter_row_via_not_null_column_metadata() {
19250        let payload_values = vec![
19251            SqliteValue::Null,
19252            SqliteValue::Integer(2),
19253            SqliteValue::Integer(2),
19254            SqliteValue::Text("remote-ci".into()),
19255            SqliteValue::Text("/sessions/b.jsonl".into()),
19256            SqliteValue::Integer(1_700_000_001_000),
19257        ];
19258
19259        assert!(payload_includes_rowid_alias(
19260            &payload_values,
19261            2,
19262            0,
19263            Some(7),
19264            Some(1)
19265        ));
19266    }
19267
19268    #[test]
19269    fn test_payload_includes_rowid_alias_lazy_detects_short_alter_row_via_not_null_column_metadata()
19270    {
19271        let record = serialize_record(&[
19272            SqliteValue::Null,
19273            SqliteValue::Integer(2),
19274            SqliteValue::Integer(2),
19275            SqliteValue::Text("remote-ci".into()),
19276            SqliteValue::Text("/sessions/b.jsonl".into()),
19277            SqliteValue::Integer(1_700_000_001_000),
19278        ]);
19279        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19280        scratch
19281            .prepare_for_record(&record)
19282            .expect("payload should decode");
19283
19284        assert!(payload_includes_rowid_alias_lazy(
19285            &scratch,
19286            &record,
19287            2,
19288            0,
19289            Some(7),
19290            Some(1)
19291        ));
19292    }
19293
19294    #[test]
19295    fn test_payload_includes_rowid_alias_accepts_short_null_placeholder() {
19296        let payload_values = vec![
19297            SqliteValue::Null,
19298            SqliteValue::Integer(5),
19299            SqliteValue::Text("tail".into()),
19300        ];
19301
19302        assert!(payload_includes_rowid_alias(
19303            &payload_values,
19304            9,
19305            0,
19306            Some(4),
19307            Some(2)
19308        ));
19309    }
19310
19311    #[test]
19312    fn test_payload_includes_rowid_alias_accepts_multi_column_short_null_placeholder() {
19313        let payload_values = vec![SqliteValue::Null, SqliteValue::Text("old".into())];
19314
19315        assert!(payload_includes_rowid_alias(
19316            &payload_values,
19317            9,
19318            0,
19319            Some(4),
19320            None
19321        ));
19322    }
19323
19324    #[test]
19325    fn test_payload_includes_rowid_alias_lazy_accepts_short_null_placeholder() {
19326        let record = serialize_record(&[
19327            SqliteValue::Null,
19328            SqliteValue::Integer(5),
19329            SqliteValue::Text("tail".into()),
19330        ]);
19331        let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19332        scratch
19333            .prepare_for_record(&record)
19334            .expect("payload should decode");
19335
19336        assert!(payload_includes_rowid_alias_lazy(
19337            &scratch,
19338            &record,
19339            9,
19340            0,
19341            Some(4),
19342            Some(2)
19343        ));
19344    }
19345
19346    fn install_duplicate_prefix_index_fixture(
19347        engine: &mut VdbeEngine,
19348        cursor_id: i32,
19349    ) -> (Vec<u8>, Vec<u8>) {
19350        let mut db = MemDatabase::new();
19351        let index_root = db.allocate_root_page();
19352
19353        engine.enable_storage_cursors(true);
19354        engine.set_database(db);
19355        engine.set_reject_mem_fallback(false);
19356
19357        assert!(
19358            run_async(engine.open_storage_cursor(cursor_id, index_root, true)),
19359            "index storage cursor should open"
19360        );
19361
19362        let sc = engine
19363            .storage_cursors
19364            .get_mut(&cursor_id)
19365            .expect("storage cursor should exist");
19366        for rowid in 1_i64..=128 {
19367            let key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(rowid)]);
19368            run_async(sc.cursor.index_insert(&sc.cx, &key))
19369                .expect("duplicate-prefix index key should insert");
19370        }
19371        run_async(sc.cursor.index_insert(
19372            &sc.cx,
19373            &encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(999)]),
19374        ))
19375        .expect("next-prefix index key should insert");
19376
19377        let conflict_key = encode_record(&[SqliteValue::Integer(7)]);
19378        let miss_key = encode_record(&[SqliteValue::Integer(9)]);
19379
19380        (conflict_key, miss_key)
19381    }
19382
19383    fn build_storage_index_engine_with_duplicate_prefixes() -> (VdbeEngine, Vec<u8>, Vec<u8>) {
19384        let mut engine = VdbeEngine::new(8);
19385        let (conflict_key, miss_key) = install_duplicate_prefix_index_fixture(&mut engine, 0);
19386        (engine, conflict_key, miss_key)
19387    }
19388
19389    fn build_storage_index_engine_with_nocase_prefixes() -> (VdbeEngine, Vec<u8>, Vec<u8>) {
19390        let mut engine = VdbeEngine::new(8);
19391        let mut db = MemDatabase::new();
19392        let index_root = db.allocate_root_page();
19393
19394        engine.enable_storage_cursors(true);
19395        engine.set_database(db);
19396        engine.set_reject_mem_fallback(false);
19397        engine.set_index_collations_by_root_page(HashMap::from([(
19398            index_root,
19399            vec![Some("NOCASE".to_owned())],
19400        )]));
19401
19402        assert!(
19403            run_async(engine.open_storage_cursor(0, index_root, true)),
19404            "NOCASE index storage cursor should open"
19405        );
19406
19407        let sc = engine
19408            .storage_cursors
19409            .get_mut(&0)
19410            .expect("NOCASE storage cursor should exist");
19411        for (text, rowid) in [("alpha", 1_i64), ("beta", 2_i64)] {
19412            let key = encode_record(&[
19413                SqliteValue::Text(text.to_owned().into()),
19414                SqliteValue::Integer(rowid),
19415            ]);
19416            run_async(sc.cursor.index_insert(&sc.cx, &key))
19417                .expect("NOCASE fixture key should insert");
19418        }
19419
19420        let conflict_key = encode_record(&[SqliteValue::Text("ALPHA".into())]);
19421        let miss_key = encode_record(&[SqliteValue::Text("gamma".into())]);
19422
19423        (engine, conflict_key, miss_key)
19424    }
19425
19426    fn build_storage_index_engine_with_nocase_mixed_case_ordering() -> VdbeEngine {
19427        let mut engine = VdbeEngine::new(8);
19428        let mut db = MemDatabase::new();
19429        let index_root = db.allocate_root_page();
19430
19431        engine.enable_storage_cursors(true);
19432        engine.set_database(db);
19433        engine.set_reject_mem_fallback(false);
19434        engine.set_index_collations_by_root_page(HashMap::from([(
19435            index_root,
19436            vec![Some("NOCASE".to_owned())],
19437        )]));
19438
19439        assert!(
19440            run_async(engine.open_storage_cursor(0, index_root, true)),
19441            "NOCASE index storage cursor should open"
19442        );
19443
19444        let sc = engine
19445            .storage_cursors
19446            .get_mut(&0)
19447            .expect("NOCASE storage cursor should exist");
19448        for key in [
19449            encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(1)]),
19450            encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(2)]),
19451            encode_record(&[SqliteValue::Text("beta".into()), SqliteValue::Integer(3)]),
19452        ] {
19453            run_async(sc.cursor.index_insert(&sc.cx, &key))
19454                .expect("mixed-case NOCASE fixture key should insert");
19455        }
19456
19457        engine
19458    }
19459
19460    #[test]
19461    fn test_find_conflicting_rowid_rejects_index_entry_without_rowid_suffix() {
19462        let mut engine = VdbeEngine::new(8);
19463        let mut db = MemDatabase::new();
19464        let index_root = db.allocate_root_page();
19465
19466        engine.enable_storage_cursors(true);
19467        engine.set_database(db);
19468        engine.set_reject_mem_fallback(false);
19469        assert!(
19470            run_async(engine.open_storage_cursor(0, index_root, true)),
19471            "index storage cursor should open"
19472        );
19473
19474        let sc = engine
19475            .storage_cursors
19476            .get_mut(&0)
19477            .expect("storage cursor should exist");
19478        run_async(
19479            sc.cursor
19480                .index_insert(&sc.cx, &encode_record(&[SqliteValue::Integer(7)])),
19481        )
19482        .expect("malformed index fixture should insert for corruption regression");
19483
19484        let probe_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(42)]);
19485        let err = run_async(find_conflicting_rowid_in_index(sc, &probe_key, 1))
19486            .expect_err("missing rowid suffix should be reported as corruption");
19487
19488        assert!(matches!(
19489            err,
19490            FrankenError::DatabaseCorrupt { detail }
19491                if detail.contains("integer rowid suffix")
19492        ));
19493    }
19494
19495    #[test]
19496    fn test_find_conflicting_rowid_rejects_index_entry_with_extra_suffix_fields() {
19497        let mut engine = VdbeEngine::new(8);
19498        let mut db = MemDatabase::new();
19499        let index_root = db.allocate_root_page();
19500
19501        engine.enable_storage_cursors(true);
19502        engine.set_database(db);
19503        engine.set_reject_mem_fallback(false);
19504        assert!(
19505            run_async(engine.open_storage_cursor(0, index_root, true)),
19506            "index storage cursor should open"
19507        );
19508
19509        let sc = engine
19510            .storage_cursors
19511            .get_mut(&0)
19512            .expect("storage cursor should exist");
19513        run_async(sc.cursor.index_insert(
19514            &sc.cx,
19515            &encode_record(&[
19516                SqliteValue::Integer(7),
19517                SqliteValue::Integer(1),
19518                SqliteValue::Text("extra".into()),
19519            ]),
19520        ))
19521        .expect("malformed index fixture should insert for corruption regression");
19522
19523        let probe_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(42)]);
19524        let err = run_async(find_conflicting_rowid_in_index(sc, &probe_key, 1))
19525            .expect_err("extra suffix fields should be reported as corruption");
19526
19527        assert!(matches!(
19528            err,
19529            FrankenError::DatabaseCorrupt { detail }
19530                if detail.contains("integer rowid suffix")
19531        ));
19532    }
19533
19534    #[test]
19535    fn test_collated_conflicting_rowid_rejects_index_entry_without_rowid_suffix() {
19536        let mut engine = VdbeEngine::new(8);
19537        let mut db = MemDatabase::new();
19538        let index_root = db.allocate_root_page();
19539
19540        engine.enable_storage_cursors(true);
19541        engine.set_database(db);
19542        engine.set_reject_mem_fallback(false);
19543        engine.set_index_collations_by_root_page(HashMap::from([(
19544            index_root,
19545            vec![Some("NOCASE".to_owned())],
19546        )]));
19547        assert!(
19548            run_async(engine.open_storage_cursor(0, index_root, true)),
19549            "NOCASE index storage cursor should open"
19550        );
19551
19552        let sc = engine
19553            .storage_cursors
19554            .get_mut(&0)
19555            .expect("NOCASE storage cursor should exist");
19556        run_async(
19557            sc.cursor
19558                .index_insert(&sc.cx, &encode_record(&[SqliteValue::Text("alpha".into())])),
19559        )
19560        .expect("malformed NOCASE index fixture should insert for corruption regression");
19561
19562        let probe_key =
19563            encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(42)]);
19564        let err = run_async(find_conflicting_rowid_in_index_collated(
19565            sc,
19566            &probe_key,
19567            1,
19568            &[false],
19569            &[Some("NOCASE".to_owned())],
19570            &BUILTIN_COLLATION_REGISTRY,
19571        ))
19572        .expect_err("missing rowid suffix should be reported as corruption");
19573
19574        assert!(matches!(
19575            err,
19576            FrankenError::DatabaseCorrupt { detail }
19577                if detail.contains("integer rowid suffix")
19578        ));
19579    }
19580
19581    #[test]
19582    fn test_collated_conflicting_rowid_rejects_index_entry_with_extra_suffix_fields() {
19583        let mut engine = VdbeEngine::new(8);
19584        let mut db = MemDatabase::new();
19585        let index_root = db.allocate_root_page();
19586
19587        engine.enable_storage_cursors(true);
19588        engine.set_database(db);
19589        engine.set_reject_mem_fallback(false);
19590        engine.set_index_collations_by_root_page(HashMap::from([(
19591            index_root,
19592            vec![Some("NOCASE".to_owned())],
19593        )]));
19594        assert!(
19595            run_async(engine.open_storage_cursor(0, index_root, true)),
19596            "NOCASE index storage cursor should open"
19597        );
19598
19599        let sc = engine
19600            .storage_cursors
19601            .get_mut(&0)
19602            .expect("NOCASE storage cursor should exist");
19603        run_async(sc.cursor.index_insert(
19604            &sc.cx,
19605            &encode_record(&[
19606                SqliteValue::Text("alpha".into()),
19607                SqliteValue::Integer(1),
19608                SqliteValue::Text("extra".into()),
19609            ]),
19610        ))
19611        .expect("malformed NOCASE index fixture should insert for corruption regression");
19612
19613        let probe_key =
19614            encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(42)]);
19615        let err = run_async(find_conflicting_rowid_in_index_collated(
19616            sc,
19617            &probe_key,
19618            1,
19619            &[false],
19620            &[Some("NOCASE".to_owned())],
19621            &BUILTIN_COLLATION_REGISTRY,
19622        ))
19623        .expect_err("extra suffix fields should be reported as corruption");
19624
19625        assert!(matches!(
19626            err,
19627            FrankenError::DatabaseCorrupt { detail }
19628                if detail.contains("integer rowid suffix")
19629        ));
19630    }
19631
19632    #[test]
19633    fn test_idxinsert_consumes_make_record_sideband_for_storage_cursor() {
19634        let mut engine = VdbeEngine::new(8);
19635        let mut db = MemDatabase::new();
19636        let index_root = db.allocate_root_page();
19637        engine.enable_storage_cursors(true);
19638        engine.set_database(db);
19639        engine.set_reject_mem_fallback(false);
19640        assert!(
19641            run_async(engine.open_storage_cursor(0, index_root, true)),
19642            "index storage cursor should open"
19643        );
19644
19645        let mut b = ProgramBuilder::new();
19646        let end = b.emit_label();
19647        let found = b.emit_label();
19648        let done = b.emit_label();
19649        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19650
19651        let r_insert_val = b.alloc_reg();
19652        let r_insert_key = b.alloc_reg();
19653        let r_probe_val = b.alloc_reg();
19654        let r_probe_key = b.alloc_reg();
19655        let r_out = b.alloc_reg();
19656
19657        b.emit_op(Opcode::Integer, 7, r_insert_val, 0, P4::None, 0);
19658        b.emit_op(
19659            Opcode::MakeRecord,
19660            r_insert_val,
19661            1,
19662            r_insert_key,
19663            P4::None,
19664            0,
19665        );
19666        b.emit_op(Opcode::IdxInsert, 0, r_insert_key, 0, P4::None, 0);
19667
19668        b.emit_op(Opcode::Integer, 7, r_probe_val, 0, P4::None, 0);
19669        b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
19670        b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
19671        b.emit_jump_to_label(Opcode::Found, 0, r_probe_key, found, P4::None, 0);
19672        b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
19673        b.resolve_label(found);
19674        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
19675        b.resolve_label(done);
19676
19677        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
19678        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19679        b.resolve_label(end);
19680
19681        let program = b.finish().expect("program should build");
19682        let rows = execute_program_with_engine(&mut engine, &program);
19683        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
19684    }
19685
19686    #[test]
19687    fn test_idxinsert_conflict_skip_preserves_make_record_sideband_buffer() {
19688        let _guard = VDBE_OBSERVABILITY_LOCK
19689            .lock()
19690            .unwrap_or_else(|e| e.into_inner());
19691        reset_vdbe_test_sideband_materialization_count();
19692
19693        let mut builder = ProgramBuilder::new();
19694        let end = builder.emit_label();
19695        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19696
19697        let r_value = builder.alloc_reg();
19698        let r_record = builder.alloc_reg();
19699        builder.emit_op(
19700            Opcode::String8,
19701            0,
19702            r_value,
19703            0,
19704            P4::Str("idx-skip-sideband".repeat(32)),
19705            0,
19706        );
19707        builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19708        builder.emit_op(Opcode::IdxInsert, 0, r_record, 0, P4::None, 0);
19709        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19710        builder.resolve_label(end);
19711
19712        let program = builder.finish().expect("program should build");
19713        let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19714        let mut engine = VdbeEngine::new(program.register_count());
19715        engine.set_conflict_skip_idx(true);
19716
19717        let before = vdbe_test_sideband_materialization_count_snapshot();
19718        let outcome = run_async(engine.execute(&program)).expect("program should execute");
19719        let after = vdbe_test_sideband_materialization_count_snapshot();
19720
19721        assert_eq!(outcome, ExecOutcome::Done);
19722        assert_eq!(
19723            after - before,
19724            0,
19725            "skipped IdxInsert should not materialize a sideband-backed key"
19726        );
19727        assert!(
19728            engine.make_record_lookaside.sideband_is_armed_for(r_record),
19729            "skipped IdxInsert should leave the MakeRecord sideband attached to its register"
19730        );
19731        assert!(
19732            !engine.make_record_lookaside.is_empty(),
19733            "skipped IdxInsert should retain the sideband bytes for reuse/materialization"
19734        );
19735        assert!(
19736            engine.make_record_lookaside.capacity() >= expected_capacity,
19737            "skipped IdxInsert should not drop the reusable sideband allocation"
19738        );
19739    }
19740
19741    #[test]
19742    fn test_idxinsert_returns_make_record_sideband_buffer_after_storage_insert() {
19743        let _guard = VDBE_OBSERVABILITY_LOCK
19744            .lock()
19745            .unwrap_or_else(|e| e.into_inner());
19746        reset_vdbe_test_sideband_materialization_count();
19747
19748        let index_value = "idx-insert-sideband".repeat(32);
19749        let expected_key = encode_record(&[SqliteValue::Text(index_value.clone().into())]);
19750
19751        let mut engine = VdbeEngine::new(8);
19752        let mut db = MemDatabase::new();
19753        let index_root = db.allocate_root_page();
19754        engine.enable_storage_cursors(true);
19755        engine.set_database(db);
19756        engine.set_reject_mem_fallback(false);
19757        assert!(
19758            run_async(engine.open_storage_cursor(0, index_root, true)),
19759            "index storage cursor should open"
19760        );
19761
19762        let mut builder = ProgramBuilder::new();
19763        let end = builder.emit_label();
19764        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19765
19766        let r_value = builder.alloc_reg();
19767        let r_record = builder.alloc_reg();
19768        builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(index_value), 0);
19769        builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19770        builder.emit_op(Opcode::IdxInsert, 0, r_record, 0, P4::None, 0);
19771        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19772        builder.resolve_label(end);
19773
19774        let program = builder.finish().expect("program should build");
19775        let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19776        let before = vdbe_test_sideband_materialization_count_snapshot();
19777        let outcome = run_async(engine.execute(&program)).expect("program should execute");
19778        let after = vdbe_test_sideband_materialization_count_snapshot();
19779
19780        assert_eq!(outcome, ExecOutcome::Done);
19781        assert_eq!(
19782            after - before,
19783            0,
19784            "IdxInsert should consume MakeRecord sideband bytes without materializing"
19785        );
19786        assert!(
19787            engine.make_record_lookaside.is_empty(),
19788            "successful IdxInsert should return a cleared scratch buffer"
19789        );
19790        assert!(
19791            engine.make_record_lookaside.capacity() >= expected_capacity,
19792            "successful IdxInsert should keep the reusable sideband allocation"
19793        );
19794
19795        let index_cursor = engine
19796            .storage_cursors
19797            .get_mut(&0)
19798            .expect("index cursor should remain available");
19799        assert!(
19800            run_async(
19801                index_cursor
19802                    .cursor
19803                    .index_move_to(&index_cursor.cx, &expected_key)
19804            )
19805            .expect("index seek should succeed")
19806            .is_found(),
19807            "IdxInsert should still insert the key while recycling the sideband buffer"
19808        );
19809    }
19810
19811    #[test]
19812    fn test_idxinsert_unique_abort_returns_make_record_sideband_buffer_on_error() {
19813        let _guard = VDBE_OBSERVABILITY_LOCK
19814            .lock()
19815            .unwrap_or_else(|e| e.into_inner());
19816        reset_vdbe_test_sideband_materialization_count();
19817
19818        let conflict_value = "idx-abort-sideband".repeat(32);
19819        let existing_key = encode_record(&[
19820            SqliteValue::Text(conflict_value.clone().into()),
19821            SqliteValue::Integer(1),
19822        ]);
19823
19824        let mut db = MemDatabase::new();
19825        let index_root = db.allocate_root_page();
19826        let table_root = db.create_table(1);
19827        let mut engine = VdbeEngine::new(8);
19828        engine.enable_storage_cursors(true);
19829        engine.set_database(db);
19830        engine.set_reject_mem_fallback(false);
19831        assert!(run_async(engine.open_storage_cursor(0, index_root, true)));
19832        assert!(run_async(engine.open_storage_cursor(1, table_root, true)));
19833
19834        {
19835            let index_cursor = engine
19836                .storage_cursors
19837                .get_mut(&0)
19838                .expect("index cursor should exist");
19839            run_async(
19840                index_cursor
19841                    .cursor
19842                    .index_insert(&index_cursor.cx, &existing_key),
19843            )
19844            .expect("existing unique index key should insert");
19845        }
19846
19847        let provisional_payload = encode_record(&[SqliteValue::Integer(99)]);
19848        {
19849            let table_cursor = engine
19850                .storage_cursors
19851                .get_mut(&1)
19852                .expect("table cursor should exist");
19853            run_async(
19854                table_cursor
19855                    .cursor
19856                    .table_insert(&table_cursor.cx, 2, &provisional_payload),
19857            )
19858            .expect("provisional table row should insert");
19859        }
19860        engine.changes = 1;
19861        engine.set_pending_insert_rollback(Some(PendingInsertRollback {
19862            cursor_id: 1,
19863            rowid: 2,
19864            previous_last_insert_rowid: 0,
19865            previous_last_insert_rowid_valid: false,
19866            update_restore: None,
19867        }));
19868
19869        let mut builder = ProgramBuilder::new();
19870        let end = builder.emit_label();
19871        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19872
19873        let r_value = builder.alloc_reg();
19874        let r_record = builder.alloc_reg();
19875        builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(conflict_value), 0);
19876        builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19877        builder.emit_op(
19878            Opcode::IdxInsert,
19879            0,
19880            r_record,
19881            1,
19882            P4::Table("idx_col".to_owned()),
19883            1,
19884        );
19885        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19886        builder.resolve_label(end);
19887
19888        let program = builder.finish().expect("program should build");
19889        let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19890        let before = vdbe_test_sideband_materialization_count_snapshot();
19891        let error = run_async(engine.execute(&program))
19892            .expect_err("unique conflict should abort the statement");
19893        let after = vdbe_test_sideband_materialization_count_snapshot();
19894
19895        assert!(
19896            matches!(error, FrankenError::UniqueViolation { .. }),
19897            "expected unique violation, got {error:?}"
19898        );
19899        assert_eq!(
19900            after - before,
19901            0,
19902            "unique abort should not materialize a sideband-backed key"
19903        );
19904        assert!(
19905            engine.make_record_lookaside.is_empty(),
19906            "aborted IdxInsert should return a cleared scratch buffer"
19907        );
19908        assert!(
19909            engine.make_record_lookaside.capacity() >= expected_capacity,
19910            "aborted IdxInsert should keep the reusable sideband allocation"
19911        );
19912    }
19913
19914    #[test]
19915    fn test_idxinsert_ignore_restores_sideband_when_rollback_errors() {
19916        let _guard = VDBE_OBSERVABILITY_LOCK
19917            .lock()
19918            .unwrap_or_else(|e| e.into_inner());
19919        reset_vdbe_test_sideband_materialization_count();
19920
19921        let conflict_value = "idx-ignore-rollback-error-sideband".repeat(16);
19922        let existing_key = encode_record(&[
19923            SqliteValue::Text(conflict_value.clone().into()),
19924            SqliteValue::Integer(1),
19925        ]);
19926
19927        let mut db = MemDatabase::new();
19928        let index_root = db.allocate_root_page();
19929        let mut engine = VdbeEngine::new(8);
19930        engine.enable_storage_cursors(true);
19931        engine.set_database(db);
19932        engine.set_reject_mem_fallback(false);
19933        assert!(run_async(engine.open_storage_cursor(0, index_root, true)));
19934
19935        {
19936            let index_cursor = engine
19937                .storage_cursors
19938                .get_mut(&0)
19939                .expect("index cursor should exist");
19940            run_async(
19941                index_cursor
19942                    .cursor
19943                    .index_insert(&index_cursor.cx, &existing_key),
19944            )
19945            .expect("existing unique index key should insert");
19946        }
19947
19948        let mut builder = ProgramBuilder::new();
19949        let end = builder.emit_label();
19950        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19951
19952        let r_value = builder.alloc_reg();
19953        let r_record = builder.alloc_reg();
19954        builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(conflict_value), 0);
19955        builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19956        builder.emit_op(
19957            Opcode::IdxInsert,
19958            0,
19959            r_record,
19960            1,
19961            P4::Table("idx_col".to_owned()),
19962            1 | (4 << 1),
19963        );
19964        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19965        builder.resolve_label(end);
19966
19967        let program = builder.finish().expect("program should build");
19968        let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19969        let before = vdbe_test_sideband_materialization_count_snapshot();
19970        let error = run_async(engine.execute(&program))
19971            .expect_err("missing provisional insert rollback should error");
19972        let after = vdbe_test_sideband_materialization_count_snapshot();
19973
19974        assert!(
19975            matches!(&error, FrankenError::Internal(message) if message.contains("secondary-index conflict without pending table insert")),
19976            "expected rollback bookkeeping error, got {error:?}"
19977        );
19978        assert_eq!(
19979            after - before,
19980            0,
19981            "rollback error path should not materialize a sideband-backed key"
19982        );
19983        assert!(
19984            engine.make_record_lookaside.is_empty(),
19985            "failed IGNORE rollback should return a cleared scratch buffer"
19986        );
19987        assert!(
19988            engine.make_record_lookaside.capacity() >= expected_capacity,
19989            "failed IGNORE rollback should keep the reusable sideband allocation"
19990        );
19991    }
19992
19993    #[test]
19994    fn test_no_conflict_consumes_make_record_sideband_probe_key() {
19995        let _guard = VDBE_OBSERVABILITY_LOCK
19996            .lock()
19997            .unwrap_or_else(|e| e.into_inner());
19998        let (mut engine, _, _) = build_storage_index_engine_with_duplicate_prefixes();
19999
20000        let mut b = ProgramBuilder::new();
20001        let end = b.emit_label();
20002        let no_conflict = b.emit_label();
20003        let done = b.emit_label();
20004        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20005
20006        let r_probe = b.alloc_reg();
20007        let r_key = b.alloc_reg();
20008        let r_out = b.alloc_reg();
20009
20010        b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
20011        b.emit_op(Opcode::MakeRecord, r_probe, 1, r_key, P4::None, 0);
20012        b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20013        b.emit_jump_to_label(Opcode::NoConflict, 0, r_key, no_conflict, P4::None, 0);
20014        b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20015        b.resolve_label(no_conflict);
20016        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
20017        b.resolve_label(done);
20018
20019        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20020        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20021        b.resolve_label(end);
20022
20023        let program = b.finish().expect("program should build");
20024        let rows = execute_program_with_engine(&mut engine, &program);
20025
20026        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
20027    }
20028
20029    fn build_no_conflict_opcode_probe_program(
20030        conflict_key: Vec<u8>,
20031        miss_key: Vec<u8>,
20032        malformed_key: Vec<u8>,
20033    ) -> crate::VdbeProgram {
20034        let mut b = ProgramBuilder::new();
20035        let end = b.emit_label();
20036        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20037
20038        let r_key = b.alloc_reg();
20039        let r_out = b.alloc_reg();
20040
20041        for (probe_key, expected_insertable) in [
20042            (conflict_key, false),
20043            (miss_key, true),
20044            (malformed_key, true),
20045        ] {
20046            let no_conflict = b.emit_label();
20047            let done = b.emit_label();
20048            b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20049            b.emit_jump_to_label(Opcode::NoConflict, 0, r_key, no_conflict, P4::None, 0);
20050            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20051            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20052            b.resolve_label(no_conflict);
20053            b.emit_op(
20054                Opcode::Integer,
20055                i32::from(expected_insertable),
20056                r_out,
20057                0,
20058                P4::None,
20059                0,
20060            );
20061            b.resolve_label(done);
20062            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20063        }
20064
20065        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20066        b.resolve_label(end);
20067        b.finish().expect("program should build")
20068    }
20069
20070    fn build_found_opcode_probe_program(
20071        conflict_key: Vec<u8>,
20072        miss_key: Vec<u8>,
20073    ) -> crate::VdbeProgram {
20074        let mut b = ProgramBuilder::new();
20075        let end = b.emit_label();
20076        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20077
20078        let r_key = b.alloc_reg();
20079        let r_out = b.alloc_reg();
20080
20081        for (probe_key, expected_found) in [(conflict_key, true), (miss_key, false)] {
20082            let found = b.emit_label();
20083            let done = b.emit_label();
20084            b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20085            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20086            b.emit_jump_to_label(Opcode::Found, 0, r_key, found, P4::None, 0);
20087            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20088            b.resolve_label(found);
20089            b.emit_op(
20090                Opcode::Integer,
20091                i32::from(expected_found),
20092                r_out,
20093                0,
20094                P4::None,
20095                0,
20096            );
20097            b.resolve_label(done);
20098            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20099        }
20100
20101        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20102        b.resolve_label(end);
20103        b.finish().expect("program should build")
20104    }
20105
20106    fn build_not_found_opcode_probe_program(
20107        conflict_key: Vec<u8>,
20108        miss_key: Vec<u8>,
20109    ) -> crate::VdbeProgram {
20110        let mut b = ProgramBuilder::new();
20111        let end = b.emit_label();
20112        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20113
20114        let r_key = b.alloc_reg();
20115        let r_out = b.alloc_reg();
20116
20117        for probe_key in [conflict_key, miss_key] {
20118            let missing = b.emit_label();
20119            let done = b.emit_label();
20120            b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20121            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20122            b.emit_jump_to_label(Opcode::NotFound, 0, r_key, missing, P4::None, 0);
20123            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20124            b.resolve_label(missing);
20125            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
20126            b.resolve_label(done);
20127            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20128        }
20129
20130        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20131        b.resolve_label(end);
20132        b.finish().expect("program should build")
20133    }
20134
20135    fn execute_program_with_engine(
20136        engine: &mut VdbeEngine,
20137        program: &crate::VdbeProgram,
20138    ) -> Vec<Vec<SqliteValue>> {
20139        let outcome = run_async(engine.execute(program)).expect("execution should succeed");
20140        assert_eq!(outcome, ExecOutcome::Done);
20141        engine
20142            .take_results()
20143            .into_iter()
20144            .map(|row| row.into_vec())
20145            .collect::<Vec<_>>()
20146    }
20147
20148    fn legacy_storage_cursor_no_conflict_prefix_match(
20149        cursor: &mut StorageCursor,
20150        key_bytes: &[u8],
20151    ) -> Result<bool> {
20152        let probe_fields = match parse_record(key_bytes) {
20153            Some(fields) => fields,
20154            None => return Ok(false),
20155        };
20156        if probe_fields.iter().any(SqliteValue::is_null) {
20157            return Ok(false);
20158        }
20159        run_async(cursor.cursor.index_move_to(&cursor.cx, key_bytes))?;
20160        if let Ok(entry_bytes) = run_async(cursor.cursor.payload(&cursor.cx)) {
20161            if let Some(entry_fields) = parse_record(&entry_bytes) {
20162                let prefix_len = probe_fields.len();
20163                return Ok(entry_fields.len() >= prefix_len
20164                    && entry_fields[..prefix_len] == probe_fields[..]);
20165            }
20166        }
20167        Ok(false)
20168    }
20169
20170    #[test]
20171    fn test_take_results_preserves_result_buffer_capacity_for_reuse() {
20172        let mut b = ProgramBuilder::new();
20173        let end = b.emit_label();
20174        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20175        let r = b.alloc_reg();
20176        b.emit_op(Opcode::Integer, 1, r, 0, P4::None, 0);
20177        b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
20178        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20179        b.resolve_label(end);
20180        let program = b.finish().expect("program should build");
20181
20182        let mut engine = VdbeEngine::new(program.register_count());
20183        assert_eq!(
20184            engine.result_buffer_capacity(),
20185            64,
20186            "new engines should keep the preallocated result-row buffer"
20187        );
20188
20189        let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20190        assert_eq!(outcome, ExecOutcome::Done);
20191        let rows = engine.take_results();
20192        assert_eq!(rows.len(), 1);
20193        assert_eq!(
20194            engine.result_buffer_capacity(),
20195            64,
20196            "taking results should preserve buffer capacity for the next execution"
20197        );
20198    }
20199
20200    #[test]
20201    fn test_take_exactly_one_result_row_preserves_result_buffer_capacity_for_reuse() {
20202        let mut one_row_builder = ProgramBuilder::new();
20203        let one_row_end = one_row_builder.emit_label();
20204        one_row_builder.emit_jump_to_label(Opcode::Init, 0, 0, one_row_end, P4::None, 0);
20205        let one_row_reg = one_row_builder.alloc_reg();
20206        one_row_builder.emit_op(Opcode::Integer, 7, one_row_reg, 0, P4::None, 0);
20207        one_row_builder.emit_op(Opcode::ResultRow, one_row_reg, 1, 0, P4::None, 0);
20208        one_row_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20209        one_row_builder.resolve_label(one_row_end);
20210        let one_row_program = one_row_builder.finish().expect("program should build");
20211
20212        let mut engine = VdbeEngine::new(one_row_program.register_count());
20213        let initial_capacity = engine.result_buffer_capacity();
20214        let outcome =
20215            run_async(engine.execute(&one_row_program)).expect("one-row execution should succeed");
20216        assert_eq!(outcome, ExecOutcome::Done);
20217        assert_eq!(
20218            engine.take_exactly_one_result_row(),
20219            ExactResultRowOutcome::Row(Box::new(smallvec::smallvec![SqliteValue::Integer(7)])),
20220        );
20221        assert_eq!(
20222            engine.result_buffer_capacity(),
20223            initial_capacity,
20224            "exact-one-row drain should preserve the reusable result buffer"
20225        );
20226
20227        let mut many_rows_builder = ProgramBuilder::new();
20228        let many_rows_end = many_rows_builder.emit_label();
20229        many_rows_builder.emit_jump_to_label(Opcode::Init, 0, 0, many_rows_end, P4::None, 0);
20230        let many_rows_reg = many_rows_builder.alloc_reg();
20231        many_rows_builder.emit_op(Opcode::Integer, 1, many_rows_reg, 0, P4::None, 0);
20232        many_rows_builder.emit_op(Opcode::ResultRow, many_rows_reg, 1, 0, P4::None, 0);
20233        many_rows_builder.emit_op(Opcode::Integer, 2, many_rows_reg, 0, P4::None, 0);
20234        many_rows_builder.emit_op(Opcode::ResultRow, many_rows_reg, 1, 0, P4::None, 0);
20235        many_rows_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20236        many_rows_builder.resolve_label(many_rows_end);
20237        let many_rows_program = many_rows_builder.finish().expect("program should build");
20238
20239        let outcome = run_async(engine.execute(&many_rows_program))
20240            .expect("multi-row execution should succeed");
20241        assert_eq!(outcome, ExecOutcome::Done);
20242        assert_eq!(
20243            engine.take_exactly_one_result_row(),
20244            ExactResultRowOutcome::MultipleRows,
20245        );
20246        assert_eq!(
20247            engine.result_buffer_capacity(),
20248            initial_capacity,
20249            "multiple-row drain should also preserve the reusable result buffer"
20250        );
20251    }
20252
20253    #[test]
20254    fn test_disabling_result_row_collection_still_clears_result_registers() {
20255        let mut b = ProgramBuilder::new();
20256        let end = b.emit_label();
20257        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20258        let r1 = b.alloc_reg();
20259        let r2 = b.alloc_reg();
20260        b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20261        b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20262        b.emit_op(Opcode::IntCopy, r1, r2, 0, P4::None, 0);
20263        b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20264        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20265        b.resolve_label(end);
20266        let program = b.finish().expect("program should build");
20267
20268        let mut engine = VdbeEngine::new(program.register_count());
20269        engine.set_collect_result_rows(false);
20270        let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20271        assert_eq!(outcome, ExecOutcome::Done);
20272        assert!(
20273            engine.results().is_empty(),
20274            "rowless execution should not retain ResultRow payloads"
20275        );
20276        assert_eq!(
20277            engine.get_reg(r1),
20278            &SqliteValue::Null,
20279            "discarded ResultRow should still clear its source registers"
20280        );
20281        assert_eq!(
20282            engine.get_reg(r2),
20283            &SqliteValue::Null,
20284            "later ResultRow opcodes should observe the same cleared-register semantics"
20285        );
20286    }
20287
20288    #[test]
20289    fn test_result_row_collection_cap_discards_rows_after_limit_and_clears_registers() {
20290        let mut b = ProgramBuilder::new();
20291        let end = b.emit_label();
20292        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20293        let r1 = b.alloc_reg();
20294        let r2 = b.alloc_reg();
20295        let r3 = b.alloc_reg();
20296        let r4 = b.alloc_reg();
20297        let r5 = b.alloc_reg();
20298        let r6 = b.alloc_reg();
20299        b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20300        b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20301        b.emit_op(Opcode::Integer, 8, r2, 0, P4::None, 0);
20302        b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20303        b.emit_op(Opcode::Integer, 9, r3, 0, P4::None, 0);
20304        b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
20305        b.emit_op(Opcode::IntCopy, r1, r4, 0, P4::None, 0);
20306        b.emit_op(Opcode::IntCopy, r2, r5, 0, P4::None, 0);
20307        b.emit_op(Opcode::IntCopy, r3, r6, 0, P4::None, 0);
20308        b.emit_op(Opcode::ResultRow, r4, 3, 0, P4::None, 0);
20309        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20310        b.resolve_label(end);
20311        let program = b.finish().expect("program should build");
20312
20313        let mut engine = VdbeEngine::new(program.register_count());
20314        engine.set_max_collected_result_rows(Some(2));
20315        let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20316        assert_eq!(outcome, ExecOutcome::Done);
20317        assert_eq!(
20318            engine
20319                .results()
20320                .iter()
20321                .map(|row| row.clone().into_vec())
20322                .collect::<Vec<_>>(),
20323            vec![vec![SqliteValue::Integer(7)], vec![SqliteValue::Integer(8)],],
20324            "rows after the retention cap should be discarded"
20325        );
20326        assert_eq!(engine.get_reg(r3), &SqliteValue::Null);
20327        assert_eq!(engine.get_reg(r4), &SqliteValue::Null);
20328        assert_eq!(engine.get_reg(r5), &SqliteValue::Null);
20329        assert_eq!(engine.get_reg(r6), &SqliteValue::Null);
20330    }
20331
20332    #[test]
20333    fn test_result_row_handler_streams_rows_without_retaining_results() {
20334        let mut b = ProgramBuilder::new();
20335        let end = b.emit_label();
20336        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20337        let r1 = b.alloc_reg();
20338        let r2 = b.alloc_reg();
20339        b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20340        b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20341        b.emit_op(Opcode::Integer, 8, r2, 0, P4::None, 0);
20342        b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20343        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20344        b.resolve_label(end);
20345        let program = b.finish().expect("program should build");
20346
20347        let mut engine = VdbeEngine::new(program.register_count());
20348        let mut streamed_rows = Vec::new();
20349        let outcome = run_async(engine.execute_with_borrowed_bindings_and_row_handler(
20350            &program,
20351            None,
20352            &mut |row| {
20353                streamed_rows.push(row.into_vec());
20354                Ok(())
20355            },
20356        ))
20357        .expect("execution should succeed");
20358
20359        assert_eq!(outcome, ExecOutcome::Done);
20360        assert_eq!(
20361            streamed_rows,
20362            vec![vec![SqliteValue::Integer(7)], vec![SqliteValue::Integer(8)],],
20363            "row handler should observe result rows in program order"
20364        );
20365        assert!(
20366            engine.results().is_empty(),
20367            "streaming callback should avoid retaining result rows in the engine buffer"
20368        );
20369        assert_eq!(engine.get_reg(r1), &SqliteValue::Null);
20370        assert_eq!(engine.get_reg(r2), &SqliteValue::Null);
20371    }
20372
20373    #[test]
20374    fn test_execute_swaps_shared_table_index_meta_per_program() {
20375        let first_program = {
20376            let mut b = ProgramBuilder::new();
20377            let end = b.emit_label();
20378            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20379            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20380            b.resolve_label(end);
20381            b.register_table_indexes(
20382                7,
20383                vec![IndexCursorMeta {
20384                    cursor_id: 8,
20385                    column_indices: vec![0, 2],
20386                }],
20387            );
20388            b.finish().expect("first program should build")
20389        };
20390        let second_program = {
20391            let mut b = ProgramBuilder::new();
20392            let end = b.emit_label();
20393            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20394            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20395            b.resolve_label(end);
20396            b.finish().expect("second program should build")
20397        };
20398
20399        let mut engine = VdbeEngine::new(
20400            first_program
20401                .register_count()
20402                .max(second_program.register_count()),
20403        );
20404        assert!(engine.table_index_meta.is_empty());
20405
20406        let first_outcome =
20407            run_async(engine.execute(&first_program)).expect("first execution should succeed");
20408        assert_eq!(first_outcome, ExecOutcome::Done);
20409        assert!(Arc::ptr_eq(
20410            &engine.table_index_meta,
20411            first_program.shared_table_index_meta()
20412        ));
20413        let first_meta = engine
20414            .table_index_meta
20415            .get(&7)
20416            .expect("first program metadata should be visible to the engine");
20417        assert_eq!(first_meta.len(), 1);
20418        assert_eq!(first_meta[0].cursor_id, 8);
20419        assert_eq!(first_meta[0].column_indices, vec![0, 2]);
20420
20421        let second_outcome =
20422            run_async(engine.execute(&second_program)).expect("second execution should succeed");
20423        assert_eq!(second_outcome, ExecOutcome::Done);
20424        assert!(Arc::ptr_eq(
20425            &engine.table_index_meta,
20426            second_program.shared_table_index_meta()
20427        ));
20428        assert!(
20429            engine.table_index_meta.is_empty(),
20430            "executing a program without REPLACE metadata must clear prior program metadata"
20431        );
20432    }
20433
20434    /// Build and execute a program with bound SQL parameters.
20435    fn run_program_with_bindings(
20436        build: impl FnOnce(&mut ProgramBuilder),
20437        bindings: Vec<SqliteValue>,
20438    ) -> Vec<Vec<SqliteValue>> {
20439        let mut b = ProgramBuilder::new();
20440        build(&mut b);
20441        let prog = b.finish().expect("program should build");
20442        let mut engine = VdbeEngine::new(prog.register_count());
20443        engine.set_bindings(bindings);
20444        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
20445        assert_eq!(outcome, ExecOutcome::Done);
20446        engine
20447            .take_results()
20448            .into_iter()
20449            .map(|v| v.into_vec())
20450            .collect()
20451    }
20452
20453    fn track_r_log_metrics(
20454        test_name: &str,
20455        alloc_count: usize,
20456        scratch_capacity_bytes: usize,
20457        encode_time_ns: u128,
20458    ) {
20459        eprintln!(
20460            "track_r test={test_name} alloc_count={alloc_count} scratch_capacity_bytes={scratch_capacity_bytes} encode_time_ns={encode_time_ns}"
20461        );
20462    }
20463
20464    fn track_r_values_bitwise_eq(left: &SqliteValue, right: &SqliteValue) -> bool {
20465        match (left, right) {
20466            (SqliteValue::Null, SqliteValue::Null) => true,
20467            (SqliteValue::Integer(lhs), SqliteValue::Integer(rhs)) => lhs == rhs,
20468            (SqliteValue::Float(lhs), SqliteValue::Float(rhs)) => lhs.to_bits() == rhs.to_bits(),
20469            (SqliteValue::Text(lhs), SqliteValue::Text(rhs)) => lhs == rhs,
20470            (SqliteValue::Blob(lhs), SqliteValue::Blob(rhs)) => lhs == rhs,
20471            _ => false,
20472        }
20473    }
20474
20475    fn track_r_assert_rows_eq(actual: &[SqliteValue], expected: &[SqliteValue], context: &str) {
20476        assert_eq!(
20477            actual.len(),
20478            expected.len(),
20479            "{context}: column count mismatch"
20480        );
20481        for (idx, (actual_value, expected_value)) in actual.iter().zip(expected.iter()).enumerate()
20482        {
20483            assert!(
20484                track_r_values_bitwise_eq(actual_value, expected_value),
20485                "{context}: mismatch at column {idx}: actual={actual_value:?} expected={expected_value:?}",
20486            );
20487        }
20488    }
20489
20490    fn track_r_serialized_value_layout(value: &SqliteValue) -> (u64, usize) {
20491        match value {
20492            SqliteValue::Null => (0, 0),
20493            SqliteValue::Integer(integer) => {
20494                let serial_type = serial_type_for_integer(*integer);
20495                let payload_len = match serial_type {
20496                    8 | 9 => 0,
20497                    1 => 1,
20498                    2 => 2,
20499                    3 => 3,
20500                    4 => 4,
20501                    5 => 6,
20502                    6 => 8,
20503                    _ => unreachable!("integer serial type must be in 1..=9"),
20504                };
20505                (serial_type, payload_len)
20506            }
20507            SqliteValue::Float(float) => {
20508                if float.is_nan() {
20509                    (0, 0)
20510                } else {
20511                    (7, 8)
20512                }
20513            }
20514            SqliteValue::Text(text) => {
20515                let len = text.len();
20516                (
20517                    serial_type_for_text(u64::try_from(len).unwrap_or(u64::MAX)),
20518                    len,
20519                )
20520            }
20521            SqliteValue::Blob(blob) => {
20522                let len = blob.len();
20523                (
20524                    serial_type_for_blob(u64::try_from(len).unwrap_or(u64::MAX)),
20525                    len,
20526                )
20527            }
20528        }
20529    }
20530
20531    fn track_r_compute_header_size(content_size: usize) -> usize {
20532        let mut header_size = content_size + 1;
20533        loop {
20534            let needed = varint_len(header_size as u64) + content_size;
20535            if needed <= header_size {
20536                return header_size;
20537            }
20538            header_size = needed;
20539        }
20540    }
20541
20542    fn track_r_encode_serialized_value(value: &SqliteValue, payload_len: usize, buf: &mut [u8]) {
20543        match value {
20544            SqliteValue::Null => {}
20545            SqliteValue::Integer(integer) => {
20546                if payload_len == 0 {
20547                    return;
20548                }
20549                let bytes = integer.to_be_bytes();
20550                buf.copy_from_slice(&bytes[8 - payload_len..]);
20551            }
20552            SqliteValue::Float(float) => {
20553                if float.is_nan() {
20554                    return;
20555                }
20556                buf.copy_from_slice(&float.to_bits().to_be_bytes());
20557            }
20558            SqliteValue::Text(text) => buf.copy_from_slice(text.as_bytes_direct()),
20559            SqliteValue::Blob(blob) => buf.copy_from_slice(blob),
20560        }
20561    }
20562
20563    fn track_r_reference_three_pass_record(values: &[SqliteValue]) -> Vec<u8> {
20564        let layouts: Vec<_> = values.iter().map(track_r_serialized_value_layout).collect();
20565        let header_content_size = layouts
20566            .iter()
20567            .map(|(serial_type, _)| varint_len(*serial_type))
20568            .sum::<usize>();
20569        let body_size = layouts
20570            .iter()
20571            .map(|(_, payload_len)| *payload_len)
20572            .sum::<usize>();
20573        let header_size = track_r_compute_header_size(header_content_size);
20574        let total_size = header_size + body_size;
20575        let mut buf = vec![0; total_size];
20576
20577        let mut header_offset = write_varint(
20578            buf.as_mut_slice(),
20579            u64::try_from(header_size).unwrap_or(u64::MAX),
20580        );
20581        let mut body_offset = header_size;
20582        for (value, (serial_type, payload_len)) in values.iter().zip(layouts.iter().copied()) {
20583            header_offset += write_varint(&mut buf[header_offset..], serial_type);
20584            track_r_encode_serialized_value(
20585                value,
20586                payload_len,
20587                &mut buf[body_offset..body_offset + payload_len],
20588            );
20589            body_offset += payload_len;
20590        }
20591
20592        assert_eq!(header_offset, header_size, "reference header width drifted");
20593        assert_eq!(body_offset, total_size, "reference body width drifted");
20594        buf
20595    }
20596
20597    fn track_r_arb_sqlite_value() -> BoxedStrategy<SqliteValue> {
20598        prop_oneof![
20599            4 => Just(SqliteValue::Null),
20600            8 => any::<i64>().prop_map(SqliteValue::Integer),
20601            4 => (-1.0e12_f64..1.0e12_f64).prop_map(SqliteValue::Float),
20602            4 => proptest::collection::vec(any::<char>(), 0..64).prop_map(|chars| {
20603                let text = chars.into_iter().collect::<String>();
20604                SqliteValue::Text(SmallText::from_string(text))
20605            }),
20606            4 => proptest::collection::vec(any::<u8>(), 0..96)
20607                .prop_map(|bytes| SqliteValue::Blob(Arc::<[u8]>::from(bytes))),
20608        ]
20609        .boxed()
20610    }
20611
20612    fn track_r_generated_record(seed: usize) -> Vec<SqliteValue> {
20613        let integer = i64::try_from(seed).unwrap_or(i64::MAX) - 5_000;
20614        let tail = match seed % 5 {
20615            0 => SqliteValue::Null,
20616            1 => SqliteValue::Integer(0),
20617            2 => SqliteValue::Integer(1),
20618            3 => SqliteValue::Integer(-1),
20619            _ => SqliteValue::Integer(i64::try_from(seed % 97).unwrap_or(0) - 48),
20620        };
20621        let text = match seed % 4 {
20622            0 => String::new(),
20623            1 => format!("row-{seed:05}"),
20624            2 => "x".repeat((seed % 32) + 1),
20625            _ => format!("record-{seed:05}-tail"),
20626        };
20627        let blob_len = match seed % 6 {
20628            0 => 0,
20629            1 => 1,
20630            2 => 7,
20631            3 => 19,
20632            4 => 33,
20633            _ => (seed % 48) + 1,
20634        };
20635        let blob = (0..blob_len)
20636            .map(|offset| {
20637                let byte = (seed.wrapping_mul(17)).wrapping_add(offset.wrapping_mul(29)) % 251;
20638                u8::try_from(byte).unwrap_or(0)
20639            })
20640            .collect::<Vec<_>>();
20641
20642        vec![
20643            SqliteValue::Integer(integer),
20644            SqliteValue::Float((seed as f64).mul_add(1.625, -777.25)),
20645            SqliteValue::Text(SmallText::from_string(text)),
20646            SqliteValue::Blob(Arc::<[u8]>::from(blob)),
20647            tail,
20648        ]
20649    }
20650
20651    fn track_r_large_scratch_record(iteration: usize) -> Vec<SqliteValue> {
20652        if iteration == 0 {
20653            return vec![
20654                SqliteValue::Integer(-9_223_372_036_854_775_000),
20655                SqliteValue::Text(SmallText::from_string("x".repeat(2_048))),
20656                SqliteValue::Blob(Arc::<[u8]>::from(vec![0xAB; 4_096])),
20657            ];
20658        }
20659
20660        vec![
20661            SqliteValue::Integer(i64::try_from(iteration).unwrap_or(i64::MAX)),
20662            SqliteValue::Text(SmallText::from_string(format!("row-{iteration:04}"))),
20663            SqliteValue::Blob(Arc::<[u8]>::from(vec![
20664                u8::try_from(iteration % 251)
20665                    .unwrap_or(0);
20666                (iteration % 24) + 1
20667            ])),
20668        ]
20669    }
20670
20671    fn track_r_make_record_op(source_reg: i32, column_count: usize, target_reg: i32) -> VdbeOp {
20672        VdbeOp {
20673            opcode: Opcode::MakeRecord,
20674            p1: source_reg,
20675            p2: i32::try_from(column_count).expect("column count should fit in i32"),
20676            p3: target_reg,
20677            p4: P4::None,
20678            p5: 0,
20679        }
20680    }
20681
20682    fn track_r_decode_make_record_buf(engine: &VdbeEngine) -> Vec<SqliteValue> {
20683        parse_record(engine.make_record_lookaside.as_slice())
20684            .expect("MakeRecord sideband should decode")
20685    }
20686
20687    fn track_r_emit_value(builder: &mut ProgramBuilder, reg: i32, value: &SqliteValue) {
20688        match value {
20689            SqliteValue::Null => {
20690                builder.emit_op(Opcode::Null, 0, reg, 0, P4::None, 0);
20691            }
20692            SqliteValue::Integer(integer) => {
20693                builder.emit_op(Opcode::Int64, 0, reg, 0, P4::Int64(*integer), 0);
20694            }
20695            SqliteValue::Float(float) => {
20696                builder.emit_op(Opcode::Real, 0, reg, 0, P4::Real(*float), 0);
20697            }
20698            SqliteValue::Text(text) => {
20699                builder.emit_op(
20700                    Opcode::String8,
20701                    0,
20702                    reg,
20703                    0,
20704                    P4::Str(text.as_str().to_owned()),
20705                    0,
20706                );
20707            }
20708            SqliteValue::Blob(blob) => {
20709                builder.emit_op(Opcode::Blob, 0, reg, 0, P4::Blob(blob.to_vec()), 0);
20710            }
20711        }
20712    }
20713
20714    fn track_r_run_write_with_memdb(
20715        db: MemDatabase,
20716        build: impl FnOnce(&mut ProgramBuilder),
20717    ) -> (Vec<Vec<SqliteValue>>, MemDatabase) {
20718        let mut builder = ProgramBuilder::new();
20719        build(&mut builder);
20720        let program = builder.finish().expect("program should build");
20721        let mut engine = VdbeEngine::new(program.register_count());
20722        engine.set_database(db);
20723        engine.set_reject_mem_fallback(false);
20724        let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20725        assert_eq!(outcome, ExecOutcome::Done);
20726        let results = engine
20727            .take_results()
20728            .into_iter()
20729            .map(|row| row.into_vec())
20730            .collect::<Vec<_>>();
20731        let final_db = engine.take_database().expect("database should exist");
20732        (results, final_db)
20733    }
20734
20735    fn track_r_to_rusqlite_value(value: &SqliteValue) -> RusqliteValue {
20736        match value {
20737            SqliteValue::Null => RusqliteValue::Null,
20738            SqliteValue::Integer(integer) => RusqliteValue::Integer(*integer),
20739            SqliteValue::Float(float) => RusqliteValue::Real(*float),
20740            SqliteValue::Text(text) => RusqliteValue::Text(text.as_str().to_owned()),
20741            SqliteValue::Blob(blob) => RusqliteValue::Blob(blob.to_vec()),
20742        }
20743    }
20744
20745    fn track_r_from_rusqlite_value(value: RusqliteValue) -> SqliteValue {
20746        match value {
20747            RusqliteValue::Null => SqliteValue::Null,
20748            RusqliteValue::Integer(integer) => SqliteValue::Integer(integer),
20749            RusqliteValue::Real(float) => SqliteValue::Float(float),
20750            RusqliteValue::Text(text) => SqliteValue::Text(SmallText::from_string(text)),
20751            RusqliteValue::Blob(blob) => SqliteValue::Blob(Arc::<[u8]>::from(blob)),
20752        }
20753    }
20754
20755    #[test]
20756    fn test_track_r_make_record_scratch_reuse_1000_records_no_realloc_after_first() {
20757        let mut engine = VdbeEngine::new(8);
20758        let op = track_r_make_record_op(1, 3, 4);
20759        let mut scratch_capacity = None;
20760        let mut alloc_count = 0usize;
20761        let start = Instant::now();
20762
20763        for iteration in 0..1_000 {
20764            let values = track_r_large_scratch_record(iteration);
20765            for (offset, value) in values.iter().enumerate() {
20766                let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20767                engine.set_reg(reg, value.clone());
20768            }
20769
20770            engine.execute_make_record_hot(&op, false);
20771
20772            let observed_capacity = engine.make_record_lookaside.capacity();
20773            if let Some(expected_capacity) = scratch_capacity {
20774                assert_eq!(
20775                    observed_capacity, expected_capacity,
20776                    "MakeRecord scratch reallocated after warmup at iteration {iteration}",
20777                );
20778            } else {
20779                assert!(
20780                    observed_capacity > 0,
20781                    "scratch capacity should grow on first encode"
20782                );
20783                scratch_capacity = Some(observed_capacity);
20784                alloc_count += 1;
20785            }
20786
20787            let decoded = track_r_decode_make_record_buf(&engine);
20788            track_r_assert_rows_eq(
20789                &decoded,
20790                &values,
20791                "track_r scratch reuse MakeRecord decode mismatch",
20792            );
20793        }
20794
20795        track_r_log_metrics(
20796            "test_track_r_make_record_scratch_reuse_1000_records_no_realloc_after_first",
20797            alloc_count,
20798            scratch_capacity.unwrap_or(0),
20799            start.elapsed().as_nanos(),
20800        );
20801    }
20802
20803    #[test]
20804    fn test_track_r_two_pass_matches_three_pass_proptest_10k_random() {
20805        let strategy = proptest::collection::vec(track_r_arb_sqlite_value(), 0..48);
20806        let mut runner = TestRunner::new(ProptestConfig {
20807            cases: 10_000,
20808            ..ProptestConfig::default()
20809        });
20810        let two_pass = std::cell::RefCell::new(Vec::new());
20811        let alloc_count = std::cell::Cell::new(0usize);
20812        let encode_time_ns = std::cell::Cell::new(0_u128);
20813        let scratch_capacity_bytes = std::cell::Cell::new(0usize);
20814
20815        runner
20816            .run(&strategy, |values| {
20817                let start = Instant::now();
20818                let mut two_pass_buf = two_pass.borrow_mut();
20819                serialize_record_iter_into(values.iter(), &mut two_pass_buf);
20820                let current_capacity = two_pass_buf.capacity();
20821                if current_capacity != scratch_capacity_bytes.get() {
20822                    alloc_count.set(alloc_count.get().saturating_add(1));
20823                    scratch_capacity_bytes.set(current_capacity);
20824                }
20825                let three_pass = track_r_reference_three_pass_record(&values);
20826                encode_time_ns.set(
20827                    encode_time_ns
20828                        .get()
20829                        .saturating_add(start.elapsed().as_nanos()),
20830                );
20831                if *two_pass_buf != three_pass {
20832                    return Err(TestCaseError::fail(format!(
20833                        "two-pass and three-pass record encoders diverged for values={values:?}"
20834                    )));
20835                }
20836                Ok(())
20837            })
20838            .expect("two-pass encoder should match the three-pass reference");
20839
20840        track_r_log_metrics(
20841            "test_track_r_two_pass_matches_three_pass_proptest_10k_random",
20842            alloc_count.get(),
20843            scratch_capacity_bytes
20844                .get()
20845                .max(two_pass.borrow().capacity()),
20846            encode_time_ns.get(),
20847        );
20848    }
20849
20850    #[test]
20851    fn test_track_r_make_record_all_sqlite_types() {
20852        let values = vec![
20853            SqliteValue::Null,
20854            SqliteValue::Integer(-9_223_372_036_854_775_000),
20855            SqliteValue::Float(-1234.5),
20856            SqliteValue::Text(SmallText::from_string("hello-track-r".to_owned())),
20857            SqliteValue::Blob(Arc::<[u8]>::from(vec![0x00, 0x01, 0xFE, 0xFF])),
20858        ];
20859        let mut engine = VdbeEngine::new(8);
20860        let op = track_r_make_record_op(1, values.len(), 7);
20861        let start = Instant::now();
20862
20863        for (offset, value) in values.iter().enumerate() {
20864            let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20865            engine.set_reg(reg, value.clone());
20866        }
20867        engine.execute_make_record_hot(&op, false);
20868
20869        let decoded = track_r_decode_make_record_buf(&engine);
20870        track_r_assert_rows_eq(
20871            &decoded,
20872            &values,
20873            "track_r all-types MakeRecord decode mismatch",
20874        );
20875        track_r_log_metrics(
20876            "test_track_r_make_record_all_sqlite_types",
20877            1,
20878            engine.make_record_lookaside.capacity(),
20879            start.elapsed().as_nanos(),
20880        );
20881    }
20882
20883    #[test]
20884    fn test_track_r_make_record_empty_record() {
20885        let mut engine = VdbeEngine::new(2);
20886        let op = track_r_make_record_op(1, 0, 1);
20887        let start = Instant::now();
20888
20889        engine.execute_make_record_hot(&op, false);
20890
20891        let decoded = track_r_decode_make_record_buf(&engine);
20892        assert!(
20893            decoded.is_empty(),
20894            "empty MakeRecord should decode to no columns"
20895        );
20896        track_r_log_metrics(
20897            "test_track_r_make_record_empty_record",
20898            1,
20899            engine.make_record_lookaside.capacity(),
20900            start.elapsed().as_nanos(),
20901        );
20902    }
20903
20904    #[test]
20905    fn test_track_r_make_record_max_columns() {
20906        let max_columns = usize::from(MAX_COLUMN);
20907        let mut engine = VdbeEngine::new(i32::try_from(max_columns + 2).expect("register count"));
20908        let op = track_r_make_record_op(1, max_columns, i32::try_from(max_columns + 1).unwrap());
20909        let expected = (0..max_columns)
20910            .map(|idx| SqliteValue::Integer(i64::try_from(idx).unwrap_or(i64::MAX) - 1_000))
20911            .collect::<Vec<_>>();
20912        let start = Instant::now();
20913
20914        for (offset, value) in expected.iter().enumerate() {
20915            let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20916            engine.set_reg(reg, value.clone());
20917        }
20918        engine.execute_make_record_hot(&op, false);
20919
20920        let decoded = track_r_decode_make_record_buf(&engine);
20921        track_r_assert_rows_eq(
20922            &decoded,
20923            &expected,
20924            "track_r max-column MakeRecord decode mismatch",
20925        );
20926        track_r_log_metrics(
20927            "test_track_r_make_record_max_columns",
20928            1,
20929            engine.make_record_lookaside.capacity(),
20930            start.elapsed().as_nanos(),
20931        );
20932    }
20933
20934    #[test]
20935    fn test_track_r_make_record_insert_10k_oracle() {
20936        let row_count = 10_000usize;
20937        let expected_rows = (0..row_count)
20938            .map(track_r_generated_record)
20939            .collect::<Vec<_>>();
20940        let mut db = MemDatabase::new();
20941        let root = db.create_table(5);
20942        let start = Instant::now();
20943
20944        let (observed_rows, _) = track_r_run_write_with_memdb(db, |b| {
20945            let end = b.emit_label();
20946            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20947            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(5), 0);
20948
20949            for (idx, row) in expected_rows.iter().enumerate() {
20950                let rowid = i64::try_from(idx + 1).expect("rowid should fit in i64");
20951                b.emit_op(Opcode::Int64, 0, 1, 0, P4::Int64(rowid), 0);
20952                for (offset, value) in row.iter().enumerate() {
20953                    let reg = i32::try_from(offset + 2).expect("register index should fit in i32");
20954                    track_r_emit_value(b, reg, value);
20955                }
20956                b.emit_op(Opcode::MakeRecord, 2, 5, 7, P4::None, 0);
20957                b.emit_op(Opcode::Insert, 0, 7, 1, P4::None, 0);
20958            }
20959
20960            let done = b.emit_label();
20961            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
20962            let body = b.emit_label();
20963            b.resolve_label(body);
20964            for column in 0_i32..5 {
20965                let target_reg = column + 1;
20966                b.emit_op(Opcode::Column, 0, column, target_reg, P4::None, 0);
20967            }
20968            b.emit_op(Opcode::ResultRow, 1, 5, 0, P4::None, 0);
20969            b.emit_jump_to_label(Opcode::Next, 0, 0, body, P4::None, 0);
20970
20971            b.resolve_label(done);
20972            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20973            b.resolve_label(end);
20974        });
20975
20976        assert_eq!(
20977            observed_rows.len(),
20978            row_count,
20979            "MakeRecord INSERT path should persist every generated row",
20980        );
20981
20982        let oracle = rusqlite::Connection::open_in_memory().expect("oracle should open");
20983        oracle
20984            .execute(
20985                "CREATE TABLE t (c0 INTEGER, c1 REAL, c2 TEXT, c3 BLOB, c4 NUMERIC)",
20986                [],
20987            )
20988            .expect("oracle schema should create");
20989        let mut insert = oracle
20990            .prepare("INSERT INTO t (c0, c1, c2, c3, c4) VALUES (?1, ?2, ?3, ?4, ?5)")
20991            .expect("oracle insert should prepare");
20992        for row in &expected_rows {
20993            let params = row
20994                .iter()
20995                .map(track_r_to_rusqlite_value)
20996                .collect::<Vec<_>>();
20997            insert
20998                .execute(params_from_iter(params))
20999                .expect("oracle insert should succeed");
21000        }
21001
21002        let mut query = oracle
21003            .prepare("SELECT c0, c1, c2, c3, c4 FROM t ORDER BY rowid")
21004            .expect("oracle query should prepare");
21005        let oracle_rows = query
21006            .query_map([], |row| {
21007                (0..5)
21008                    .map(|idx| {
21009                        row.get::<_, RusqliteValue>(idx)
21010                            .map(track_r_from_rusqlite_value)
21011                    })
21012                    .collect::<rusqlite::Result<Vec<_>>>()
21013            })
21014            .expect("oracle query should execute")
21015            .collect::<rusqlite::Result<Vec<_>>>()
21016            .expect("oracle rows should collect");
21017
21018        assert_eq!(
21019            observed_rows.len(),
21020            oracle_rows.len(),
21021            "VDBE and oracle should expose the same row count",
21022        );
21023        for (idx, (observed, oracle_row)) in
21024            observed_rows.iter().zip(oracle_rows.iter()).enumerate()
21025        {
21026            track_r_assert_rows_eq(
21027                observed,
21028                oracle_row,
21029                &format!("track_r 10k insert oracle row {idx} mismatch"),
21030            );
21031        }
21032
21033        track_r_log_metrics(
21034            "test_track_r_make_record_insert_10k_oracle",
21035            1,
21036            0,
21037            start.elapsed().as_nanos(),
21038        );
21039    }
21040
21041    #[test]
21042    fn test_track_r_make_record_roundtrip_10k_records() {
21043        let row_count = 10_000usize;
21044        let mut encoded = Vec::new();
21045        let mut decoded = Vec::new();
21046        let mut alloc_count = 0usize;
21047        let mut encoded_capacity = 0usize;
21048        let start = Instant::now();
21049
21050        for seed in 0..row_count {
21051            let values = track_r_generated_record(seed);
21052            serialize_record_iter_into(values.iter(), &mut encoded);
21053            parse_record_into(&encoded, &mut decoded).expect("roundtrip decode should succeed");
21054            track_r_assert_rows_eq(&decoded, &values, "track_r roundtrip decode mismatch");
21055
21056            let current_capacity = encoded.capacity();
21057            if current_capacity != encoded_capacity {
21058                encoded_capacity = current_capacity;
21059                alloc_count = alloc_count.saturating_add(1);
21060            }
21061        }
21062
21063        track_r_log_metrics(
21064            "test_track_r_make_record_roundtrip_10k_records",
21065            alloc_count,
21066            encoded_capacity.max(encoded.capacity()),
21067            start.elapsed().as_nanos(),
21068        );
21069    }
21070
21071    #[test]
21072    fn test_set_bindings_slice_keeps_small_binding_sets_inline() {
21073        let mut engine = VdbeEngine::new(1);
21074        engine.set_bindings_slice(&[SqliteValue::Integer(7)]);
21075
21076        assert_eq!(engine.bindings.len(), 1);
21077        assert!(
21078            !engine.bindings.spilled(),
21079            "single-parameter statements should keep bindings inline"
21080        );
21081        assert_eq!(engine.bindings[0], SqliteValue::Integer(7));
21082    }
21083
21084    #[test]
21085    fn test_execute_with_borrowed_bindings_uses_override_for_single_run() {
21086        let mut builder = ProgramBuilder::new();
21087        builder.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
21088        builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21089        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21090        let program = builder.finish().expect("program should build");
21091
21092        let mut engine = VdbeEngine::new(program.register_count());
21093        engine.set_bindings_slice(&[SqliteValue::Integer(7)]);
21094
21095        let outcome = run_async(
21096            engine.execute_with_borrowed_bindings(&program, Some(&[SqliteValue::Integer(11)])),
21097        )
21098        .expect("execution should succeed");
21099
21100        assert_eq!(outcome, ExecOutcome::Done);
21101        assert_eq!(
21102            engine.take_results().into_iter().next().unwrap().into_vec(),
21103            vec![SqliteValue::Integer(11)]
21104        );
21105        assert_eq!(
21106            engine.bindings[0],
21107            SqliteValue::Integer(7),
21108            "borrowed bindings should not overwrite the cached owned binding set"
21109        );
21110    }
21111
21112    #[test]
21113    fn test_register_value_i64_store_and_retrieve() {
21114        let mut engine = VdbeEngine::new(2);
21115        engine.set_reg_int(1, -9_876_543_210);
21116
21117        assert_eq!(engine.get_reg(1), &SqliteValue::Integer(-9_876_543_210));
21118        engine.set_register_subtype(1, 74);
21119        engine.set_reg_int(1, 9_876_543_210);
21120
21121        assert_eq!(engine.get_reg(1), &SqliteValue::Integer(9_876_543_210));
21122        assert!(
21123            engine.register_subtype(1).is_none(),
21124            "an in-place integer write must clear stale subtype metadata"
21125        );
21126        assert_eq!(engine.take_reg(1), SqliteValue::Integer(9_876_543_210));
21127        assert_eq!(engine.get_reg(1), &SqliteValue::Null);
21128    }
21129
21130    #[test]
21131    fn test_register_value_f64_store_and_retrieve() {
21132        let mut engine = VdbeEngine::new(2);
21133        engine.set_reg_real(1, 1.25);
21134
21135        assert_eq!(engine.get_reg(1), &SqliteValue::Float(1.25));
21136        engine.set_register_subtype(1, 74);
21137        engine.set_reg_real(1, -0.0);
21138
21139        let SqliteValue::Float(value) = engine.get_reg(1) else {
21140            panic!("register should contain a float");
21141        };
21142        assert_eq!(value.to_bits(), (-0.0_f64).to_bits());
21143        assert!(
21144            engine.register_subtype(1).is_none(),
21145            "an in-place real write must clear stale subtype metadata"
21146        );
21147
21148        engine.set_reg_real(1, f64::NAN);
21149        assert_eq!(engine.get_reg(1), &SqliteValue::Null);
21150    }
21151
21152    #[test]
21153    fn test_register_text_cache_stays_valid_while_storage_cursor_row_is_pinned() {
21154        let _guard = VDBE_OBSERVABILITY_LOCK
21155            .lock()
21156            .unwrap_or_else(|e| e.into_inner());
21157        let prev_metrics_enabled = vdbe_metrics_enabled();
21158        reset_vdbe_metrics();
21159        set_vdbe_metrics_enabled(true);
21160
21161        let mut db = MemDatabase::new();
21162        let root = db.create_table(1);
21163        db.get_table_mut(root)
21164            .expect("table should exist")
21165            .insert(1, vec![SqliteValue::Text("alpha-track-s".into())]);
21166
21167        let mut engine = VdbeEngine::new(2);
21168        engine.collect_vdbe_metrics = true;
21169        engine.enable_storage_cursors(true);
21170        engine.set_database(db);
21171        engine.set_reject_mem_fallback(false);
21172        assert!(run_async(engine.open_storage_cursor(0, root, false)));
21173        engine.cursor_root_pages.insert(0, root);
21174        {
21175            let sc = engine
21176                .storage_cursors
21177                .get_mut(&0)
21178                .expect("storage cursor should exist");
21179            assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21180        }
21181
21182        let before = vdbe_metrics_snapshot();
21183        let first =
21184            run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed");
21185        let second =
21186            run_async(engine.cursor_column(0, 0)).expect("pinned-row cache hit should succeed");
21187        let after = vdbe_metrics_snapshot();
21188        let sc = engine
21189            .storage_cursors
21190            .get(&0)
21191            .expect("storage cursor should exist");
21192
21193        assert_eq!(first, SqliteValue::Text("alpha-track-s".into()));
21194        assert_eq!(second, first);
21195        assert!(sc.row_decode.cached_value_ready(0));
21196        assert_eq!(
21197            sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21198            Some("alpha-track-s")
21199        );
21200        assert_eq!(
21201            after.decode_cache_misses_total - before.decode_cache_misses_total,
21202            1
21203        );
21204        assert_eq!(
21205            after.decode_cache_hits_total - before.decode_cache_hits_total,
21206            1
21207        );
21208        assert_eq!(
21209            after.decode_cache_invalidations_position_total
21210                - before.decode_cache_invalidations_position_total,
21211            0
21212        );
21213        assert_eq!(
21214            after.decode_cache_invalidations_write_total
21215                - before.decode_cache_invalidations_write_total,
21216            0
21217        );
21218
21219        set_vdbe_metrics_enabled(prev_metrics_enabled);
21220    }
21221
21222    #[test]
21223    fn test_register_value_make_record_sideband_materializes_into_sqlite_blob_value() {
21224        let _guard = VDBE_OBSERVABILITY_LOCK
21225            .lock()
21226            .unwrap_or_else(|e| e.into_inner());
21227        reset_vdbe_test_sideband_materialization_count();
21228
21229        let mut engine = VdbeEngine::new(4);
21230        engine.set_reg_int(1, 7);
21231        engine.set_reg(2, SqliteValue::Text("payload-track-s".into()));
21232
21233        let make_record = VdbeOp {
21234            opcode: Opcode::MakeRecord,
21235            p1: 1,
21236            p2: 2,
21237            p3: 3,
21238            p4: P4::None,
21239            p5: 0,
21240        };
21241        engine.execute_make_record_hot(&make_record, false);
21242
21243        assert_eq!(
21244            engine.get_reg(3),
21245            &SqliteValue::Null,
21246            "MakeRecord keeps bytes in sideband until a consumer asks for an owned SqliteValue"
21247        );
21248
21249        let before = vdbe_test_sideband_materialization_count_snapshot();
21250        let converted = engine.clone_reg_materialized(3);
21251        let after = vdbe_test_sideband_materialization_count_snapshot();
21252
21253        assert_eq!(after - before, 1);
21254        assert_eq!(
21255            decode_record(&converted).expect("sideband value should decode"),
21256            vec![
21257                SqliteValue::Integer(7),
21258                SqliteValue::Text("payload-track-s".into()),
21259            ]
21260        );
21261        assert_eq!(
21262            decode_record(engine.get_reg(3)).expect("register should now hold the blob"),
21263            vec![
21264                SqliteValue::Integer(7),
21265                SqliteValue::Text("payload-track-s".into()),
21266            ]
21267        );
21268    }
21269
21270    #[test]
21271    fn test_copy_materializes_make_record_sideband_before_copying() {
21272        let _guard = VDBE_OBSERVABILITY_LOCK
21273            .lock()
21274            .unwrap_or_else(|e| e.into_inner());
21275        reset_vdbe_test_sideband_materialization_count();
21276
21277        let rows = run_program(|b| {
21278            let end = b.emit_label();
21279            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21280            b.emit_op(Opcode::Integer, 33, 1, 0, P4::None, 0);
21281            b.emit_op(
21282                Opcode::String8,
21283                0,
21284                2,
21285                0,
21286                P4::Str("copy-sideband".to_owned()),
21287                0,
21288            );
21289            b.emit_op(Opcode::MakeRecord, 1, 2, 3, P4::None, 0);
21290            b.emit_op(Opcode::Copy, 3, 4, 0, P4::None, 0);
21291            b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
21292            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21293            b.resolve_label(end);
21294        });
21295
21296        assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 1);
21297        assert_eq!(
21298            decode_record(&rows[0][0]).expect("copied record blob should decode"),
21299            vec![
21300                SqliteValue::Integer(33),
21301                SqliteValue::Text("copy-sideband".into()),
21302            ]
21303        );
21304    }
21305
21306    #[test]
21307    fn test_scopy_materializes_make_record_sideband_before_copying() {
21308        let _guard = VDBE_OBSERVABILITY_LOCK
21309            .lock()
21310            .unwrap_or_else(|e| e.into_inner());
21311        reset_vdbe_test_sideband_materialization_count();
21312
21313        let rows = run_program(|b| {
21314            let end = b.emit_label();
21315            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21316            b.emit_op(Opcode::Integer, 44, 1, 0, P4::None, 0);
21317            b.emit_op(
21318                Opcode::String8,
21319                0,
21320                2,
21321                0,
21322                P4::Str("scopy-sideband".to_owned()),
21323                0,
21324            );
21325            b.emit_op(Opcode::MakeRecord, 1, 2, 3, P4::None, 0);
21326            b.emit_op(Opcode::SCopy, 3, 4, 0, P4::None, 0);
21327            b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
21328            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21329            b.resolve_label(end);
21330        });
21331
21332        assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 1);
21333        assert_eq!(
21334            decode_record(&rows[0][0]).expect("shallow-copied record blob should decode"),
21335            vec![
21336                SqliteValue::Integer(44),
21337                SqliteValue::Text("scopy-sideband".into()),
21338            ]
21339        );
21340    }
21341
21342    #[test]
21343    fn test_second_make_record_preserves_unconsumed_prior_sideband() {
21344        let _guard = VDBE_OBSERVABILITY_LOCK
21345            .lock()
21346            .unwrap_or_else(|e| e.into_inner());
21347        reset_vdbe_test_sideband_materialization_count();
21348
21349        let rows = run_program(|b| {
21350            let end = b.emit_label();
21351            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21352            let r_first_value = b.alloc_reg();
21353            let r_first_record = b.alloc_reg();
21354            let r_second_value = b.alloc_reg();
21355            let r_second_record = b.alloc_reg();
21356            b.emit_op(Opcode::Integer, 11, r_first_value, 0, P4::None, 0);
21357            b.emit_op(
21358                Opcode::MakeRecord,
21359                r_first_value,
21360                1,
21361                r_first_record,
21362                P4::None,
21363                0,
21364            );
21365            b.emit_op(Opcode::Integer, 22, r_second_value, 0, P4::None, 0);
21366            b.emit_op(
21367                Opcode::MakeRecord,
21368                r_second_value,
21369                1,
21370                r_second_record,
21371                P4::None,
21372                0,
21373            );
21374            b.emit_op(Opcode::ResultRow, r_first_record, 1, 0, P4::None, 0);
21375            b.emit_op(Opcode::ResultRow, r_second_record, 1, 0, P4::None, 0);
21376            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21377            b.resolve_label(end);
21378        });
21379
21380        assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 2);
21381        assert_eq!(
21382            decode_record(&rows[0][0]).expect("first pending record should decode"),
21383            vec![SqliteValue::Integer(11)]
21384        );
21385        assert_eq!(
21386            decode_record(&rows[1][0]).expect("second pending record should decode"),
21387            vec![SqliteValue::Integer(22)]
21388        );
21389    }
21390
21391    #[test]
21392    fn test_make_record_materializes_sideband_when_source_overlaps_target() {
21393        let _guard = VDBE_OBSERVABILITY_LOCK
21394            .lock()
21395            .unwrap_or_else(|e| e.into_inner());
21396        reset_vdbe_test_sideband_materialization_count();
21397
21398        let rows = run_program(|b| {
21399            let end = b.emit_label();
21400            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21401            let r_value = b.alloc_reg();
21402            let r_record = b.alloc_reg();
21403            b.emit_op(Opcode::Integer, 33, r_value, 0, P4::None, 0);
21404            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
21405            b.emit_op(Opcode::MakeRecord, r_record, 1, r_record, P4::None, 0);
21406            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
21407            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21408            b.resolve_label(end);
21409        });
21410
21411        assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 2);
21412        let outer = decode_record(&rows[0][0]).expect("outer record should decode");
21413        assert_eq!(outer.len(), 1);
21414        let inner = decode_record(&outer[0]).expect("nested record should decode");
21415        assert_eq!(inner, vec![SqliteValue::Integer(33)]);
21416    }
21417
21418    #[test]
21419    fn test_copy_range_uses_source_snapshot_when_ranges_overlap() {
21420        let rows = run_program(|b| {
21421            let end = b.emit_label();
21422            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21423            b.emit_op(Opcode::Integer, 10, 1, 0, P4::None, 0);
21424            b.emit_op(Opcode::Integer, 20, 2, 0, P4::None, 0);
21425            b.emit_op(Opcode::Integer, 30, 3, 0, P4::None, 0);
21426            b.emit_op(Opcode::Copy, 1, 2, 2, P4::None, 0);
21427            b.emit_op(Opcode::ResultRow, 1, 4, 0, P4::None, 0);
21428            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21429            b.resolve_label(end);
21430        });
21431
21432        assert_eq!(
21433            rows[0],
21434            vec![
21435                SqliteValue::Integer(10),
21436                SqliteValue::Integer(10),
21437                SqliteValue::Integer(20),
21438                SqliteValue::Integer(30),
21439            ]
21440        );
21441    }
21442
21443    #[test]
21444    fn test_register_value_take_reg_materializes_sideband_once_and_clears_slot() {
21445        let _guard = VDBE_OBSERVABILITY_LOCK
21446            .lock()
21447            .unwrap_or_else(|e| e.into_inner());
21448        reset_vdbe_test_sideband_materialization_count();
21449
21450        let mut engine = VdbeEngine::new(4);
21451        engine.set_reg_int(1, 17);
21452        engine.set_reg(2, SqliteValue::Text("take-track-s".into()));
21453
21454        let make_record = VdbeOp {
21455            opcode: Opcode::MakeRecord,
21456            p1: 1,
21457            p2: 2,
21458            p3: 3,
21459            p4: P4::None,
21460            p5: 0,
21461        };
21462        engine.execute_make_record_hot(&make_record, false);
21463
21464        let before = vdbe_test_sideband_materialization_count_snapshot();
21465        let taken = engine.take_reg(3);
21466        let after = vdbe_test_sideband_materialization_count_snapshot();
21467
21468        assert_eq!(
21469            after - before,
21470            1,
21471            "taking a sideband-backed register should materialize it exactly once"
21472        );
21473        assert_eq!(
21474            decode_record(&taken).expect("taken register value should decode"),
21475            vec![
21476                SqliteValue::Integer(17),
21477                SqliteValue::Text("take-track-s".into()),
21478            ]
21479        );
21480        assert_eq!(
21481            engine.get_reg(3),
21482            &SqliteValue::Null,
21483            "take_reg must clear the register slot after moving the value out"
21484        );
21485        assert_eq!(
21486            engine.take_reg(3),
21487            SqliteValue::Null,
21488            "subsequent reads after take_reg should observe the cleared slot"
21489        );
21490        assert_eq!(
21491            vdbe_test_sideband_materialization_count_snapshot(),
21492            after,
21493            "cleared register should not trigger a second sideband materialization"
21494        );
21495    }
21496
21497    #[test]
21498    fn test_register_value_insert_avoids_make_record_blob_materialization() {
21499        let _guard = VDBE_OBSERVABILITY_LOCK
21500            .lock()
21501            .unwrap_or_else(|e| e.into_inner());
21502        reset_vdbe_test_sideband_materialization_count();
21503
21504        let mut db = MemDatabase::new();
21505        let root = db.create_table(2);
21506        let before = vdbe_test_sideband_materialization_count_snapshot();
21507        let (rows, _) = run_write_with_storage_cursors(db, |b| {
21508            let end = b.emit_label();
21509            let done = b.emit_label();
21510            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21511            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
21512            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
21513            b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
21514            b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("alpha".to_owned()), 0);
21515            b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
21516            b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
21517            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
21518
21519            let body = b.current_addr();
21520            b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
21521            b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
21522            b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
21523
21524            let next_target =
21525                i32::try_from(body).expect("program counter should fit into i32 for tests");
21526            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
21527            b.resolve_label(done);
21528            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21529            b.resolve_label(end);
21530        });
21531        let after = vdbe_test_sideband_materialization_count_snapshot();
21532
21533        assert_eq!(
21534            rows,
21535            vec![vec![
21536                SqliteValue::Integer(42),
21537                SqliteValue::Text("alpha".into()),
21538            ]]
21539        );
21540        assert_eq!(
21541            after - before,
21542            0,
21543            "INSERT should consume MakeRecord sideband bytes directly instead of materializing an Arc-backed record blob"
21544        );
21545    }
21546
21547    #[test]
21548    fn test_register_value_cursor_move_invalidates_cached_text() {
21549        let _guard = VDBE_OBSERVABILITY_LOCK
21550            .lock()
21551            .unwrap_or_else(|e| e.into_inner());
21552        let prev_metrics_enabled = vdbe_metrics_enabled();
21553        reset_vdbe_metrics();
21554        set_vdbe_metrics_enabled(true);
21555
21556        let mut db = MemDatabase::new();
21557        let root = db.create_table(1);
21558        let table = db.get_table_mut(root).expect("table should exist");
21559        table.insert(1, vec![SqliteValue::Text("alpha-track-s".into())]);
21560        table.insert(2, vec![SqliteValue::Text("beta-track-s".into())]);
21561
21562        let mut engine = VdbeEngine::new(2);
21563        engine.collect_vdbe_metrics = true;
21564        engine.enable_storage_cursors(true);
21565        engine.set_database(db);
21566        engine.set_reject_mem_fallback(false);
21567        assert!(run_async(engine.open_storage_cursor(0, root, false)));
21568        engine.cursor_root_pages.insert(0, root);
21569        {
21570            let sc = engine
21571                .storage_cursors
21572                .get_mut(&0)
21573                .expect("storage cursor should exist");
21574            assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21575        }
21576
21577        let before = vdbe_metrics_snapshot();
21578        assert_eq!(
21579            run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed"),
21580            SqliteValue::Text("alpha-track-s".into())
21581        );
21582        {
21583            let sc = engine
21584                .storage_cursors
21585                .get_mut(&0)
21586                .expect("storage cursor should exist");
21587            assert!(run_async(sc.cursor.next(&sc.cx)).expect("cursor should advance"));
21588        }
21589        assert_eq!(
21590            run_async(engine.cursor_column(0, 0)).expect("post-move text decode should succeed"),
21591            SqliteValue::Text("beta-track-s".into())
21592        );
21593        let after = vdbe_metrics_snapshot();
21594        let sc = engine
21595            .storage_cursors
21596            .get(&0)
21597            .expect("storage cursor should exist");
21598
21599        assert!(sc.row_decode.cached_value_ready(0));
21600        assert_eq!(
21601            sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21602            Some("beta-track-s")
21603        );
21604        assert_eq!(
21605            after.decode_cache_misses_total - before.decode_cache_misses_total,
21606            2
21607        );
21608        assert_eq!(
21609            after.decode_cache_hits_total - before.decode_cache_hits_total,
21610            0
21611        );
21612        assert_eq!(
21613            after.decode_cache_invalidations_position_total
21614                - before.decode_cache_invalidations_position_total,
21615            1
21616        );
21617        assert_eq!(
21618            after.decode_cache_invalidations_write_total
21619                - before.decode_cache_invalidations_write_total,
21620            0
21621        );
21622        assert_eq!(
21623            after.decode_cache_invalidations_pseudo_total
21624                - before.decode_cache_invalidations_pseudo_total,
21625            0
21626        );
21627
21628        set_vdbe_metrics_enabled(prev_metrics_enabled);
21629    }
21630
21631    #[test]
21632    fn test_storage_cursor_same_slot_write_mutation_invalidates_cached_text() {
21633        let _guard = VDBE_OBSERVABILITY_LOCK
21634            .lock()
21635            .unwrap_or_else(|e| e.into_inner());
21636        let prev_metrics_enabled = vdbe_metrics_enabled();
21637        reset_vdbe_metrics();
21638        set_vdbe_metrics_enabled(true);
21639
21640        let mut db = MemDatabase::new();
21641        let root = db.create_table(1);
21642        let table = db.get_table_mut(root).expect("table should exist");
21643        table.insert(1, vec![SqliteValue::Text("alpha-track-j".into())]);
21644        table.insert(2, vec![SqliteValue::Text("beta-track-j".into())]);
21645
21646        let mut engine = VdbeEngine::new(2);
21647        engine.collect_vdbe_metrics = true;
21648        engine.enable_storage_cursors(true);
21649        engine.set_database(db);
21650        engine.set_reject_mem_fallback(false);
21651        assert!(run_async(engine.open_storage_cursor(0, root, true)));
21652        engine.cursor_root_pages.insert(0, root);
21653        {
21654            let sc = engine
21655                .storage_cursors
21656                .get_mut(&0)
21657                .expect("storage cursor should exist");
21658            assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21659        }
21660
21661        let before = vdbe_metrics_snapshot();
21662        assert_eq!(
21663            run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed"),
21664            SqliteValue::Text("alpha-track-j".into())
21665        );
21666        {
21667            let sc = engine
21668                .storage_cursors
21669                .get_mut(&0)
21670                .expect("storage cursor should exist");
21671            run_async(sc.cursor.delete(&sc.cx))
21672                .expect("direct delete should succeed without helper invalidation");
21673        }
21674        assert_eq!(
21675            run_async(engine.cursor_column(0, 0))
21676                .expect("same-slot successor should force a fresh decode"),
21677            SqliteValue::Text("beta-track-j".into())
21678        );
21679        let after = vdbe_metrics_snapshot();
21680        let sc = engine
21681            .storage_cursors
21682            .get(&0)
21683            .expect("storage cursor should exist");
21684
21685        assert!(sc.row_decode.cached_value_ready(0));
21686        assert_eq!(
21687            sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21688            Some("beta-track-j")
21689        );
21690        assert_eq!(
21691            after.decode_cache_invalidations_write_total
21692                - before.decode_cache_invalidations_write_total,
21693            1
21694        );
21695        assert_eq!(
21696            after.decode_cache_invalidations_position_total
21697                - before.decode_cache_invalidations_position_total,
21698            0
21699        );
21700        assert_eq!(
21701            after.decode_cache_hits_total - before.decode_cache_hits_total,
21702            0
21703        );
21704        assert_eq!(
21705            after.decode_cache_misses_total - before.decode_cache_misses_total,
21706            2
21707        );
21708
21709        set_vdbe_metrics_enabled(prev_metrics_enabled);
21710    }
21711
21712    #[test]
21713    fn test_execute_honors_cancelled_execution_context() {
21714        let mut builder = ProgramBuilder::new();
21715        for _ in 0..=VDBE_EXECUTION_CHECKPOINT_INTERVAL {
21716            builder.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
21717        }
21718        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21719        let program = builder.finish().expect("program should build");
21720
21721        let cx = Cx::new();
21722        cx.transition_to_running();
21723        cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
21724
21725        let mut engine =
21726            VdbeEngine::new_with_execution_cx(program.register_count(), &cx, PageSize::DEFAULT);
21727        let err = run_async(engine.execute(&program)).unwrap_err();
21728        assert!(matches!(err, FrankenError::Abort));
21729        assert!(
21730            engine.results().is_empty(),
21731            "cancelled execute should not emit rows"
21732        );
21733    }
21734
21735    #[test]
21736    fn test_execute_reuse_clears_prior_results() {
21737        let mut first_builder = ProgramBuilder::new();
21738        first_builder.emit_op(Opcode::Integer, 11, 1, 0, P4::None, 0);
21739        first_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21740        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21741        let first_program = first_builder.finish().expect("first program should build");
21742
21743        let mut second_builder = ProgramBuilder::new();
21744        second_builder.emit_op(Opcode::Integer, 22, 1, 0, P4::None, 0);
21745        second_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21746        second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21747        let second_program = second_builder
21748            .finish()
21749            .expect("second program should build");
21750
21751        let mut engine = VdbeEngine::new(
21752            first_program
21753                .register_count()
21754                .max(second_program.register_count()),
21755        );
21756        assert_eq!(
21757            run_async(engine.execute(&first_program)).expect("first execution"),
21758            ExecOutcome::Done
21759        );
21760        assert_eq!(
21761            run_async(engine.execute(&second_program)).expect("second execution"),
21762            ExecOutcome::Done
21763        );
21764
21765        let results = engine
21766            .results()
21767            .iter()
21768            .map(|row| row.clone().into_vec())
21769            .collect::<Vec<_>>();
21770        assert_eq!(results, vec![vec![SqliteValue::Integer(22)]]);
21771    }
21772
21773    #[test]
21774    fn test_execute_reuse_resets_statement_accounting() {
21775        let mut db = MemDatabase::new();
21776        let root = db.create_table(1);
21777
21778        let mut insert_builder = ProgramBuilder::new();
21779        let insert_end = insert_builder.emit_label();
21780        insert_builder.emit_jump_to_label(Opcode::Init, 0, 0, insert_end, P4::None, 0);
21781        insert_builder.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
21782        insert_builder.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
21783        insert_builder.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
21784        insert_builder.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
21785        insert_builder.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
21786        insert_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21787        insert_builder.resolve_label(insert_end);
21788        let insert_program = insert_builder
21789            .finish()
21790            .expect("insert program should build");
21791
21792        let mut noop_builder = ProgramBuilder::new();
21793        noop_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21794        let noop_program = noop_builder.finish().expect("noop program should build");
21795
21796        let mut engine = VdbeEngine::new(
21797            insert_program
21798                .register_count()
21799                .max(noop_program.register_count()),
21800        );
21801        engine.set_database(db);
21802        engine.set_reject_mem_fallback(false);
21803
21804        assert_eq!(
21805            run_async(engine.execute(&insert_program)).expect("insert execution"),
21806            ExecOutcome::Done
21807        );
21808        assert_eq!(engine.changes(), 1);
21809        assert_eq!(engine.last_insert_rowid(), Some(1));
21810
21811        assert_eq!(
21812            run_async(engine.execute(&noop_program)).expect("noop execution"),
21813            ExecOutcome::Done
21814        );
21815        assert_eq!(engine.changes(), 0);
21816        assert_eq!(engine.last_insert_rowid(), None);
21817    }
21818
21819    #[test]
21820    fn test_execute_reuse_empty_program_clears_prior_statement_state() {
21821        let mut first_builder = ProgramBuilder::new();
21822        first_builder.emit_op(Opcode::Integer, 11, 1, 0, P4::None, 0);
21823        first_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21824        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21825        let first_program = first_builder.finish().expect("first program should build");
21826
21827        let empty_program = ProgramBuilder::new()
21828            .finish()
21829            .expect("empty program should build");
21830
21831        let mut engine = VdbeEngine::new(
21832            first_program
21833                .register_count()
21834                .max(empty_program.register_count()),
21835        );
21836        assert_eq!(
21837            run_async(engine.execute(&first_program)).expect("first execution"),
21838            ExecOutcome::Done
21839        );
21840        assert_eq!(engine.results().len(), 1);
21841
21842        assert_eq!(
21843            run_async(engine.execute(&empty_program)).expect("empty execution"),
21844            ExecOutcome::Done
21845        );
21846        assert!(engine.results().is_empty());
21847        assert_eq!(engine.changes(), 0);
21848        assert_eq!(engine.last_insert_rowid(), None);
21849    }
21850
21851    #[test]
21852    fn test_reset_for_reuse_keeps_cached_engine_results_clean() {
21853        let mut first_builder = ProgramBuilder::new();
21854        let first_reg = first_builder.alloc_reg();
21855        first_builder.emit_op(Opcode::Integer, 11, first_reg, 0, P4::None, 0);
21856        first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21857        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21858        let first_program = first_builder.finish().expect("first program should build");
21859
21860        let mut second_builder = ProgramBuilder::new();
21861        let second_reg = second_builder.alloc_reg();
21862        second_builder.emit_op(Opcode::Integer, 22, second_reg, 0, P4::None, 0);
21863        second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21864        second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21865        let second_program = second_builder
21866            .finish()
21867            .expect("second program should build");
21868
21869        let mut engine = VdbeEngine::new(
21870            first_program
21871                .register_count()
21872                .max(second_program.register_count()),
21873        );
21874        assert_eq!(
21875            run_async(engine.execute(&first_program)).expect("first execution"),
21876            ExecOutcome::Done
21877        );
21878
21879        let reset_cx = Cx::new();
21880        engine.reset_for_reuse(
21881            first_program
21882                .register_count()
21883                .max(second_program.register_count()),
21884            &reset_cx,
21885            PageSize::DEFAULT,
21886        );
21887
21888        assert_eq!(
21889            run_async(engine.execute(&second_program)).expect("second execution"),
21890            ExecOutcome::Done
21891        );
21892        assert_eq!(
21893            engine
21894                .results()
21895                .iter()
21896                .map(|row| row.clone().into_vec())
21897                .collect::<Vec<_>>(),
21898            vec![vec![SqliteValue::Integer(22)]]
21899        );
21900    }
21901
21902    #[test]
21903    fn test_reset_for_reuse_preserves_engine_trace_setting() {
21904        let reset_cx = Cx::new();
21905        let mut engine = VdbeEngine::new_with_execution_cx(4, &reset_cx, PageSize::DEFAULT);
21906
21907        engine.trace_opcodes = false;
21908        engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
21909        assert!(!engine.trace_opcodes);
21910
21911        engine.trace_opcodes = true;
21912        engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
21913        assert!(engine.trace_opcodes);
21914    }
21915
21916    #[test]
21917    fn test_reset_for_reuse_clears_result_row_collection_cap() {
21918        let mut first_builder = ProgramBuilder::new();
21919        let first_reg = first_builder.alloc_reg();
21920        first_builder.emit_op(Opcode::Integer, 11, first_reg, 0, P4::None, 0);
21921        first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21922        first_builder.emit_op(Opcode::Integer, 12, first_reg, 0, P4::None, 0);
21923        first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21924        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21925        let first_program = first_builder.finish().expect("first program should build");
21926
21927        let mut second_builder = ProgramBuilder::new();
21928        let second_reg = second_builder.alloc_reg();
21929        second_builder.emit_op(Opcode::Integer, 21, second_reg, 0, P4::None, 0);
21930        second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21931        second_builder.emit_op(Opcode::Integer, 22, second_reg, 0, P4::None, 0);
21932        second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21933        second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21934        let second_program = second_builder
21935            .finish()
21936            .expect("second program should build");
21937
21938        let mut engine = VdbeEngine::new(
21939            first_program
21940                .register_count()
21941                .max(second_program.register_count()),
21942        );
21943        engine.set_max_collected_result_rows(Some(1));
21944        assert_eq!(
21945            run_async(engine.execute(&first_program)).expect("first execution"),
21946            ExecOutcome::Done
21947        );
21948        assert_eq!(engine.results().len(), 1);
21949
21950        let reset_cx = Cx::new();
21951        engine.reset_for_reuse(
21952            first_program
21953                .register_count()
21954                .max(second_program.register_count()),
21955            &reset_cx,
21956            PageSize::DEFAULT,
21957        );
21958
21959        assert_eq!(
21960            run_async(engine.execute(&second_program)).expect("second execution"),
21961            ExecOutcome::Done
21962        );
21963        assert_eq!(
21964            engine
21965                .results()
21966                .iter()
21967                .map(|row| row.clone().into_vec())
21968                .collect::<Vec<_>>(),
21969            vec![
21970                vec![SqliteValue::Integer(21)],
21971                vec![SqliteValue::Integer(22)],
21972            ],
21973            "reset_for_reuse should restore uncapped row collection"
21974        );
21975    }
21976
21977    #[test]
21978    fn test_reusable_table_execution_state_skips_identical_rebinds_after_preserving_reset() {
21979        let mut engine = VdbeEngine::new(4);
21980        let func_registry = Arc::new(FunctionRegistry::new());
21981        let collation_registry = Arc::new(std::sync::Mutex::new(CollationRegistry::new()));
21982        let state = ReusableTableExecutionState {
21983            func_registry: Arc::clone(&func_registry),
21984            collation_registry: Arc::clone(&collation_registry),
21985            schema_cookie: 7,
21986            autoincrement_seq_by_root_page: HashMap::from([(5, 11)]),
21987            rowid_alias_col_by_root_page: Arc::new(HashMap::from([(5, 0)])),
21988            table_column_count_by_root_page: Arc::new(HashMap::from([(5, 2)])),
21989            first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::from([(5, 1)])),
21990            column_defaults_by_root_page: Arc::new(HashMap::new()),
21991            index_desc_flags_by_root_page: Arc::new(HashMap::from([(9, vec![false, true])])),
21992            index_collations_by_root_page: Arc::new(HashMap::from([(
21993                9,
21994                vec![Some("BINARY".to_owned()), None],
21995            )])),
21996            reject_mem_fallback: false,
21997            memdb_rows_loaded: true,
21998            storage_cursor_memdb_count_shortcuts_safe: true,
21999            version_store: None,
22000            collect_result_rows: false,
22001            max_collected_result_rows: Some(1),
22002        };
22003
22004        let first_outcome = engine.apply_reusable_table_execution_state(state.clone());
22005        assert!(
22006            first_outcome.metadata_rebind_count > 0,
22007            "fresh engines should bind the reusable execution state"
22008        );
22009
22010        let reset_cx = Cx::new();
22011        engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22012        let second_outcome = engine.apply_reusable_table_execution_state(state.clone());
22013        assert_eq!(
22014            second_outcome.metadata_rebind_count, 0,
22015            "preserving reset should let identical reusable table state skip rebinding"
22016        );
22017
22018        engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
22019        let third_outcome = engine.apply_reusable_table_execution_state(state);
22020        assert!(
22021            third_outcome.metadata_rebind_count > 0,
22022            "legacy reset_for_reuse should still clear runtime bindings"
22023        );
22024    }
22025
22026    #[test]
22027    fn test_w1_function_cache_preserved_when_func_registry_unchanged() {
22028        // W1 optimization: function caches should be preserved across reusable-lane
22029        // resets when the func_registry pointer is unchanged, avoiding unnecessary
22030        // cache rebuilds on every prepared-statement execution.
22031        let mut engine = VdbeEngine::new(4);
22032        let func_registry = Arc::new(FunctionRegistry::new());
22033        let collation_registry = Arc::new(std::sync::Mutex::new(CollationRegistry::new()));
22034        let state = ReusableTableExecutionState {
22035            func_registry: Arc::clone(&func_registry),
22036            collation_registry: Arc::clone(&collation_registry),
22037            schema_cookie: 1,
22038            autoincrement_seq_by_root_page: HashMap::new(),
22039            rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
22040            table_column_count_by_root_page: Arc::new(HashMap::new()),
22041            first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
22042            column_defaults_by_root_page: Arc::new(HashMap::new()),
22043            index_desc_flags_by_root_page: Arc::new(HashMap::new()),
22044            index_collations_by_root_page: Arc::new(HashMap::new()),
22045            reject_mem_fallback: false,
22046            memdb_rows_loaded: false,
22047            storage_cursor_memdb_count_shortcuts_safe: false,
22048            version_store: None,
22049            collect_result_rows: true,
22050            max_collected_result_rows: None,
22051        };
22052
22053        // First apply: fresh engine, caches will be cleared (func_registry was None)
22054        let first_outcome = engine.apply_reusable_table_execution_state(state.clone());
22055        assert!(
22056            first_outcome.function_cache_cleared,
22057            "first apply should clear function caches (func_registry was None)"
22058        );
22059
22060        // Reset preserving runtime setup, then re-apply with same func_registry
22061        let reset_cx = Cx::new();
22062        engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22063        let second_outcome = engine.apply_reusable_table_execution_state(state.clone());
22064        assert!(
22065            !second_outcome.function_cache_cleared,
22066            "W1: function caches should NOT be cleared when func_registry is unchanged"
22067        );
22068
22069        // Change the func_registry and verify caches ARE cleared
22070        let new_func_registry = Arc::new(FunctionRegistry::new());
22071        let changed_state = ReusableTableExecutionState {
22072            func_registry: Arc::clone(&new_func_registry),
22073            ..state
22074        };
22075        engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22076        let third_outcome = engine.apply_reusable_table_execution_state(changed_state);
22077        assert!(
22078            third_outcome.function_cache_cleared,
22079            "function caches should be cleared when func_registry changes"
22080        );
22081
22082        // Legacy reset should clear everything
22083        engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
22084        let fourth_outcome =
22085            engine.apply_reusable_table_execution_state(ReusableTableExecutionState {
22086                func_registry: Arc::clone(&new_func_registry),
22087                collation_registry: Arc::clone(&collation_registry),
22088                schema_cookie: 1,
22089                autoincrement_seq_by_root_page: HashMap::new(),
22090                rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
22091                table_column_count_by_root_page: Arc::new(HashMap::new()),
22092                first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
22093                column_defaults_by_root_page: Arc::new(HashMap::new()),
22094                index_desc_flags_by_root_page: Arc::new(HashMap::new()),
22095                index_collations_by_root_page: Arc::new(HashMap::new()),
22096                reject_mem_fallback: false,
22097                memdb_rows_loaded: false,
22098                storage_cursor_memdb_count_shortcuts_safe: false,
22099                version_store: None,
22100                collect_result_rows: true,
22101                max_collected_result_rows: None,
22102            });
22103        assert!(
22104            fourth_outcome.function_cache_cleared,
22105            "legacy reset_for_reuse clears caches, so next apply should clear them again"
22106        );
22107    }
22108
22109    #[test]
22110    fn test_execute_clears_cold_subtype_state_between_statements() {
22111        let mut subtype_builder = ProgramBuilder::new();
22112        let subtype_reg = subtype_builder.alloc_reg();
22113        let tagged_value_reg = subtype_builder.alloc_reg();
22114        subtype_builder.emit_op(Opcode::Integer, 74, subtype_reg, 0, P4::None, 0);
22115        subtype_builder.emit_op(
22116            Opcode::SetSubtype,
22117            subtype_reg,
22118            tagged_value_reg,
22119            0,
22120            P4::None,
22121            0,
22122        );
22123        subtype_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22124        let subtype_program = subtype_builder
22125            .finish()
22126            .expect("subtype program should build");
22127
22128        let mut probe_builder = ProgramBuilder::new();
22129        let probe_result_reg = probe_builder.alloc_reg();
22130        let probe_value_reg = probe_builder.alloc_reg();
22131        probe_builder.emit_op(
22132            Opcode::GetSubtype,
22133            probe_value_reg,
22134            probe_result_reg,
22135            0,
22136            P4::None,
22137            0,
22138        );
22139        probe_builder.emit_op(Opcode::ResultRow, probe_result_reg, 1, 0, P4::None, 0);
22140        probe_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22141        let probe_program = probe_builder.finish().expect("probe program should build");
22142
22143        let mut engine = VdbeEngine::new(
22144            subtype_program
22145                .register_count()
22146                .max(probe_program.register_count()),
22147        );
22148        assert_eq!(
22149            run_async(engine.execute(&subtype_program)).expect("subtype execution"),
22150            ExecOutcome::Done
22151        );
22152        assert_eq!(engine.register_subtype(tagged_value_reg), Some(74));
22153        assert!(
22154            engine
22155                .statement_cold_state
22156                .contains(StatementColdState::REGISTER_SUBTYPES)
22157        );
22158
22159        assert_eq!(
22160            run_async(engine.execute(&probe_program)).expect("probe execution"),
22161            ExecOutcome::Done
22162        );
22163        assert!(engine.register_subtype(tagged_value_reg).is_none());
22164        assert!(engine.statement_cold_state.is_empty());
22165        assert_eq!(
22166            engine
22167                .results()
22168                .iter()
22169                .map(|row| row.clone().into_vec())
22170                .collect::<Vec<_>>(),
22171            vec![vec![SqliteValue::Integer(0)]]
22172        );
22173    }
22174
22175    #[test]
22176    fn test_secondary_index_rollback_removes_tracked_replace_entry() {
22177        let mut db = MemDatabase::new();
22178        let table_root = db.create_table(1);
22179        let index_root = db.allocate_root_page();
22180
22181        let mut engine = VdbeEngine::new(8);
22182        engine.enable_storage_cursors(true);
22183        engine.set_database(db);
22184        engine.set_reject_mem_fallback(false);
22185
22186        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
22187        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
22188
22189        let payload = encode_record(&[SqliteValue::Integer(99)]);
22190        {
22191            let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
22192            run_async(
22193                table_cursor
22194                    .cursor
22195                    .table_insert(&table_cursor.cx, 2, &payload),
22196            )
22197            .expect("provisional table row should insert");
22198            assert!(
22199                run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 2),)
22200                    .expect("table seek should succeed")
22201                    .is_found()
22202            );
22203        }
22204
22205        let index_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(2)]);
22206        {
22207            let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
22208            run_async(
22209                index_cursor
22210                    .cursor
22211                    .index_insert(&index_cursor.cx, &index_key),
22212            )
22213            .expect("index entry should insert");
22214            assert!(
22215                run_async(
22216                    index_cursor
22217                        .cursor
22218                        .index_move_to(&index_cursor.cx, &index_key),
22219                )
22220                .expect("index seek should succeed")
22221                .is_found()
22222            );
22223        }
22224
22225        engine.changes = 1;
22226        engine.set_pending_insert_rollback(Some(PendingInsertRollback {
22227            cursor_id: 0,
22228            rowid: 2,
22229            previous_last_insert_rowid: 0,
22230            previous_last_insert_rowid_valid: false,
22231            update_restore: None,
22232        }));
22233        engine.push_pending_idx_entry(1, index_key.clone());
22234
22235        run_async(engine.rollback_pending_insert_after_index_conflict(true))
22236            .expect("rollback should remove provisional row and tracked index entries");
22237
22238        let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
22239        assert!(
22240            !run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 2),)
22241                .expect("post-rollback table seek should succeed")
22242                .is_found()
22243        );
22244
22245        let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
22246        assert!(
22247            !run_async(
22248                index_cursor
22249                    .cursor
22250                    .index_move_to(&index_cursor.cx, &index_key),
22251            )
22252            .expect("post-rollback index seek should succeed")
22253            .is_found()
22254        );
22255        assert_eq!(engine.changes(), 0);
22256    }
22257
22258    #[test]
22259    fn test_execute_insert_with_explicit_rowid_zero_tracks_last_insert_rowid() {
22260        let mut db = MemDatabase::new();
22261        let root = db.create_table(1);
22262
22263        let mut builder = ProgramBuilder::new();
22264        let end = builder.emit_label();
22265        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22266        builder.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
22267        builder.emit_op(Opcode::Integer, 0, 1, 0, P4::None, 0);
22268        builder.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
22269        builder.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
22270        builder.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
22271        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22272        builder.resolve_label(end);
22273        let program = builder.finish().expect("program should build");
22274
22275        let mut engine = VdbeEngine::new(program.register_count());
22276        engine.set_database(db);
22277        engine.set_reject_mem_fallback(false);
22278
22279        assert_eq!(
22280            run_async(engine.execute(&program)).expect("insert execution"),
22281            ExecOutcome::Done
22282        );
22283        assert_eq!(engine.changes(), 1);
22284        assert_eq!(engine.last_insert_rowid(), Some(0));
22285    }
22286
22287    #[test]
22288    fn test_execute_observes_cancelled_execution_context_before_first_opcode() {
22289        let cx = Cx::new();
22290        cx.cancel();
22291
22292        let mut builder = ProgramBuilder::new();
22293        builder.emit_op(Opcode::Integer, 7, 1, 0, P4::None, 0);
22294        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22295        let program = builder.finish().expect("program should build");
22296
22297        let mut engine =
22298            VdbeEngine::new_with_execution_cx(program.register_count(), &cx, PageSize::DEFAULT);
22299        let err = run_async(engine.execute(&program))
22300            .expect_err("cancelled execution context should abort before opcode dispatch");
22301
22302        assert!(matches!(err, FrankenError::Abort));
22303    }
22304
22305    #[test]
22306    fn test_execute_observes_execution_cx_cancellation_immediately_after_function_opcode() {
22307        let root_cx = Cx::new();
22308
22309        let mut builder = ProgramBuilder::new();
22310        let result_reg = builder.alloc_reg();
22311        builder.emit_op(
22312            Opcode::Function,
22313            0,
22314            0,
22315            result_reg,
22316            P4::FuncName("cancel_exec".to_owned()),
22317            0,
22318        );
22319        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22320        let program = builder.finish().expect("program should build");
22321
22322        let mut engine = VdbeEngine::new_with_execution_cx(
22323            program.register_count(),
22324            &root_cx,
22325            PageSize::DEFAULT,
22326        );
22327        let mut registry = FunctionRegistry::new();
22328        registry.register_scalar(CancelExecutionFunc {
22329            cx: root_cx.clone(),
22330        });
22331        engine.set_function_registry(Arc::new(registry));
22332
22333        let err = run_async(engine.execute(&program))
22334            .expect_err("cancellation should be observed before dispatch continues");
22335        assert!(matches!(err, FrankenError::Abort));
22336        assert!(engine.results().is_empty());
22337    }
22338
22339    /// Build and execute a program with the builtin function registry attached.
22340    fn run_program_with_functions(
22341        build: impl FnOnce(&mut ProgramBuilder),
22342    ) -> Vec<Vec<SqliteValue>> {
22343        let mut b = ProgramBuilder::new();
22344        build(&mut b);
22345        let prog = b.finish().expect("program should build");
22346        let mut engine = VdbeEngine::new(prog.register_count());
22347        let mut registry = FunctionRegistry::new();
22348        register_builtins(&mut registry);
22349        engine.set_function_registry(Arc::new(registry));
22350        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22351        assert_eq!(outcome, ExecOutcome::Done);
22352        engine
22353            .take_results()
22354            .into_iter()
22355            .map(|v| v.into_vec())
22356            .collect()
22357    }
22358
22359    #[test]
22360    fn test_opcode_register_spans_for_variable() {
22361        let op = VdbeOp {
22362            opcode: Opcode::Variable,
22363            p1: 2,
22364            p2: 9,
22365            p3: 0,
22366            p4: P4::None,
22367            p5: 0,
22368        };
22369        let spans = opcode_register_spans(&op);
22370        assert_eq!(spans.read_start, -1);
22371        assert_eq!(spans.read_len, 0);
22372        assert_eq!(spans.write_start, 9);
22373        assert_eq!(spans.write_len, 1);
22374    }
22375
22376    #[test]
22377    fn test_opcode_register_spans_for_result_row() {
22378        let op = VdbeOp {
22379            opcode: Opcode::ResultRow,
22380            p1: 4,
22381            p2: 3,
22382            p3: 0,
22383            p4: P4::None,
22384            p5: 0,
22385        };
22386        let spans = opcode_register_spans(&op);
22387        assert_eq!(spans.read_start, 4);
22388        assert_eq!(spans.read_len, 3);
22389        assert_eq!(spans.write_start, -1);
22390        assert_eq!(spans.write_len, 0);
22391    }
22392
22393    #[test]
22394    fn test_opcode_register_spans_for_copy_range() {
22395        let op = VdbeOp {
22396            opcode: Opcode::Copy,
22397            p1: 4,
22398            p2: 9,
22399            p3: 2,
22400            p4: P4::None,
22401            p5: 0,
22402        };
22403        let spans = opcode_register_spans(&op);
22404        assert_eq!(spans.read_start, 4);
22405        assert_eq!(spans.read_len, 3);
22406        assert_eq!(spans.write_start, 9);
22407        assert_eq!(spans.write_len, 3);
22408    }
22409
22410    #[test]
22411    fn test_variable_uses_bound_parameter_value() {
22412        let rows = run_program_with_bindings(
22413            |b| {
22414                let end = b.emit_label();
22415                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22416                let r1 = b.alloc_reg();
22417                b.emit_op(Opcode::Variable, 2, r1, 0, P4::None, 0);
22418                b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22419                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22420                b.resolve_label(end);
22421            },
22422            vec![SqliteValue::Integer(11), SqliteValue::Text("bound".into())],
22423        );
22424        assert_eq!(rows, vec![vec![SqliteValue::Text("bound".into())]]);
22425    }
22426
22427    #[test]
22428    fn test_variable_integer_binding_in_place_fast_path() {
22429        // An integer parameter must land the identical Integer value in the
22430        // target register both when it starts NULL and when it already holds
22431        // an Integer (the in-place set_reg_int fast lane). Two Variable writes
22432        // to the same register exercise both cases in one program.
22433        let rows = run_program_with_bindings(
22434            |b| {
22435                let end = b.emit_label();
22436                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22437                let r1 = b.alloc_reg();
22438                b.emit_op(Opcode::Variable, 1, r1, 0, P4::None, 0);
22439                b.emit_op(Opcode::Variable, 1, r1, 0, P4::None, 0);
22440                b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22441                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22442                b.resolve_label(end);
22443            },
22444            vec![SqliteValue::Integer(-7)],
22445        );
22446        assert_eq!(rows, vec![vec![SqliteValue::Integer(-7)]]);
22447    }
22448
22449    #[test]
22450    fn test_arith_integer_result_in_place_fast_path() {
22451        // Integer arithmetic results must land the exact integer value in the
22452        // output register, including the already-Integer in-place path (a
22453        // second write to the same register) and a Float-producing operand
22454        // that must fall through to the general set_reg_fast path.
22455        let rows = run_program_with_bindings(
22456            |b| {
22457                let end = b.emit_label();
22458                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22459                let lhs = b.alloc_reg();
22460                let rhs = b.alloc_reg();
22461                let out = b.alloc_reg();
22462                b.emit_op(Opcode::Integer, 17, lhs, 0, P4::None, 0);
22463                b.emit_op(Opcode::Integer, 25, rhs, 0, P4::None, 0);
22464                // out := 17 + 25, twice — the second Add hits the in-place path.
22465                b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22466                b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22467                // out := out * 2 = 84 (still Integer, in place again).
22468                let two = b.alloc_reg();
22469                b.emit_op(Opcode::Integer, 2, two, 0, P4::None, 0);
22470                b.emit_op(Opcode::Multiply, two, out, out, P4::None, 0);
22471                // out := out - 84 = 0.
22472                b.emit_op(Opcode::Subtract, out, out, out, P4::None, 0);
22473                b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22474                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22475                b.resolve_label(end);
22476            },
22477            vec![],
22478        );
22479        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
22480    }
22481
22482    #[test]
22483    fn test_arith_float_result_falls_through_general_path() {
22484        // A Float operand makes the result Float, which must take the general
22485        // set_reg_fast path (not the integer in-place lane).
22486        let rows = run_program_with_bindings(
22487            |b| {
22488                let end = b.emit_label();
22489                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22490                let lhs = b.alloc_reg();
22491                let rhs = b.alloc_reg();
22492                let out = b.alloc_reg();
22493                b.emit_op(Opcode::Integer, 5, lhs, 0, P4::None, 0);
22494                b.emit_op(Opcode::Real, 0, rhs, 0, P4::Real(2.5), 0);
22495                b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22496                b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22497                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22498                b.resolve_label(end);
22499            },
22500            vec![],
22501        );
22502        assert_eq!(rows, vec![vec![SqliteValue::Float(7.5)]]);
22503    }
22504
22505    #[test]
22506    fn test_div_rem_integer_result_in_place_and_zero_null() {
22507        // Integer divide results reuse the output register in place (the shipped
22508        // lever); remainder and a division by zero (Null) go through the general
22509        // set_reg_fast path. All must land the exact value regardless.
22510        let rows = run_program_with_bindings(
22511            |b| {
22512                let end = b.emit_label();
22513                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22514                let dividend = b.alloc_reg();
22515                let divisor = b.alloc_reg();
22516                let zero = b.alloc_reg();
22517                let out = b.alloc_reg();
22518                b.emit_op(Opcode::Integer, 86, dividend, 0, P4::None, 0);
22519                b.emit_op(Opcode::Integer, 7, divisor, 0, P4::None, 0);
22520                b.emit_op(Opcode::Integer, 0, zero, 0, P4::None, 0);
22521                // out := 86 / 7 = 12, twice (the second is the in-place path).
22522                b.emit_op(Opcode::Divide, divisor, dividend, out, P4::None, 0);
22523                b.emit_op(Opcode::Divide, divisor, dividend, out, P4::None, 0);
22524                // out := 86 % 7 = 2 (in-place over Integer 12).
22525                b.emit_op(Opcode::Remainder, divisor, dividend, out, P4::None, 0);
22526                b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22527                // out := 86 / 0 = NULL (general path over the Integer 2).
22528                b.emit_op(Opcode::Divide, zero, dividend, out, P4::None, 0);
22529                b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22530                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22531                b.resolve_label(end);
22532            },
22533            vec![],
22534        );
22535        assert_eq!(
22536            rows,
22537            vec![vec![SqliteValue::Integer(2)], vec![SqliteValue::Null]]
22538        );
22539    }
22540
22541    #[test]
22542    fn test_variable_unbound_parameter_defaults_to_null() {
22543        let rows = run_program_with_bindings(
22544            |b| {
22545                let end = b.emit_label();
22546                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22547                let r1 = b.alloc_reg();
22548                b.emit_op(Opcode::Variable, 3, r1, 0, P4::None, 0);
22549                b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22550                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22551                b.resolve_label(end);
22552            },
22553            vec![SqliteValue::Integer(11)],
22554        );
22555        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
22556    }
22557
22558    // ── test_select_integer_literal ─────────────────────────────────────
22559    #[test]
22560    fn test_select_integer_literal() {
22561        // SELECT 42 → [(42,)]
22562        let rows = run_program(|b| {
22563            let end = b.emit_label();
22564            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22565            let r1 = b.alloc_reg();
22566            b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
22567            b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22568            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22569            b.resolve_label(end);
22570        });
22571        assert_eq!(rows.len(), 1);
22572        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
22573    }
22574
22575    // ── test_select_arithmetic ──────────────────────────────────────────
22576    #[test]
22577    fn test_select_arithmetic() {
22578        // SELECT 1+2 → [(3,)]
22579        let rows = run_program(|b| {
22580            let end = b.emit_label();
22581            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22582
22583            let r1 = b.alloc_reg(); // 1
22584            let r2 = b.alloc_reg(); // 2
22585            let r3 = b.alloc_reg(); // result
22586
22587            b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
22588            b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
22589            b.emit_op(Opcode::Add, r1, r2, r3, P4::None, 0);
22590            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22591            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22592            b.resolve_label(end);
22593        });
22594        assert_eq!(rows.len(), 1);
22595        assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
22596    }
22597
22598    // ── test_select_expression_eval ─────────────────────────────────────
22599    #[test]
22600    fn test_select_expression_eval() {
22601        // SELECT 1+2, 'abc'||'def' → [(3, "abcdef")]
22602        let rows = run_program(|b| {
22603            let end = b.emit_label();
22604            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22605
22606            let r1 = b.alloc_reg();
22607            let r2 = b.alloc_reg();
22608            let r3 = b.alloc_reg(); // 1+2 result
22609            let r4 = b.alloc_reg();
22610            let r5 = b.alloc_reg();
22611            let r6 = b.alloc_reg(); // concat result
22612
22613            // 1 + 2
22614            b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
22615            b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
22616            b.emit_op(Opcode::Add, r1, r2, r3, P4::None, 0);
22617
22618            // 'abc' || 'def'
22619            b.emit_op(Opcode::String8, 0, r4, 0, P4::Str("abc".to_owned()), 0);
22620            b.emit_op(Opcode::String8, 0, r5, 0, P4::Str("def".to_owned()), 0);
22621            b.emit_op(Opcode::Concat, r5, r4, r6, P4::None, 0);
22622
22623            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22624            // Also emit second column as separate row for now
22625            b.emit_op(Opcode::ResultRow, r6, 1, 0, P4::None, 0);
22626
22627            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22628            b.resolve_label(end);
22629        });
22630        assert_eq!(rows.len(), 2);
22631        assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
22632        assert_eq!(rows[1], vec![SqliteValue::Text("abcdef".into())]);
22633    }
22634
22635    // ── test_select_multi_column ────────────────────────────────────────
22636    #[test]
22637    fn test_select_multi_column() {
22638        // SELECT 1+2, 'abc'||'def' as a single row
22639        let rows = run_program(|b| {
22640            let end = b.emit_label();
22641            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22642
22643            let out_start = b.alloc_regs(2);
22644            let r_tmp1 = b.alloc_reg();
22645            let r_tmp2 = b.alloc_reg();
22646
22647            // 1 + 2 → out_start
22648            b.emit_op(Opcode::Integer, 1, r_tmp1, 0, P4::None, 0);
22649            b.emit_op(Opcode::Integer, 2, r_tmp2, 0, P4::None, 0);
22650            b.emit_op(Opcode::Add, r_tmp1, r_tmp2, out_start, P4::None, 0);
22651
22652            // 'abc' || 'def' → out_start+1
22653            b.emit_op(Opcode::String8, 0, r_tmp1, 0, P4::Str("abc".to_owned()), 0);
22654            b.emit_op(Opcode::String8, 0, r_tmp2, 0, P4::Str("def".to_owned()), 0);
22655            b.emit_op(Opcode::Concat, r_tmp2, r_tmp1, out_start + 1, P4::None, 0);
22656
22657            b.emit_op(Opcode::ResultRow, out_start, 2, 0, P4::None, 0);
22658            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22659            b.resolve_label(end);
22660        });
22661        assert_eq!(rows.len(), 1);
22662        assert_eq!(
22663            rows[0],
22664            vec![SqliteValue::Integer(3), SqliteValue::Text("abcdef".into()),]
22665        );
22666    }
22667
22668    // ── test_vdbe_null_handling ──────────────────────────────────────────
22669    #[test]
22670    fn test_vdbe_null_handling() {
22671        // NULL + 1 = NULL, NULL = NULL is NULL (no jump)
22672        let rows = run_program(|b| {
22673            let end = b.emit_label();
22674            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22675
22676            let r_null = b.alloc_reg();
22677            let r_one = b.alloc_reg();
22678            let r_result = b.alloc_reg();
22679            let r_is_null = b.alloc_reg();
22680
22681            // NULL
22682            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
22683            // 1
22684            b.emit_op(Opcode::Integer, 1, r_one, 0, P4::None, 0);
22685            // NULL + 1
22686            b.emit_op(Opcode::Add, r_null, r_one, r_result, P4::None, 0);
22687            // Check: result IS NULL → set r_is_null=1
22688            b.emit_op(Opcode::Integer, 0, r_is_null, 0, P4::None, 0);
22689            let skip = b.emit_label();
22690            b.emit_jump_to_label(Opcode::NotNull, r_result, 0, skip, P4::None, 0);
22691            b.emit_op(Opcode::Integer, 1, r_is_null, 0, P4::None, 0);
22692            b.resolve_label(skip);
22693
22694            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
22695            b.emit_op(Opcode::ResultRow, r_is_null, 1, 0, P4::None, 0);
22696            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22697            b.resolve_label(end);
22698        });
22699        assert_eq!(rows.len(), 2);
22700        assert_eq!(rows[0], vec![SqliteValue::Null]); // NULL + 1 = NULL
22701        assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); // IS NULL = true
22702    }
22703
22704    // ── test_vdbe_comparison_affinity ────────────────────────────────────
22705    #[test]
22706    fn test_vdbe_comparison_affinity() {
22707        // Test: 5 > 3 → jump taken (result 1), 3 > 5 → not taken (result 0)
22708        let rows = run_program(|b| {
22709            let end = b.emit_label();
22710            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22711
22712            let r_5 = b.alloc_reg();
22713            let r_3 = b.alloc_reg();
22714            let r_out = b.alloc_reg();
22715
22716            b.emit_op(Opcode::Integer, 5, r_5, 0, P4::None, 0);
22717            b.emit_op(Opcode::Integer, 3, r_3, 0, P4::None, 0);
22718
22719            // Test 5 > 3: if r_5 (p3) > r_3 (p1), jump.
22720            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
22721            let gt_taken = b.emit_label();
22722            b.emit_jump_to_label(Opcode::Gt, r_3, r_5, gt_taken, P4::None, 0);
22723            // Not taken path:
22724            let done1 = b.emit_label();
22725            b.emit_jump_to_label(Opcode::Goto, 0, 0, done1, P4::None, 0);
22726            // Taken path:
22727            b.resolve_label(gt_taken);
22728            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
22729            b.resolve_label(done1);
22730
22731            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
22732
22733            // Test 3 > 5: should NOT jump
22734            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
22735            let gt_taken2 = b.emit_label();
22736            // p3=r_3 (3), p1=r_5 (5): is 3 > 5? No.
22737            b.emit_jump_to_label(Opcode::Gt, r_5, r_3, gt_taken2, P4::None, 0);
22738            let done2 = b.emit_label();
22739            b.emit_jump_to_label(Opcode::Goto, 0, 0, done2, P4::None, 0);
22740            b.resolve_label(gt_taken2);
22741            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
22742            b.resolve_label(done2);
22743
22744            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
22745            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22746            b.resolve_label(end);
22747        });
22748        assert_eq!(rows.len(), 2);
22749        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); // 5 > 3 = true
22750        assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); // 3 > 5 = false
22751    }
22752
22753    #[test]
22754    fn test_comparison_affinity_precedes_same_storage_fast_paths() {
22755        let rows = run_program(|b| {
22756            let end = b.emit_label();
22757            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22758
22759            let r_text_non_numeric = b.alloc_reg();
22760            let r_text_two = b.alloc_reg();
22761            let r_text_ten = b.alloc_reg();
22762            let r_int_two = b.alloc_reg();
22763            let r_int_ten = b.alloc_reg();
22764            let r_float_two = b.alloc_reg();
22765            let r_float_ten = b.alloc_reg();
22766            let outputs = b.alloc_regs(4);
22767
22768            b.emit_op(
22769                Opcode::String8,
22770                0,
22771                r_text_non_numeric,
22772                0,
22773                P4::Str("0x".to_owned()),
22774                0,
22775            );
22776            b.emit_op(
22777                Opcode::String8,
22778                0,
22779                r_text_two,
22780                0,
22781                P4::Str("2".to_owned()),
22782                0,
22783            );
22784            b.emit_op(
22785                Opcode::String8,
22786                0,
22787                r_text_ten,
22788                0,
22789                P4::Str("10".to_owned()),
22790                0,
22791            );
22792            b.emit_op(Opcode::Integer, 2, r_int_two, 0, P4::None, 0);
22793            b.emit_op(Opcode::Integer, 10, r_int_ten, 0, P4::None, 0);
22794            b.emit_op(Opcode::Real, 0, r_float_two, 0, P4::Real(2.0), 0);
22795            b.emit_op(Opcode::Real, 0, r_float_ten, 0, P4::Real(10.0), 0);
22796
22797            // NUMERIC affinity converts each TEXT operand independently. "0x"
22798            // remains TEXT while "10" becomes INTEGER, so storage-class order
22799            // makes "0x" greater. A same-TEXT fast path would compare bytes and
22800            // produce the opposite result.
22801            b.emit_op(
22802                Opcode::Gt,
22803                r_text_ten,
22804                outputs,
22805                r_text_non_numeric,
22806                P4::None,
22807                SQLITE_AFF_NUMERIC | 0x20,
22808            );
22809            // Both numeric-looking TEXT operands become integers: 2 < 10.
22810            b.emit_op(
22811                Opcode::Lt,
22812                r_text_ten,
22813                outputs + 1,
22814                r_text_two,
22815                P4::None,
22816                SQLITE_AFF_NUMERIC | 0x20,
22817            );
22818            // TEXT affinity stringifies same-storage numeric operands before
22819            // comparison, making both "2" > "10" and "2.0" > "10.0".
22820            b.emit_op(
22821                Opcode::Gt,
22822                r_int_ten,
22823                outputs + 2,
22824                r_int_two,
22825                P4::None,
22826                SQLITE_AFF_TEXT | 0x20,
22827            );
22828            b.emit_op(
22829                Opcode::Gt,
22830                r_float_ten,
22831                outputs + 3,
22832                r_float_two,
22833                P4::None,
22834                SQLITE_AFF_TEXT | 0x20,
22835            );
22836
22837            b.emit_op(Opcode::ResultRow, outputs, 4, 0, P4::None, 0);
22838            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22839            b.resolve_label(end);
22840        });
22841
22842        assert_eq!(
22843            rows,
22844            vec![vec![
22845                SqliteValue::Integer(1),
22846                SqliteValue::Integer(1),
22847                SqliteValue::Integer(1),
22848                SqliteValue::Integer(1),
22849            ]]
22850        );
22851    }
22852
22853    #[test]
22854    fn test_compare_opcode_uses_per_field_collation_list() {
22855        let mut b = ProgramBuilder::new();
22856        let left_key = b.alloc_regs(2);
22857        let right_key = b.alloc_regs(2);
22858
22859        b.emit_op(
22860            Opcode::String8,
22861            0,
22862            left_key,
22863            0,
22864            P4::Str("Apple".to_owned()),
22865            0,
22866        );
22867        b.emit_op(
22868            Opcode::String8,
22869            0,
22870            left_key + 1,
22871            0,
22872            P4::Str("beta".to_owned()),
22873            0,
22874        );
22875        b.emit_op(
22876            Opcode::String8,
22877            0,
22878            right_key,
22879            0,
22880            P4::Str("apple".to_owned()),
22881            0,
22882        );
22883        b.emit_op(
22884            Opcode::String8,
22885            0,
22886            right_key + 1,
22887            0,
22888            P4::Str("Beta".to_owned()),
22889            0,
22890        );
22891        b.emit_op(
22892            Opcode::Compare,
22893            left_key,
22894            right_key,
22895            2,
22896            P4::Str("NOCASE,BINARY".to_owned()),
22897            0,
22898        );
22899        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22900
22901        let prog = b.finish().expect("program should build");
22902        let mut engine = VdbeEngine::new(prog.register_count());
22903        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22904        assert_eq!(outcome, ExecOutcome::Done);
22905        assert_eq!(
22906            engine.last_compare_result,
22907            Some(std::cmp::Ordering::Greater)
22908        );
22909    }
22910
22911    #[test]
22912    fn test_compare_opcode_parses_trimmed_collation_list() {
22913        let mut b = ProgramBuilder::new();
22914        let left_key = b.alloc_regs(2);
22915        let right_key = b.alloc_regs(2);
22916
22917        b.emit_op(
22918            Opcode::String8,
22919            0,
22920            left_key,
22921            0,
22922            P4::Str("Alpha".to_owned()),
22923            0,
22924        );
22925        b.emit_op(
22926            Opcode::String8,
22927            0,
22928            left_key + 1,
22929            0,
22930            P4::Str("tail   ".to_owned()),
22931            0,
22932        );
22933        b.emit_op(
22934            Opcode::String8,
22935            0,
22936            right_key,
22937            0,
22938            P4::Str("alpha".to_owned()),
22939            0,
22940        );
22941        b.emit_op(
22942            Opcode::String8,
22943            0,
22944            right_key + 1,
22945            0,
22946            P4::Str("tail".to_owned()),
22947            0,
22948        );
22949        b.emit_op(
22950            Opcode::Compare,
22951            left_key,
22952            right_key,
22953            2,
22954            P4::Str(" NOCASE | RTRIM ".to_owned()),
22955            0,
22956        );
22957        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22958
22959        let prog = b.finish().expect("program should build");
22960        let mut engine = VdbeEngine::new(prog.register_count());
22961        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22962        assert_eq!(outcome, ExecOutcome::Done);
22963        assert_eq!(engine.last_compare_result, Some(std::cmp::Ordering::Equal));
22964    }
22965
22966    // ── test_vdbe_division_by_zero ──────────────────────────────────────
22967    #[test]
22968    fn test_vdbe_division_by_zero() {
22969        // SELECT 10 / 0 → NULL
22970        let rows = run_program(|b| {
22971            let end = b.emit_label();
22972            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22973
22974            let r1 = b.alloc_reg();
22975            let r2 = b.alloc_reg();
22976            let r3 = b.alloc_reg();
22977
22978            b.emit_op(Opcode::Integer, 0, r1, 0, P4::None, 0);
22979            b.emit_op(Opcode::Integer, 10, r2, 0, P4::None, 0);
22980            b.emit_op(Opcode::Divide, r1, r2, r3, P4::None, 0);
22981            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22982            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22983            b.resolve_label(end);
22984        });
22985        assert_eq!(rows.len(), 1);
22986        assert_eq!(rows[0], vec![SqliteValue::Null]); // div by zero → NULL
22987    }
22988
22989    #[test]
22990    fn test_vdbe_nan_arithmetic_normalized_to_null() {
22991        // +Inf - +Inf and 0 * +Inf both produce NaN at IEEE-754 level.
22992        // VDBE register writes must normalize NaN to SQL NULL.
22993        let rows = run_program(|b| {
22994            let end = b.emit_label();
22995            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22996
22997            let r_inf = b.alloc_reg();
22998            let r_zero = b.alloc_reg();
22999            let r_sub = b.alloc_reg();
23000            let r_mul = b.alloc_reg();
23001
23002            b.emit_op(Opcode::Real, 0, r_inf, 0, P4::Real(f64::INFINITY), 0);
23003            b.emit_op(Opcode::Real, 0, r_zero, 0, P4::Real(0.0), 0);
23004            b.emit_op(Opcode::Subtract, r_inf, r_inf, r_sub, P4::None, 0); // Inf - Inf
23005            b.emit_op(Opcode::Multiply, r_inf, r_zero, r_mul, P4::None, 0); // 0 * Inf
23006            b.emit_op(Opcode::ResultRow, r_sub, 2, 0, P4::None, 0);
23007            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23008            b.resolve_label(end);
23009        });
23010
23011        assert_eq!(rows.len(), 1);
23012        assert_eq!(rows[0], vec![SqliteValue::Null, SqliteValue::Null]);
23013    }
23014
23015    // ── test_vdbe_string_concat_null ────────────────────────────────────
23016    #[test]
23017    fn test_vdbe_string_concat_null() {
23018        // 'abc' || NULL → NULL
23019        let rows = run_program(|b| {
23020            let end = b.emit_label();
23021            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23022
23023            let r1 = b.alloc_reg();
23024            let r2 = b.alloc_reg();
23025            let r3 = b.alloc_reg();
23026
23027            b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("abc".to_owned()), 0);
23028            b.emit_op(Opcode::Null, 0, r2, 0, P4::None, 0);
23029            b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
23030            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23031            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23032            b.resolve_label(end);
23033        });
23034        assert_eq!(rows.len(), 1);
23035        assert_eq!(rows[0], vec![SqliteValue::Null]);
23036    }
23037
23038    // ── test_vdbe_boolean_logic ─────────────────────────────────────────
23039    #[test]
23040    fn test_vdbe_boolean_logic() {
23041        // TRUE AND FALSE → 0, TRUE OR FALSE → 1, NOT TRUE → 0
23042        let rows = run_program(|b| {
23043            let end = b.emit_label();
23044            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23045
23046            let r_true = b.alloc_reg();
23047            let r_false = b.alloc_reg();
23048            let r_and = b.alloc_reg();
23049            let r_or = b.alloc_reg();
23050            let r_not = b.alloc_reg();
23051
23052            b.emit_op(Opcode::Integer, 1, r_true, 0, P4::None, 0);
23053            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
23054            b.emit_op(Opcode::And, r_true, r_false, r_and, P4::None, 0);
23055            b.emit_op(Opcode::Or, r_true, r_false, r_or, P4::None, 0);
23056            b.emit_op(Opcode::Not, r_true, r_not, 0, P4::None, 0);
23057
23058            b.emit_op(Opcode::ResultRow, r_and, 1, 0, P4::None, 0);
23059            b.emit_op(Opcode::ResultRow, r_or, 1, 0, P4::None, 0);
23060            b.emit_op(Opcode::ResultRow, r_not, 1, 0, P4::None, 0);
23061            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23062            b.resolve_label(end);
23063        });
23064        assert_eq!(rows.len(), 3);
23065        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); // T AND F = F
23066        assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); // T OR F = T
23067        assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); // NOT T = F
23068    }
23069
23070    // ── test_vdbe_three_valued_logic ────────────────────────────────────
23071    #[test]
23072    fn test_vdbe_three_valued_logic() {
23073        // NULL AND FALSE → 0, NULL AND TRUE → NULL
23074        // NULL OR TRUE → 1, NULL OR FALSE → NULL
23075        let rows = run_program(|b| {
23076            let end = b.emit_label();
23077            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23078
23079            let r_null = b.alloc_reg();
23080            let r_true = b.alloc_reg();
23081            let r_false = b.alloc_reg();
23082            let r1 = b.alloc_reg();
23083            let r2 = b.alloc_reg();
23084            let r3 = b.alloc_reg();
23085            let r4 = b.alloc_reg();
23086
23087            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
23088            b.emit_op(Opcode::Integer, 1, r_true, 0, P4::None, 0);
23089            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
23090
23091            b.emit_op(Opcode::And, r_null, r_false, r1, P4::None, 0); // NULL AND F
23092            b.emit_op(Opcode::And, r_null, r_true, r2, P4::None, 0); // NULL AND T
23093            b.emit_op(Opcode::Or, r_null, r_true, r3, P4::None, 0); // NULL OR T
23094            b.emit_op(Opcode::Or, r_null, r_false, r4, P4::None, 0); // NULL OR F
23095
23096            b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
23097            b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
23098            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23099            b.emit_op(Opcode::ResultRow, r4, 1, 0, P4::None, 0);
23100            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23101            b.resolve_label(end);
23102        });
23103        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); // NULL AND F = F
23104        assert_eq!(rows[1], vec![SqliteValue::Null]); // NULL AND T = NULL
23105        assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); // NULL OR T = T
23106        assert_eq!(rows[3], vec![SqliteValue::Null]); // NULL OR F = NULL
23107    }
23108
23109    // ── test_vdbe_gosub_return ──────────────────────────────────────────
23110    #[test]
23111    fn test_vdbe_gosub_return() {
23112        // Use Gosub/Return to call a subroutine that sets r2=99.
23113        let rows = run_program(|b| {
23114            let end = b.emit_label();
23115            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23116
23117            let r_return = b.alloc_reg(); // return address storage
23118            let r_val = b.alloc_reg(); // output
23119
23120            // Main: call subroutine, then output r_val.
23121            let sub_label = b.emit_label();
23122            b.emit_jump_to_label(Opcode::Gosub, r_return, 0, sub_label, P4::None, 0);
23123            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23124            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23125
23126            // Subroutine: set r_val=99, return.
23127            b.resolve_label(sub_label);
23128            b.emit_op(Opcode::Integer, 99, r_val, 0, P4::None, 0);
23129            b.emit_op(Opcode::Return, r_return, 0, 0, P4::None, 0);
23130
23131            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23132            b.resolve_label(end);
23133        });
23134        assert_eq!(rows.len(), 1);
23135        assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
23136    }
23137
23138    // ── test_vdbe_is_null_comparison ─────────────────────────────────────
23139    #[test]
23140    fn test_vdbe_is_null_comparison() {
23141        // NULL IS NULL → true (using Eq with NULLEQ flag)
23142        let rows = run_program(|b| {
23143            let end = b.emit_label();
23144            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23145
23146            let r_null = b.alloc_reg();
23147            let r_out = b.alloc_reg();
23148
23149            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
23150            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
23151
23152            // Eq with p5=0x80 (SQLITE_NULLEQ): NULL IS NULL → jump
23153            let is_null_label = b.emit_label();
23154            // p1=r_null, p3=r_null (compare same register)
23155            b.emit_jump_to_label(Opcode::Eq, r_null, 0, is_null_label, P4::None, 0x80);
23156            let done = b.emit_label();
23157            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
23158            b.resolve_label(is_null_label);
23159            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23160            b.resolve_label(done);
23161
23162            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23163            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23164            b.resolve_label(end);
23165        });
23166        assert_eq!(rows.len(), 1);
23167        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); // NULL IS NULL = true
23168    }
23169
23170    // ── test_vdbe_coroutine ─────────────────────────────────────────────
23171    #[test]
23172    fn test_vdbe_coroutine() {
23173        // Test coroutine: producer yields values 10, 20, 30.
23174        let rows = run_program(|b| {
23175            let end = b.emit_label();
23176            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23177
23178            let r_co = b.alloc_reg(); // coroutine state register
23179            let r_val = b.alloc_reg(); // value register
23180
23181            let init_addr = b.emit_op(Opcode::InitCoroutine, r_co, 0, 0, P4::None, 0);
23182
23183            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
23184            let consumer_start = b.current_addr() as i32;
23185            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23186            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23187            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23188            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23189            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23190            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23191            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23192
23193            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
23194            let producer_start = b.current_addr() as i32;
23195            b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
23196            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23197            b.emit_op(Opcode::Integer, 20, r_val, 0, P4::None, 0);
23198            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23199            b.emit_op(Opcode::Integer, 30, r_val, 0, P4::None, 0);
23200            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23201            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23202
23203            if let Some(init_op) = b.op_at_mut(init_addr) {
23204                init_op.p2 = consumer_start;
23205                init_op.p3 = producer_start;
23206            }
23207
23208            b.resolve_label(end);
23209        });
23210        assert_eq!(rows.len(), 3);
23211        assert_eq!(rows[0], vec![SqliteValue::Integer(10)]);
23212        assert_eq!(rows[1], vec![SqliteValue::Integer(20)]);
23213        assert_eq!(rows[2], vec![SqliteValue::Integer(30)]);
23214    }
23215
23216    // ── test_vdbe_halt_with_error ───────────────────────────────────────
23217    #[test]
23218    fn test_vdbe_halt_with_error() {
23219        let mut b = ProgramBuilder::new();
23220        b.emit_op(
23221            Opcode::Halt,
23222            1,
23223            0,
23224            0,
23225            P4::Str("constraint failed".to_owned()),
23226            0,
23227        );
23228        let prog = b.finish().unwrap();
23229        let mut engine = VdbeEngine::new(prog.register_count());
23230        let outcome = run_async(engine.execute(&prog)).unwrap();
23231        assert_eq!(
23232            outcome,
23233            ExecOutcome::Error {
23234                code: 1,
23235                message: "constraint failed".to_owned(),
23236            }
23237        );
23238    }
23239
23240    #[test]
23241    fn test_vdbe_unique_halt_returns_constraint_error_without_mutation() {
23242        let mut b = ProgramBuilder::new();
23243        b.emit_op(
23244            Opcode::Halt,
23245            ErrorCode::Constraint as i32,
23246            0,
23247            0,
23248            P4::Str("t.email".to_owned()),
23249            OPFLAG_HALT_UNIQUE,
23250        );
23251        let program = b.finish().expect("program should finish");
23252        let mut engine = VdbeEngine::new(program.register_count());
23253        let error = run_async(engine.execute(&program)).expect_err("UNIQUE halt must fail");
23254        assert!(
23255            matches!(error, FrankenError::UniqueViolation { ref columns } if columns == "t.email"),
23256            "unexpected UNIQUE halt error: {error:?}"
23257        );
23258        assert_eq!(engine.changes(), 0);
23259    }
23260
23261    // ── test_vdbe_disassemble_and_exec ──────────────────────────────────
23262    #[test]
23263    fn test_vdbe_disassemble_and_exec() {
23264        // Build a program, disassemble it, and verify output.
23265        let mut b = ProgramBuilder::new();
23266        let end = b.emit_label();
23267        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23268        let r1 = b.alloc_reg();
23269        let r2 = b.alloc_reg();
23270        let r3 = b.alloc_reg();
23271        b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
23272        b.emit_op(Opcode::Integer, 20, r2, 0, P4::None, 0);
23273        b.emit_op(Opcode::Multiply, r1, r2, r3, P4::None, 0);
23274        b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23275        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23276        b.resolve_label(end);
23277
23278        let prog = b.finish().unwrap();
23279        let asm = prog.disassemble();
23280        assert!(asm.contains("Init"));
23281        assert!(asm.contains("Integer"));
23282        assert!(asm.contains("Multiply"));
23283        assert!(asm.contains("ResultRow"));
23284        assert!(asm.contains("Halt"));
23285
23286        let mut engine = VdbeEngine::new(prog.register_count());
23287        let outcome = run_async(engine.execute(&prog)).unwrap();
23288        assert_eq!(outcome, ExecOutcome::Done);
23289        assert_eq!(engine.results().len(), 1);
23290        assert_eq!(
23291            engine.results()[0].to_vec(),
23292            vec![SqliteValue::Integer(200)]
23293        );
23294    }
23295
23296    #[test]
23297    fn test_sorter_opcodes_sort_and_emit_rows() {
23298        let rows = run_program(|b| {
23299            let end = b.emit_label();
23300            let loop_start = b.emit_label();
23301            let empty = b.emit_label();
23302            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23303
23304            let r_value = b.alloc_reg();
23305            let r_record = b.alloc_reg();
23306            let r_sorted = b.alloc_reg();
23307
23308            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23309
23310            for value in [30, 10, 20] {
23311                b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
23312                b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23313                b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23314            }
23315
23316            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23317            b.resolve_label(loop_start);
23318            b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
23319            b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
23320            b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23321            b.resolve_label(empty);
23322            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23323            b.resolve_label(end);
23324        });
23325
23326        let decoded: Vec<i64> = rows
23327            .into_iter()
23328            .map(|row| decode_record(&row[0]).unwrap()[0].to_integer())
23329            .collect();
23330        assert_eq!(decoded, vec![10, 20, 30]);
23331    }
23332
23333    #[test]
23334    fn test_sorter_insert_consumes_make_record_sideband_without_materializing() {
23335        let _guard = VDBE_OBSERVABILITY_LOCK
23336            .lock()
23337            .unwrap_or_else(|e| e.into_inner());
23338        reset_vdbe_test_sideband_materialization_count();
23339
23340        let before = vdbe_test_sideband_materialization_count_snapshot();
23341        let rows = run_program(|b| {
23342            let end = b.emit_label();
23343            let loop_start = b.emit_label();
23344            let empty = b.emit_label();
23345            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23346
23347            let r_value = b.alloc_reg();
23348            let r_record = b.alloc_reg();
23349            let r_sorted = b.alloc_reg();
23350
23351            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23352
23353            for value in [3, 1, 2] {
23354                b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
23355                b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23356                b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23357            }
23358
23359            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23360            b.resolve_label(loop_start);
23361            b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
23362            b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
23363            b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23364            b.resolve_label(empty);
23365            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23366            b.resolve_label(end);
23367        });
23368        let after = vdbe_test_sideband_materialization_count_snapshot();
23369
23370        assert_eq!(
23371            after - before,
23372            0,
23373            "SorterInsert should move MakeRecord sideband bytes directly into the sorter"
23374        );
23375        let decoded: Vec<i64> = rows
23376            .into_iter()
23377            .map(|row| decode_record(&row[0]).unwrap()[0].to_integer())
23378            .collect();
23379        assert_eq!(decoded, vec![1, 2, 3]);
23380    }
23381
23382    #[test]
23383    fn test_sorter_compare_jumps_on_key_difference() {
23384        let rows = run_program(|b| {
23385            let end = b.emit_label();
23386            let diff = b.emit_label();
23387            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23388
23389            let r_value = b.alloc_reg();
23390            let r_record = b.alloc_reg();
23391            let r_probe = b.alloc_reg();
23392            let r_probe_record = b.alloc_reg();
23393            let r_out = b.alloc_reg();
23394
23395            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23396            b.emit_op(Opcode::Integer, 10, r_value, 0, P4::None, 0);
23397            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23398            b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23399            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, diff, P4::None, 0);
23400
23401            b.emit_op(Opcode::Integer, 20, r_probe, 0, P4::None, 0);
23402            b.emit_op(Opcode::MakeRecord, r_probe, 1, r_probe_record, P4::None, 0);
23403            b.emit_jump_to_label(Opcode::SorterCompare, 0, r_probe_record, diff, P4::None, 0);
23404
23405            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23406            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23407            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23408
23409            b.resolve_label(diff);
23410            b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23411            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23412            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23413            b.resolve_label(end);
23414        });
23415
23416        assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
23417    }
23418
23419    #[test]
23420    fn test_sorter_compare_falls_through_on_equal_make_record_sideband_key() {
23421        let rows = run_program(|b| {
23422            let end = b.emit_label();
23423            let diff = b.emit_label();
23424            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23425
23426            let r_value = b.alloc_reg();
23427            let r_record = b.alloc_reg();
23428            let r_probe = b.alloc_reg();
23429            let r_probe_record = b.alloc_reg();
23430            let r_out = b.alloc_reg();
23431
23432            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23433            b.emit_op(Opcode::Integer, 10, r_value, 0, P4::None, 0);
23434            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23435            b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23436            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, diff, P4::None, 0);
23437
23438            b.emit_op(Opcode::Integer, 10, r_probe, 0, P4::None, 0);
23439            b.emit_op(Opcode::MakeRecord, r_probe, 1, r_probe_record, P4::None, 0);
23440            b.emit_jump_to_label(Opcode::SorterCompare, 0, r_probe_record, diff, P4::None, 0);
23441
23442            b.emit_op(Opcode::Copy, r_probe_record, r_out, 0, P4::None, 0);
23443            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23444            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23445
23446            b.resolve_label(diff);
23447            b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23448            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23449            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23450            b.resolve_label(end);
23451        });
23452
23453        assert_eq!(rows.len(), 1);
23454        assert_eq!(
23455            decode_record(&rows[0][0]).expect("copied probe should remain a valid record"),
23456            vec![SqliteValue::Integer(10)]
23457        );
23458    }
23459
23460    #[test]
23461    fn test_sorter_compare_unpositioned_sorter_jumps_without_decoding_probe() {
23462        let rows = run_program(|b| {
23463            let end = b.emit_label();
23464            let unpositioned = b.emit_label();
23465            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23466
23467            let r_uninitialized_probe = b.alloc_reg();
23468            let r_out = b.alloc_reg();
23469            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23470            b.emit_jump_to_label(
23471                Opcode::SorterCompare,
23472                0,
23473                r_uninitialized_probe,
23474                unpositioned,
23475                P4::None,
23476                0,
23477            );
23478            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23479            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23480            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23481
23482            b.resolve_label(unpositioned);
23483            b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23484            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23485            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23486            b.resolve_label(end);
23487        });
23488
23489        assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
23490    }
23491
23492    #[test]
23493    fn test_sorter_compare_top_n_preflight_uses_runtime_bound() {
23494        let _guard = VDBE_OBSERVABILITY_LOCK
23495            .lock()
23496            .unwrap_or_else(|error| error.into_inner());
23497        reset_vdbe_test_sideband_materialization_count();
23498        let materializations_before = vdbe_test_sideband_materialization_count_snapshot();
23499        let rows = run_program(|b| {
23500            let end = b.emit_label();
23501            let rejected = b.emit_label();
23502            let after_rejected_probe = b.emit_label();
23503            let unexpectedly_rejected = b.emit_label();
23504            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23505
23506            let r_bound = b.alloc_reg();
23507            let r_key = b.alloc_reg();
23508            let r_record = b.alloc_reg();
23509            let r_out = b.alloc_reg();
23510
23511            b.emit_op(Opcode::Integer, 1, r_bound, 0, P4::None, 0);
23512            b.emit_op(
23513                Opcode::SorterOpen,
23514                0,
23515                1,
23516                r_bound,
23517                P4::Str("+".to_owned()),
23518                SORTER_OPEN_TOP_N_REGISTER,
23519            );
23520            b.emit_op(Opcode::Integer, 10, r_key, 0, P4::None, 0);
23521            b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23522            b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23523
23524            // A later, worse key is rejected before payload evaluation.
23525            b.emit_op(Opcode::Integer, 20, r_key, 0, P4::None, 0);
23526            b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23527            b.emit_jump_to_label(
23528                Opcode::SorterCompare,
23529                0,
23530                r_record,
23531                rejected,
23532                P4::None,
23533                SORTER_COMPARE_TOP_N_PREFLIGHT,
23534            );
23535            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23536            b.emit_jump_to_label(Opcode::Goto, 0, 0, after_rejected_probe, P4::None, 0);
23537            b.resolve_label(rejected);
23538            b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23539            b.resolve_label(after_rejected_probe);
23540            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23541
23542            // A better key falls through and is eligible for projection.
23543            b.emit_op(Opcode::Integer, 5, r_key, 0, P4::None, 0);
23544            b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23545            b.emit_jump_to_label(
23546                Opcode::SorterCompare,
23547                0,
23548                r_record,
23549                unexpectedly_rejected,
23550                P4::None,
23551                SORTER_COMPARE_TOP_N_PREFLIGHT,
23552            );
23553            b.emit_op(Opcode::Integer, 3, r_out, 0, P4::None, 0);
23554            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23555            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23556
23557            b.resolve_label(unexpectedly_rejected);
23558            b.emit_op(Opcode::Integer, 4, r_out, 0, P4::None, 0);
23559            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23560            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23561            b.resolve_label(end);
23562        });
23563        let materializations_after = vdbe_test_sideband_materialization_count_snapshot();
23564
23565        assert_eq!(
23566            rows,
23567            vec![vec![SqliteValue::Integer(2)], vec![SqliteValue::Integer(3)],]
23568        );
23569        assert_eq!(
23570            materializations_after - materializations_before,
23571            0,
23572            "top-N preflight should consume MakeRecord sideband bytes without allocating an Arc-backed register blob"
23573        );
23574    }
23575
23576    #[test]
23577    fn test_sorter_open_zero_runtime_bound_retains_no_rows() {
23578        let rows = run_program(|b| {
23579            let end = b.emit_label();
23580            let empty = b.emit_label();
23581            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23582
23583            let r_bound = b.alloc_reg();
23584            let r_key = b.alloc_reg();
23585            let r_record = b.alloc_reg();
23586            let r_out = b.alloc_reg();
23587
23588            b.emit_op(Opcode::Integer, 0, r_bound, 0, P4::None, 0);
23589            b.emit_op(
23590                Opcode::SorterOpen,
23591                0,
23592                1,
23593                r_bound,
23594                P4::Str("+".to_owned()),
23595                SORTER_OPEN_TOP_N_REGISTER,
23596            );
23597            b.emit_op(Opcode::Integer, 10, r_key, 0, P4::None, 0);
23598            b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23599            b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23600            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23601
23602            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23603            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23604            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23605
23606            b.resolve_label(empty);
23607            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23608            b.resolve_label(end);
23609        });
23610
23611        assert!(rows.is_empty());
23612    }
23613
23614    #[test]
23615    fn test_sorter_column_reads_decode_non_key_payload_columns() {
23616        let rows = run_program(|b| {
23617            let end = b.emit_label();
23618            let loop_start = b.emit_label();
23619            let empty = b.emit_label();
23620            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23621
23622            let r_key = b.alloc_reg();
23623            let r_payload = b.alloc_reg();
23624            let r_record = b.alloc_reg();
23625            let r_out = b.alloc_reg();
23626
23627            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23628
23629            for (key, payload) in [(30, 300), (10, 100), (20, 200)] {
23630                b.emit_op(Opcode::Integer, key, r_key, 0, P4::None, 0);
23631                b.emit_op(Opcode::Integer, payload, r_payload, 0, P4::None, 0);
23632                b.emit_op(Opcode::MakeRecord, r_key, 2, r_record, P4::None, 0);
23633                b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23634            }
23635
23636            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23637            b.resolve_label(loop_start);
23638            b.emit_op(Opcode::Column, 0, 1, r_out, P4::None, 0);
23639            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23640            b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23641            b.resolve_label(empty);
23642            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23643            b.resolve_label(end);
23644        });
23645
23646        assert_eq!(
23647            rows,
23648            vec![
23649                vec![SqliteValue::Integer(100)],
23650                vec![SqliteValue::Integer(200)],
23651                vec![SqliteValue::Integer(300)],
23652            ]
23653        );
23654    }
23655
23656    #[test]
23657    fn test_reset_sorter_clears_entries() {
23658        let rows = run_program(|b| {
23659            let end = b.emit_label();
23660            let empty = b.emit_label();
23661            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23662
23663            let r_value = b.alloc_reg();
23664            let r_record = b.alloc_reg();
23665            let r_out = b.alloc_reg();
23666
23667            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23668            b.emit_op(Opcode::Integer, 7, r_value, 0, P4::None, 0);
23669            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23670            b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23671            b.emit_op(Opcode::ResetSorter, 0, 0, 0, P4::None, 0);
23672            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23673
23674            // If ResetSorter failed, this row would be emitted.
23675            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23676            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23677            b.resolve_label(empty);
23678            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23679            b.resolve_label(end);
23680        });
23681
23682        assert!(rows.is_empty());
23683    }
23684
23685    // ── Codegen → Engine Integration Tests ──────────────────────────────
23686
23687    mod codegen_integration {
23688        use super::*;
23689        use crate::codegen::{
23690            CodegenContext, ColumnInfo, TableSchema, codegen_delete, codegen_insert,
23691            codegen_select, codegen_update,
23692        };
23693        use fsqlite_ast::{
23694            Assignment, AssignmentTarget, BinaryOp as AstBinaryOp, ColumnRef, DeleteStatement,
23695            Distinctness, Expr, FromClause, InsertSource, InsertStatement, Literal,
23696            PlaceholderType, QualifiedName, QualifiedTableRef, ResultColumn, SelectBody,
23697            SelectCore, SelectStatement, Span, TableOrSubquery, UpdateStatement,
23698        };
23699
23700        fn test_schema() -> Vec<TableSchema> {
23701            vec![TableSchema {
23702                name: "t".to_owned(),
23703                root_page: 2,
23704                columns: vec![
23705                    ColumnInfo {
23706                        name: "a".to_owned(),
23707                        affinity: 'd',
23708                        is_ipk: false,
23709                        type_name: None,
23710                        notnull: false,
23711                        unique: false,
23712                        default_value: None,
23713                        strict_type: None,
23714                        generated_expr: None,
23715                        generated_stored: None,
23716                        collation: None,
23717                        conflict_action: None,
23718                    },
23719                    ColumnInfo {
23720                        name: "b".to_owned(),
23721                        affinity: 'C',
23722                        is_ipk: false,
23723                        type_name: None,
23724                        notnull: false,
23725                        unique: false,
23726                        default_value: None,
23727                        strict_type: None,
23728                        generated_expr: None,
23729                        generated_stored: None,
23730                        collation: None,
23731                        conflict_action: None,
23732                    },
23733                ],
23734                indexes: vec![],
23735                strict: false,
23736                without_rowid: false,
23737                primary_key_constraints: Vec::new(),
23738                foreign_keys: Vec::new(),
23739                check_constraints: Vec::new(),
23740            }]
23741        }
23742
23743        fn from_table(name: &str) -> FromClause {
23744            FromClause {
23745                source: TableOrSubquery::Table {
23746                    name: QualifiedName {
23747                        schema: None,
23748                        name: name.to_owned(),
23749                    },
23750                    alias: None,
23751                    index_hint: None,
23752                    time_travel: None,
23753                },
23754                joins: Vec::new(),
23755            }
23756        }
23757
23758        fn span() -> Span {
23759            Span { start: 0, end: 0 }
23760        }
23761
23762        /// Verify codegen_insert produces a program that executes without panic.
23763        #[test]
23764        fn test_codegen_insert_executes() {
23765            let schema = test_schema();
23766            let ctx = CodegenContext::default();
23767
23768            let stmt = InsertStatement {
23769                with: None,
23770                or_conflict: None,
23771                table: QualifiedName {
23772                    schema: None,
23773                    name: "t".to_owned(),
23774                },
23775                alias: None,
23776                columns: vec![],
23777                source: InsertSource::Values(vec![vec![
23778                    Expr::Literal(Literal::Integer(42), span()),
23779                    Expr::Literal(Literal::String("hello".to_owned()), span()),
23780                ]]),
23781                upsert: vec![],
23782                returning: vec![],
23783            };
23784
23785            let mut b = ProgramBuilder::new();
23786            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23787            let prog = b.finish().expect("program should build");
23788
23789            let mut engine = VdbeEngine::new(prog.register_count());
23790            engine.set_reject_mem_fallback(false);
23791            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23792            assert_eq!(outcome, ExecOutcome::Done);
23793        }
23794
23795        /// Verify codegen_select (full scan) produces a program that executes.
23796        #[test]
23797        fn test_codegen_select_full_scan_executes() {
23798            let schema = test_schema();
23799            let ctx = CodegenContext::default();
23800
23801            let stmt = SelectStatement {
23802                with: None,
23803                body: SelectBody {
23804                    select: SelectCore::Select {
23805                        distinct: Distinctness::All,
23806                        columns: vec![ResultColumn::Star],
23807                        from: Some(from_table("t")),
23808                        where_clause: None,
23809                        group_by: vec![],
23810                        having: None,
23811                        windows: vec![],
23812                    },
23813                    compounds: vec![],
23814                },
23815                order_by: vec![],
23816                limit: None,
23817            };
23818
23819            let mut b = ProgramBuilder::new();
23820            codegen_select(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23821            let prog = b.finish().expect("program should build");
23822
23823            // Engine should execute without panic (cursor ops are stubbed).
23824            let mut engine = VdbeEngine::new(prog.register_count());
23825            engine.set_reject_mem_fallback(false);
23826            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23827            assert_eq!(outcome, ExecOutcome::Done);
23828        }
23829
23830        /// Verify `OpenRead` can route through the storage-backed cursor path.
23831        #[test]
23832        fn test_openread_uses_storage_cursor_backend_when_enabled() {
23833            let mut b = ProgramBuilder::new();
23834            b.emit_op(Opcode::OpenRead, 0, 2, 0, P4::None, 0);
23835            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23836            let prog = b.finish().expect("program should build");
23837
23838            let mut db = MemDatabase::new();
23839            let root = db.create_table(1);
23840            assert_eq!(root, 2);
23841            if let Some(table) = db.get_table_mut(root) {
23842                table.insert(1, vec![SqliteValue::Integer(99)]);
23843            }
23844
23845            let mut engine = VdbeEngine::new(prog.register_count());
23846            engine.enable_storage_read_cursors(true);
23847            engine.set_database(db);
23848            engine.set_reject_mem_fallback(false);
23849            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23850            assert_eq!(outcome, ExecOutcome::Done);
23851            assert!(engine.storage_cursors.contains_key(&0));
23852            assert!(!engine.cursors.contains_key(&0));
23853        }
23854
23855        /// Verify codegen_update produces a program that executes.
23856        #[test]
23857        fn test_codegen_update_executes() {
23858            let schema = test_schema();
23859            let ctx = CodegenContext::default();
23860
23861            let stmt = UpdateStatement {
23862                with: None,
23863                or_conflict: None,
23864                table: QualifiedTableRef {
23865                    name: QualifiedName {
23866                        schema: None,
23867                        name: "t".to_owned(),
23868                    },
23869                    alias: None,
23870                    index_hint: None,
23871                    time_travel: None,
23872                },
23873                assignments: vec![Assignment {
23874                    target: AssignmentTarget::Column("b".to_owned()),
23875                    value: Expr::Placeholder(PlaceholderType::Numbered(1), span()),
23876                }],
23877                from: None,
23878                where_clause: Some(Expr::BinaryOp {
23879                    left: Box::new(Expr::Column(
23880                        ColumnRef {
23881                            table: None,
23882                            column: "rowid".into(),
23883                        },
23884                        span(),
23885                    )),
23886                    op: AstBinaryOp::Eq,
23887                    right: Box::new(Expr::Placeholder(PlaceholderType::Numbered(2), span())),
23888                    span: span(),
23889                }),
23890                returning: vec![],
23891                order_by: vec![],
23892                limit: None,
23893            };
23894
23895            let mut b = ProgramBuilder::new();
23896            codegen_update(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23897            let prog = b.finish().expect("program should build");
23898
23899            let mut engine = VdbeEngine::new(prog.register_count());
23900            engine.set_reject_mem_fallback(false);
23901            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23902            assert_eq!(outcome, ExecOutcome::Done);
23903        }
23904
23905        /// Verify codegen_delete produces a program that executes.
23906        #[test]
23907        fn test_codegen_delete_executes() {
23908            let schema = test_schema();
23909            let ctx = CodegenContext::default();
23910
23911            let stmt = DeleteStatement {
23912                with: None,
23913                table: QualifiedTableRef {
23914                    name: QualifiedName {
23915                        schema: None,
23916                        name: "t".to_owned(),
23917                    },
23918                    alias: None,
23919                    index_hint: None,
23920                    time_travel: None,
23921                },
23922                where_clause: Some(Expr::BinaryOp {
23923                    left: Box::new(Expr::Column(
23924                        ColumnRef {
23925                            table: None,
23926                            column: "rowid".into(),
23927                        },
23928                        span(),
23929                    )),
23930                    op: AstBinaryOp::Eq,
23931                    right: Box::new(Expr::Placeholder(PlaceholderType::Numbered(1), span())),
23932                    span: span(),
23933                }),
23934                returning: vec![],
23935                order_by: vec![],
23936                limit: None,
23937            };
23938
23939            let mut b = ProgramBuilder::new();
23940            codegen_delete(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23941            let prog = b.finish().expect("program should build");
23942
23943            let mut engine = VdbeEngine::new(prog.register_count());
23944            engine.set_reject_mem_fallback(false);
23945            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23946            assert_eq!(outcome, ExecOutcome::Done);
23947        }
23948
23949        /// Verify codegen_insert with RETURNING produces a ResultRow.
23950        #[test]
23951        fn test_codegen_insert_returning_produces_result() {
23952            let schema = test_schema();
23953            let ctx = CodegenContext::default();
23954
23955            let stmt = InsertStatement {
23956                with: None,
23957                or_conflict: None,
23958                table: QualifiedName {
23959                    schema: None,
23960                    name: "t".to_owned(),
23961                },
23962                alias: None,
23963                columns: vec![],
23964                source: InsertSource::Values(vec![vec![
23965                    Expr::Literal(Literal::Integer(7), span()),
23966                    Expr::Literal(Literal::String("world".to_owned()), span()),
23967                ]]),
23968                upsert: vec![],
23969                returning: vec![ResultColumn::Star],
23970            };
23971
23972            let mut b = ProgramBuilder::new();
23973            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23974            let prog = b.finish().expect("program should build");
23975
23976            // Provide a MemDatabase so Insert stores the row and SeekRowid
23977            // (used by emit_returning) can find it.
23978            let mut db = MemDatabase::new();
23979            let root = db.create_table(2);
23980            assert_eq!(root, 2);
23981
23982            let mut engine = VdbeEngine::new(prog.register_count());
23983            engine.set_database(db);
23984            engine.set_reject_mem_fallback(false);
23985            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23986            assert_eq!(outcome, ExecOutcome::Done);
23987            // RETURNING * emits a ResultRow with all columns.
23988            assert_eq!(engine.results().len(), 1);
23989        }
23990
23991        /// Verify INSERT with literal values emits the correct value registers.
23992        #[test]
23993        fn test_codegen_insert_literal_values_disassemble() {
23994            let schema = test_schema();
23995            let ctx = CodegenContext::default();
23996
23997            let stmt = InsertStatement {
23998                with: None,
23999                or_conflict: None,
24000                table: QualifiedName {
24001                    schema: None,
24002                    name: "t".to_owned(),
24003                },
24004                alias: None,
24005                columns: vec![],
24006                source: InsertSource::Values(vec![vec![
24007                    Expr::Literal(Literal::Integer(99), span()),
24008                    Expr::Literal(Literal::String("test".to_owned()), span()),
24009                ]]),
24010                upsert: vec![],
24011                returning: vec![],
24012            };
24013
24014            let mut b = ProgramBuilder::new();
24015            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24016            let prog = b.finish().expect("program should build");
24017
24018            let asm = prog.disassemble();
24019            let insert = prog
24020                .ops()
24021                .iter()
24022                .find(|op| op.opcode == Opcode::Insert)
24023                .expect("program should contain Insert");
24024            assert!(asm.contains("Init"), "should have Init opcode");
24025            assert!(asm.contains("OpenWrite"), "should have OpenWrite opcode");
24026            assert!(asm.contains("NewRowid"), "should have NewRowid opcode");
24027            assert!(
24028                asm.contains("Integer"),
24029                "should have Integer opcode for literal 99"
24030            );
24031            assert!(
24032                asm.contains("String8"),
24033                "should have String8 opcode for literal 'test'"
24034            );
24035            assert!(
24036                !prog.ops().iter().any(|op| op.opcode == Opcode::Blob),
24037                "preformatted table records should avoid a standalone Blob opcode"
24038            );
24039            assert!(
24040                !asm.contains("MakeRecord"),
24041                "literal-only INSERT should not execute MakeRecord for the table record"
24042            );
24043            assert!(
24044                matches!(&insert.p4, P4::Blob(record) if !record.is_empty()),
24045                "Insert should carry the preformatted table record directly in P4"
24046            );
24047            assert!(asm.contains("Insert"), "should have Insert opcode");
24048            assert!(asm.contains("Halt"), "should have Halt opcode");
24049        }
24050
24051        #[test]
24052        fn test_codegen_insert_literal_values_execute_without_make_record_opcode() {
24053            let schema = test_schema();
24054            let ctx = CodegenContext::default();
24055            let stmt = InsertStatement {
24056                with: None,
24057                or_conflict: None,
24058                table: QualifiedName {
24059                    schema: None,
24060                    name: "t".to_owned(),
24061                },
24062                alias: None,
24063                columns: vec![],
24064                source: InsertSource::Values(vec![vec![
24065                    Expr::Literal(Literal::Integer(99), span()),
24066                    Expr::Literal(Literal::String("test".to_owned()), span()),
24067                ]]),
24068                upsert: vec![],
24069                returning: vec![ResultColumn::Star],
24070            };
24071
24072            let mut b = ProgramBuilder::new();
24073            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24074            let prog = b.finish().expect("program should build");
24075            assert!(
24076                !prog.ops().iter().any(|op| op.opcode == Opcode::MakeRecord),
24077                "preformatted INSERT should skip table-row MakeRecord execution"
24078            );
24079
24080            let mut db = MemDatabase::new();
24081            let _root = db.create_table(2);
24082
24083            let mut engine = VdbeEngine::new(prog.register_count());
24084            engine.enable_storage_cursors(true);
24085            engine.set_database(db);
24086            engine.set_reject_mem_fallback(false);
24087            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24088            assert_eq!(outcome, ExecOutcome::Done);
24089
24090            let rows: Vec<_> = engine
24091                .take_results()
24092                .into_iter()
24093                .map(|row| row.into_vec())
24094                .collect();
24095
24096            assert_eq!(
24097                rows,
24098                vec![vec![
24099                    SqliteValue::Integer(99),
24100                    SqliteValue::Text("test".into())
24101                ]]
24102            );
24103        }
24104
24105        #[test]
24106        fn test_codegen_insert_large_literal_blob_round_trips_via_preformatted_insert() {
24107            let schema = vec![TableSchema {
24108                name: "t".to_owned(),
24109                root_page: 2,
24110                columns: vec![
24111                    ColumnInfo {
24112                        name: "a".to_owned(),
24113                        affinity: 'd',
24114                        is_ipk: false,
24115                        type_name: None,
24116                        notnull: false,
24117                        unique: false,
24118                        default_value: None,
24119                        strict_type: None,
24120                        generated_expr: None,
24121                        generated_stored: None,
24122                        collation: None,
24123                        conflict_action: None,
24124                    },
24125                    ColumnInfo {
24126                        name: "b".to_owned(),
24127                        affinity: 'A',
24128                        is_ipk: false,
24129                        type_name: None,
24130                        notnull: false,
24131                        unique: false,
24132                        default_value: None,
24133                        strict_type: None,
24134                        generated_expr: None,
24135                        generated_stored: None,
24136                        collation: None,
24137                        conflict_action: None,
24138                    },
24139                ],
24140                indexes: vec![],
24141                strict: false,
24142                without_rowid: false,
24143                primary_key_constraints: Vec::new(),
24144                foreign_keys: Vec::new(),
24145                check_constraints: Vec::new(),
24146            }];
24147            let large_blob = vec![0xAB; 5_000];
24148            let stmt = InsertStatement {
24149                with: None,
24150                or_conflict: None,
24151                table: QualifiedName {
24152                    schema: None,
24153                    name: "t".to_owned(),
24154                },
24155                alias: None,
24156                columns: vec![],
24157                source: InsertSource::Values(vec![vec![
24158                    Expr::Literal(Literal::Integer(7), span()),
24159                    Expr::Literal(Literal::Blob(large_blob.clone()), span()),
24160                ]]),
24161                upsert: vec![],
24162                returning: vec![ResultColumn::Star],
24163            };
24164            let ctx = CodegenContext::default();
24165
24166            let mut b = ProgramBuilder::new();
24167            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24168            let prog = b.finish().expect("program should build");
24169
24170            let insert = prog
24171                .ops()
24172                .iter()
24173                .find(|op| op.opcode == Opcode::Insert)
24174                .expect("program should contain Insert");
24175            assert!(
24176                !prog
24177                    .ops()
24178                    .iter()
24179                    .any(|op| op.opcode == Opcode::Blob && op.p2 == insert.p2),
24180                "preformatted literal rows should not materialize the table record into a Blob register"
24181            );
24182            assert!(
24183                matches!(&insert.p4, P4::Blob(record) if record.len() > 4_096),
24184                "Insert should carry the full preformatted record, including large overflow-bound payloads"
24185            );
24186
24187            let mut db = MemDatabase::new();
24188            let root = db.create_table(2);
24189            assert_eq!(root, 2);
24190
24191            let mut engine = VdbeEngine::new(prog.register_count());
24192            engine.enable_storage_cursors(true);
24193            engine.set_database(db);
24194            engine.set_reject_mem_fallback(false);
24195            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24196            assert_eq!(outcome, ExecOutcome::Done);
24197
24198            let rows: Vec<_> = engine
24199                .take_results()
24200                .into_iter()
24201                .map(|row| row.into_vec())
24202                .collect();
24203            assert_eq!(
24204                rows,
24205                vec![vec![
24206                    SqliteValue::Integer(7),
24207                    SqliteValue::Blob(large_blob.into()),
24208                ]],
24209                "large literal blob should round-trip through storage even when the record is preformatted on Insert"
24210            );
24211        }
24212
24213        #[test]
24214        fn test_codegen_insert_current_timestamp_uses_runtime_make_record() {
24215            let schema = test_schema();
24216            let ctx = CodegenContext::default();
24217            let stmt = InsertStatement {
24218                with: None,
24219                or_conflict: None,
24220                table: QualifiedName {
24221                    schema: None,
24222                    name: "t".to_owned(),
24223                },
24224                alias: None,
24225                columns: vec![],
24226                source: InsertSource::Values(vec![vec![
24227                    Expr::Literal(Literal::CurrentTimestamp, span()),
24228                    Expr::Literal(Literal::String("runtime".to_owned()), span()),
24229                ]]),
24230                upsert: vec![],
24231                returning: vec![],
24232            };
24233
24234            let mut b = ProgramBuilder::new();
24235            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24236            let prog = b.finish().expect("program should build");
24237            let insert = prog
24238                .ops()
24239                .iter()
24240                .find(|op| op.opcode == Opcode::Insert)
24241                .expect("program should contain Insert");
24242
24243            assert!(
24244                prog.ops().iter().any(|op| op.opcode == Opcode::MakeRecord),
24245                "volatile CURRENT_TIMESTAMP values must stay on the runtime record assembly path"
24246            );
24247            assert!(
24248                !matches!(&insert.p4, P4::Blob(_)),
24249                "volatile CURRENT_TIMESTAMP values must not be captured in a preformatted Insert payload"
24250            );
24251        }
24252
24253        /// Verify emit_expr handles arithmetic BinaryOp in INSERT values.
24254        #[test]
24255        fn test_codegen_insert_arithmetic_expr() {
24256            let schema = test_schema();
24257            let ctx = CodegenContext::default();
24258
24259            // INSERT INTO t VALUES (2 + 3, 'hi')
24260            let stmt = InsertStatement {
24261                with: None,
24262                or_conflict: None,
24263                table: QualifiedName {
24264                    schema: None,
24265                    name: "t".to_owned(),
24266                },
24267                alias: None,
24268                columns: vec![],
24269                source: InsertSource::Values(vec![vec![
24270                    Expr::BinaryOp {
24271                        left: Box::new(Expr::Literal(Literal::Integer(2), span())),
24272                        op: AstBinaryOp::Add,
24273                        right: Box::new(Expr::Literal(Literal::Integer(3), span())),
24274                        span: span(),
24275                    },
24276                    Expr::Literal(Literal::String("hi".to_owned()), span()),
24277                ]]),
24278                upsert: vec![],
24279                returning: vec![],
24280            };
24281
24282            let mut b = ProgramBuilder::new();
24283            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24284            let prog = b.finish().expect("program should build");
24285
24286            let asm = prog.disassemble();
24287            assert!(asm.contains("Add"), "should have Add opcode for 2+3");
24288            assert!(asm.contains("Integer"), "should have Integer opcodes");
24289
24290            let mut engine = VdbeEngine::new(prog.register_count());
24291            engine.set_reject_mem_fallback(false);
24292            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24293            assert_eq!(outcome, ExecOutcome::Done);
24294        }
24295
24296        /// Verify emit_expr handles UnaryOp (negation) in INSERT values.
24297        #[test]
24298        fn test_codegen_insert_negation_expr() {
24299            use fsqlite_ast::UnaryOp as AstUnaryOp;
24300
24301            let schema = test_schema();
24302            let ctx = CodegenContext::default();
24303
24304            // INSERT INTO t VALUES (-42, 'neg')
24305            let stmt = InsertStatement {
24306                with: None,
24307                or_conflict: None,
24308                table: QualifiedName {
24309                    schema: None,
24310                    name: "t".to_owned(),
24311                },
24312                alias: None,
24313                columns: vec![],
24314                source: InsertSource::Values(vec![vec![
24315                    Expr::UnaryOp {
24316                        op: AstUnaryOp::Negate,
24317                        expr: Box::new(Expr::Literal(Literal::Integer(42), span())),
24318                        span: span(),
24319                    },
24320                    Expr::Literal(Literal::String("neg".to_owned()), span()),
24321                ]]),
24322                upsert: vec![],
24323                returning: vec![],
24324            };
24325
24326            let mut b = ProgramBuilder::new();
24327            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24328            let prog = b.finish().expect("program should build");
24329
24330            let asm = prog.disassemble();
24331            assert!(asm.contains("Multiply"), "negation emits Multiply by -1");
24332
24333            let mut engine = VdbeEngine::new(prog.register_count());
24334            engine.set_reject_mem_fallback(false);
24335            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24336            assert_eq!(outcome, ExecOutcome::Done);
24337        }
24338
24339        /// Verify emit_expr handles CASE expression in INSERT values.
24340        #[test]
24341        fn test_codegen_insert_case_expr() {
24342            let schema = test_schema();
24343            let ctx = CodegenContext::default();
24344
24345            // INSERT INTO t VALUES (CASE WHEN TRUE THEN 10 ELSE 20 END, 'case')
24346            let stmt = InsertStatement {
24347                with: None,
24348                or_conflict: None,
24349                table: QualifiedName {
24350                    schema: None,
24351                    name: "t".to_owned(),
24352                },
24353                alias: None,
24354                columns: vec![],
24355                source: InsertSource::Values(vec![vec![
24356                    Expr::Case {
24357                        operand: None,
24358                        whens: vec![(
24359                            Expr::Literal(Literal::True, span()),
24360                            Expr::Literal(Literal::Integer(10), span()),
24361                        )],
24362                        else_expr: Some(Box::new(Expr::Literal(Literal::Integer(20), span()))),
24363                        span: span(),
24364                    },
24365                    Expr::Literal(Literal::String("case".to_owned()), span()),
24366                ]]),
24367                upsert: vec![],
24368                returning: vec![],
24369            };
24370
24371            let mut b = ProgramBuilder::new();
24372            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24373            let prog = b.finish().expect("program should build");
24374
24375            let asm = prog.disassemble();
24376            assert!(asm.contains("IfNot"), "searched CASE emits IfNot");
24377            assert!(asm.contains("Goto"), "CASE branches with Goto");
24378
24379            let mut engine = VdbeEngine::new(prog.register_count());
24380            engine.set_reject_mem_fallback(false);
24381            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24382            assert_eq!(outcome, ExecOutcome::Done);
24383        }
24384
24385        /// Verify emit_expr handles comparison expression producing 0/1 result.
24386        #[test]
24387        fn test_codegen_insert_comparison_expr() {
24388            let schema = test_schema();
24389            let ctx = CodegenContext::default();
24390
24391            // INSERT INTO t VALUES (3 > 2, 'cmp') — should produce integer 1
24392            let stmt = InsertStatement {
24393                with: None,
24394                or_conflict: None,
24395                table: QualifiedName {
24396                    schema: None,
24397                    name: "t".to_owned(),
24398                },
24399                alias: None,
24400                columns: vec![],
24401                source: InsertSource::Values(vec![vec![
24402                    Expr::BinaryOp {
24403                        left: Box::new(Expr::Literal(Literal::Integer(3), span())),
24404                        op: AstBinaryOp::Gt,
24405                        right: Box::new(Expr::Literal(Literal::Integer(2), span())),
24406                        span: span(),
24407                    },
24408                    Expr::Literal(Literal::String("cmp".to_owned()), span()),
24409                ]]),
24410                upsert: vec![],
24411                returning: vec![],
24412            };
24413
24414            let mut b = ProgramBuilder::new();
24415            codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24416            let prog = b.finish().expect("program should build");
24417
24418            let asm = prog.disassemble();
24419            assert!(asm.contains("Gt"), "comparison emits Gt opcode");
24420
24421            let mut engine = VdbeEngine::new(prog.register_count());
24422            engine.set_reject_mem_fallback(false);
24423            let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24424            assert_eq!(outcome, ExecOutcome::Done);
24425        }
24426    }
24427
24428    // ===================================================================
24429    // bd-202x §16 Phase 4: Comprehensive VDBE opcode unit tests
24430    // ===================================================================
24431
24432    // ── Constants & Register Operations ────────────────────────────────
24433
24434    #[test]
24435    fn test_int64_large_value() {
24436        let rows = run_program(|b| {
24437            let end = b.emit_label();
24438            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24439            let r = b.alloc_reg();
24440            b.emit_op(Opcode::Int64, 0, r, 0, P4::Int64(i64::MAX), 0);
24441            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24442            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24443            b.resolve_label(end);
24444        });
24445        assert_eq!(rows[0], vec![SqliteValue::Integer(i64::MAX)]);
24446    }
24447
24448    #[test]
24449    fn test_int64_negative() {
24450        let rows = run_program(|b| {
24451            let end = b.emit_label();
24452            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24453            let r = b.alloc_reg();
24454            b.emit_op(Opcode::Int64, 0, r, 0, P4::Int64(-999_999_999_999), 0);
24455            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24456            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24457            b.resolve_label(end);
24458        });
24459        assert_eq!(rows[0], vec![SqliteValue::Integer(-999_999_999_999)]);
24460    }
24461
24462    #[test]
24463    fn test_real_constant() {
24464        let rows = run_program(|b| {
24465            let end = b.emit_label();
24466            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24467            let r = b.alloc_reg();
24468            b.emit_op(Opcode::Real, 0, r, 0, P4::Real(std::f64::consts::PI), 0);
24469            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24470            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24471            b.resolve_label(end);
24472        });
24473        assert_eq!(rows[0], vec![SqliteValue::Float(std::f64::consts::PI)]);
24474    }
24475
24476    #[test]
24477    fn test_real_negative_zero() {
24478        let rows = run_program(|b| {
24479            let end = b.emit_label();
24480            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24481            let r = b.alloc_reg();
24482            b.emit_op(Opcode::Real, 0, r, 0, P4::Real(0.0), 0);
24483            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24484            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24485            b.resolve_label(end);
24486        });
24487        assert_eq!(rows[0], vec![SqliteValue::Float(0.0)]);
24488    }
24489
24490    #[test]
24491    fn test_string_opcode() {
24492        let rows = run_program(|b| {
24493            let end = b.emit_label();
24494            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24495            let r = b.alloc_reg();
24496            b.emit_op(Opcode::String, 5, r, 0, P4::Str("hello".to_owned()), 0);
24497            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24498            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24499            b.resolve_label(end);
24500        });
24501        assert_eq!(rows[0], vec![SqliteValue::Text("hello".into())]);
24502    }
24503
24504    #[test]
24505    fn test_blob_constant() {
24506        let rows = run_program(|b| {
24507            let end = b.emit_label();
24508            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24509            let r = b.alloc_reg();
24510            b.emit_op(
24511                Opcode::Blob,
24512                0,
24513                r,
24514                0,
24515                P4::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
24516                0,
24517            );
24518            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24519            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24520            b.resolve_label(end);
24521        });
24522        assert_eq!(
24523            rows[0],
24524            vec![SqliteValue::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF].into())]
24525        );
24526    }
24527
24528    #[test]
24529    fn test_null_range() {
24530        // Null with p3=2: set registers p2, p2+1, p2+2 to NULL.
24531        let rows = run_program(|b| {
24532            let end = b.emit_label();
24533            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24534            let r1 = b.alloc_reg();
24535            let r2 = b.alloc_reg();
24536            let r3 = b.alloc_reg();
24537            // Pre-populate with integers
24538            b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
24539            b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
24540            b.emit_op(Opcode::Integer, 3, r3, 0, P4::None, 0);
24541            // Null range: p2=r1, p3=r3 → set r1..=r3 to NULL (absolute end register).
24542            b.emit_op(Opcode::Null, 0, r1, r3, P4::None, 0);
24543            // Repeat the range write to exercise the already-NULL path.
24544            b.emit_op(Opcode::Null, 0, r1, r3, P4::None, 0);
24545            b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24546            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24547            b.resolve_label(end);
24548        });
24549        assert_eq!(
24550            rows[0],
24551            vec![SqliteValue::Null, SqliteValue::Null, SqliteValue::Null]
24552        );
24553    }
24554
24555    #[test]
24556    fn test_null_clears_already_null_sideband_and_subtype() {
24557        let rows = run_program(|b| {
24558            let end = b.emit_label();
24559            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24560            let r_value = b.alloc_reg();
24561            let r_record = b.alloc_reg();
24562            let r_subtype = b.alloc_reg();
24563            let r_observed_subtype = b.alloc_reg();
24564
24565            // MakeRecord leaves a logical record in the lookaside sideband
24566            // while the physical destination register remains NULL.
24567            b.emit_op(Opcode::Integer, 42, r_value, 0, P4::None, 0);
24568            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
24569            b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
24570            b.emit_op(Opcode::SetSubtype, r_subtype, r_record, 0, P4::None, 0);
24571
24572            b.emit_op(Opcode::Null, 0, r_record, 0, P4::None, 0);
24573            b.emit_op(
24574                Opcode::GetSubtype,
24575                r_record,
24576                r_observed_subtype,
24577                0,
24578                P4::None,
24579                0,
24580            );
24581            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
24582            b.emit_op(Opcode::ResultRow, r_observed_subtype, 1, 0, P4::None, 0);
24583            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24584            b.resolve_label(end);
24585        });
24586
24587        assert_eq!(
24588            rows,
24589            vec![vec![SqliteValue::Null], vec![SqliteValue::Integer(0)],]
24590        );
24591    }
24592
24593    #[test]
24594    fn test_soft_null() {
24595        let rows = run_program(|b| {
24596            let end = b.emit_label();
24597            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24598            let r = b.alloc_reg();
24599            b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
24600            b.emit_op(Opcode::SoftNull, r, 0, 0, P4::None, 0);
24601            // The second write exercises the already-NULL fast path.
24602            b.emit_op(Opcode::SoftNull, r, 0, 0, P4::None, 0);
24603            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24604            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24605            b.resolve_label(end);
24606        });
24607        assert_eq!(rows[0], vec![SqliteValue::Null]);
24608    }
24609
24610    #[test]
24611    fn test_soft_null_clears_already_null_sideband_and_subtype() {
24612        let rows = run_program(|b| {
24613            let end = b.emit_label();
24614            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24615            let r_value = b.alloc_reg();
24616            let r_record = b.alloc_reg();
24617            let r_subtype = b.alloc_reg();
24618            let r_observed_subtype = b.alloc_reg();
24619
24620            // MakeRecord leaves a logical record in the lookaside sideband
24621            // while the physical destination register remains NULL.
24622            b.emit_op(Opcode::Integer, 42, r_value, 0, P4::None, 0);
24623            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
24624            b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
24625            b.emit_op(Opcode::SetSubtype, r_subtype, r_record, 0, P4::None, 0);
24626
24627            b.emit_op(Opcode::SoftNull, r_record, 0, 0, P4::None, 0);
24628            b.emit_op(
24629                Opcode::GetSubtype,
24630                r_record,
24631                r_observed_subtype,
24632                0,
24633                P4::None,
24634                0,
24635            );
24636            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
24637            b.emit_op(Opcode::ResultRow, r_observed_subtype, 1, 0, P4::None, 0);
24638            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24639            b.resolve_label(end);
24640        });
24641
24642        assert_eq!(
24643            rows,
24644            vec![vec![SqliteValue::Null], vec![SqliteValue::Integer(0)],]
24645        );
24646    }
24647
24648    #[test]
24649    fn test_move_registers() {
24650        // Move nullifies source and copies to destination.
24651        let rows = run_program(|b| {
24652            let end = b.emit_label();
24653            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24654            let src = b.alloc_reg();
24655            let dst = b.alloc_reg();
24656            b.emit_op(Opcode::Integer, 77, src, 0, P4::None, 0);
24657            // Move 1 register from src to dst
24658            b.emit_op(Opcode::Move, src, dst, 1, P4::None, 0);
24659            // dst should be 77, src should be NULL
24660            b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24661            b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24662            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24663            b.resolve_label(end);
24664        });
24665        assert_eq!(rows[0], vec![SqliteValue::Integer(77)]);
24666        assert_eq!(rows[1], vec![SqliteValue::Null]);
24667    }
24668
24669    #[test]
24670    fn test_move_register_range_preserves_forward_overlap() {
24671        let rows = run_program(|b| {
24672            let end = b.emit_label();
24673            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24674            let r1 = b.alloc_reg();
24675            let r2 = b.alloc_reg();
24676            let r3 = b.alloc_reg();
24677            b.emit_op(Opcode::Integer, 11, r1, 0, P4::None, 0);
24678            b.emit_op(Opcode::Integer, 22, r2, 0, P4::None, 0);
24679            b.emit_op(Opcode::Integer, 33, r3, 0, P4::None, 0);
24680            b.emit_op(Opcode::Move, r1, r2, 2, P4::None, 0);
24681            b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24682            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24683            b.resolve_label(end);
24684        });
24685        assert_eq!(
24686            rows[0],
24687            vec![
24688                SqliteValue::Null,
24689                SqliteValue::Integer(11),
24690                SqliteValue::Integer(22),
24691            ]
24692        );
24693    }
24694
24695    #[test]
24696    fn test_move_register_range_preserves_backward_overlap() {
24697        let rows = run_program(|b| {
24698            let end = b.emit_label();
24699            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24700            let r1 = b.alloc_reg();
24701            let r2 = b.alloc_reg();
24702            let r3 = b.alloc_reg();
24703            b.emit_op(Opcode::Integer, 11, r1, 0, P4::None, 0);
24704            b.emit_op(Opcode::Integer, 22, r2, 0, P4::None, 0);
24705            b.emit_op(Opcode::Integer, 33, r3, 0, P4::None, 0);
24706            b.emit_op(Opcode::Move, r2, r1, 2, P4::None, 0);
24707            b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24708            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24709            b.resolve_label(end);
24710        });
24711        assert_eq!(
24712            rows[0],
24713            vec![
24714                SqliteValue::Integer(22),
24715                SqliteValue::Integer(33),
24716                SqliteValue::Null,
24717            ]
24718        );
24719    }
24720
24721    #[test]
24722    fn test_copy_register() {
24723        let rows = run_program(|b| {
24724            let end = b.emit_label();
24725            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24726            let src = b.alloc_reg();
24727            let dst = b.alloc_reg();
24728            b.emit_op(Opcode::String8, 0, src, 0, P4::Str("copy_me".to_owned()), 0);
24729            b.emit_op(Opcode::Copy, src, dst, 0, P4::None, 0);
24730            // Both should be the same value
24731            b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24732            b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24733            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24734            b.resolve_label(end);
24735        });
24736        assert_eq!(rows[0], vec![SqliteValue::Text("copy_me".into())]);
24737        assert_eq!(rows[1], vec![SqliteValue::Text("copy_me".into())]);
24738    }
24739
24740    #[test]
24741    fn test_copy_integer_updates_payload_and_preserves_write_bookkeeping() {
24742        let mut engine = VdbeEngine::new(4);
24743        engine.set_reg(1, SqliteValue::Integer(41));
24744        engine.set_reg(2, SqliteValue::Integer(7));
24745        engine.set_register_subtype(2, 74);
24746
24747        engine.copy_single_reg(1, 2);
24748        assert_eq!(engine.get_reg(2), &SqliteValue::Integer(41));
24749        assert!(engine.register_subtype(2).is_none());
24750
24751        engine.set_register_subtype(1, 74);
24752        engine.copy_single_reg(1, 1);
24753        assert_eq!(engine.get_reg(1), &SqliteValue::Integer(41));
24754        assert!(engine.register_subtype(1).is_none());
24755
24756        let growing_dst = i32::try_from(engine.registers.len()).expect("small register file");
24757        engine.copy_single_reg(1, growing_dst);
24758        assert_eq!(engine.get_reg(growing_dst), &SqliteValue::Integer(41));
24759    }
24760
24761    #[test]
24762    fn test_scopy_register_via_hot_path() {
24763        // SCopy (shallow copy) lives in `try_execute_hot_opcode`: this test
24764        // pins the hot-path arm to the same single-register `clone + write`
24765        // semantics as the main-match arm. Uses a heap-carrying Text so a
24766        // subsequent mutation of the destination can't alias the source.
24767        let rows = run_program(|b| {
24768            let end = b.emit_label();
24769            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24770            let src = b.alloc_reg();
24771            let dst = b.alloc_reg();
24772            b.emit_op(
24773                Opcode::String8,
24774                0,
24775                src,
24776                0,
24777                P4::Str("shallow_me".to_owned()),
24778                0,
24779            );
24780            b.emit_op(Opcode::SCopy, src, dst, 0, P4::None, 0);
24781            b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24782            b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24783            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24784            b.resolve_label(end);
24785        });
24786        assert_eq!(rows[0], vec![SqliteValue::Text("shallow_me".into())]);
24787        assert_eq!(rows[1], vec![SqliteValue::Text("shallow_me".into())]);
24788    }
24789
24790    #[test]
24791    fn test_string8_via_hot_path() {
24792        let rows = run_program(|b| {
24793            let end = b.emit_label();
24794            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24795            let r1 = b.alloc_reg();
24796            let r2 = b.alloc_reg();
24797            b.emit_op(
24798                Opcode::String8,
24799                0,
24800                r1,
24801                0,
24802                P4::Str("hello_hot".to_owned()),
24803                0,
24804            );
24805            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str(String::new()), 0);
24806            b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
24807            b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
24808            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24809            b.resolve_label(end);
24810        });
24811        assert_eq!(rows[0], vec![SqliteValue::Text("hello_hot".into())]);
24812        assert_eq!(rows[1], vec![SqliteValue::Text("".into())]);
24813    }
24814
24815    #[test]
24816    fn test_string8_hot_path_reuses_existing_text_buffer() {
24817        let rows = run_program(|b| {
24818            let end = b.emit_label();
24819            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24820            let r = b.alloc_reg();
24821            b.emit_op(
24822                Opcode::String8,
24823                0,
24824                r,
24825                0,
24826                P4::Str("first_value".to_owned()),
24827                0,
24828            );
24829            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24830            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("second".to_owned()), 0);
24831            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24832            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24833            b.resolve_label(end);
24834        });
24835        assert_eq!(rows[0], vec![SqliteValue::Text("first_value".into())]);
24836        assert_eq!(rows[1], vec![SqliteValue::Text("second".into())]);
24837    }
24838
24839    #[test]
24840    fn test_intcopy_coerces() {
24841        // IntCopy converts value to integer.
24842        let rows = run_program(|b| {
24843            let end = b.emit_label();
24844            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24845            let src = b.alloc_reg();
24846            let dst = b.alloc_reg();
24847            b.emit_op(Opcode::Real, 0, src, 0, P4::Real(3.7), 0);
24848            b.emit_op(Opcode::IntCopy, src, dst, 0, P4::None, 0);
24849            b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24850            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24851            b.resolve_label(end);
24852        });
24853        assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
24854    }
24855
24856    // ── Arithmetic Edge Cases ──────────────────────────────────────────
24857
24858    #[test]
24859    fn test_subtract_integers() {
24860        let rows = run_program(|b| {
24861            let end = b.emit_label();
24862            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24863            let r1 = b.alloc_reg();
24864            let r2 = b.alloc_reg();
24865            let r3 = b.alloc_reg();
24866            b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
24867            b.emit_op(Opcode::Integer, 3, r2, 0, P4::None, 0);
24868            // p3 = p2 - p1 → r3 = r1 - r2 if p2=r1, p1=r2 → 10 - 3 = 7
24869            b.emit_op(Opcode::Subtract, r2, r1, r3, P4::None, 0);
24870            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
24871            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24872            b.resolve_label(end);
24873        });
24874        assert_eq!(rows[0], vec![SqliteValue::Integer(7)]);
24875    }
24876
24877    #[test]
24878    fn test_multiply_large() {
24879        let rows = run_program(|b| {
24880            let end = b.emit_label();
24881            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24882            let r1 = b.alloc_reg();
24883            let r2 = b.alloc_reg();
24884            let r3 = b.alloc_reg();
24885            b.emit_op(Opcode::Integer, 100, r1, 0, P4::None, 0);
24886            b.emit_op(Opcode::Integer, 200, r2, 0, P4::None, 0);
24887            b.emit_op(Opcode::Multiply, r1, r2, r3, P4::None, 0);
24888            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
24889            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24890            b.resolve_label(end);
24891        });
24892        assert_eq!(rows[0], vec![SqliteValue::Integer(20_000)]);
24893    }
24894
24895    #[test]
24896    fn test_integer_division_truncates() {
24897        // 7 / 2 = 3 (integer division truncates)
24898        let rows = run_program(|b| {
24899            let end = b.emit_label();
24900            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24901            let r_divisor = b.alloc_reg();
24902            let r_dividend = b.alloc_reg();
24903            let r_result = b.alloc_reg();
24904            b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24905            b.emit_op(Opcode::Integer, 7, r_dividend, 0, P4::None, 0);
24906            // p3 = p2 / p1 → r_result = r_dividend / r_divisor
24907            b.emit_op(Opcode::Divide, r_divisor, r_dividend, r_result, P4::None, 0);
24908            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24909            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24910            b.resolve_label(end);
24911        });
24912        assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
24913    }
24914
24915    #[test]
24916    fn test_remainder_integers() {
24917        // 7 % 3 = 1
24918        let rows = run_program(|b| {
24919            let end = b.emit_label();
24920            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24921            let r_divisor = b.alloc_reg();
24922            let r_dividend = b.alloc_reg();
24923            let r_result = b.alloc_reg();
24924            b.emit_op(Opcode::Integer, 3, r_divisor, 0, P4::None, 0);
24925            b.emit_op(Opcode::Integer, 7, r_dividend, 0, P4::None, 0);
24926            b.emit_op(
24927                Opcode::Remainder,
24928                r_divisor,
24929                r_dividend,
24930                r_result,
24931                P4::None,
24932                0,
24933            );
24934            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24935            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24936            b.resolve_label(end);
24937        });
24938        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
24939    }
24940
24941    #[test]
24942    fn test_remainder_by_zero() {
24943        let rows = run_program(|b| {
24944            let end = b.emit_label();
24945            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24946            let r_zero = b.alloc_reg();
24947            let r_val = b.alloc_reg();
24948            let r_result = b.alloc_reg();
24949            b.emit_op(Opcode::Integer, 0, r_zero, 0, P4::None, 0);
24950            b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
24951            b.emit_op(Opcode::Remainder, r_zero, r_val, r_result, P4::None, 0);
24952            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24953            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24954            b.resolve_label(end);
24955        });
24956        assert_eq!(rows[0], vec![SqliteValue::Null]);
24957    }
24958
24959    #[test]
24960    fn test_divide_text_prefix_uses_integer_path() {
24961        let rows = run_program(|b| {
24962            let end = b.emit_label();
24963            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24964            let r_divisor = b.alloc_reg();
24965            let r_dividend = b.alloc_reg();
24966            let r_result = b.alloc_reg();
24967            b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24968            b.emit_op(
24969                Opcode::String8,
24970                0,
24971                r_dividend,
24972                0,
24973                P4::Str("123abc".to_owned()),
24974                0,
24975            );
24976            b.emit_op(Opcode::Divide, r_divisor, r_dividend, r_result, P4::None, 0);
24977            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24978            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24979            b.resolve_label(end);
24980        });
24981        assert_eq!(rows[0], vec![SqliteValue::Integer(61)]);
24982    }
24983
24984    #[test]
24985    fn test_remainder_blob_prefix_uses_integer_path() {
24986        let rows = run_program(|b| {
24987            let end = b.emit_label();
24988            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24989            let r_divisor = b.alloc_reg();
24990            let r_dividend = b.alloc_reg();
24991            let r_result = b.alloc_reg();
24992            b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24993            b.emit_op(
24994                Opcode::Blob,
24995                4,
24996                r_dividend,
24997                0,
24998                P4::Blob(b"123a".to_vec()),
24999                0,
25000            );
25001            b.emit_op(
25002                Opcode::Remainder,
25003                r_divisor,
25004                r_dividend,
25005                r_result,
25006                P4::None,
25007                0,
25008            );
25009            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25010            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25011            b.resolve_label(end);
25012        });
25013        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25014    }
25015
25016    #[test]
25017    fn test_null_arithmetic_propagation() {
25018        // NULL + 1, NULL * 5, NULL - 3 should all be NULL.
25019        let rows = run_program(|b| {
25020            let end = b.emit_label();
25021            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25022            let r_null = b.alloc_reg();
25023            let r_one = b.alloc_reg();
25024            let r_add = b.alloc_reg();
25025            let r_mul = b.alloc_reg();
25026            let r_sub = b.alloc_reg();
25027            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25028            b.emit_op(Opcode::Integer, 5, r_one, 0, P4::None, 0);
25029            b.emit_op(Opcode::Add, r_null, r_one, r_add, P4::None, 0);
25030            b.emit_op(Opcode::Multiply, r_null, r_one, r_mul, P4::None, 0);
25031            b.emit_op(Opcode::Subtract, r_null, r_one, r_sub, P4::None, 0);
25032            b.emit_op(Opcode::ResultRow, r_add, 1, 0, P4::None, 0);
25033            b.emit_op(Opcode::ResultRow, r_mul, 1, 0, P4::None, 0);
25034            b.emit_op(Opcode::ResultRow, r_sub, 1, 0, P4::None, 0);
25035            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25036            b.resolve_label(end);
25037        });
25038        assert_eq!(rows[0], vec![SqliteValue::Null]);
25039        assert_eq!(rows[1], vec![SqliteValue::Null]);
25040        assert_eq!(rows[2], vec![SqliteValue::Null]);
25041    }
25042
25043    #[test]
25044    fn test_add_imm() {
25045        // AddImm: register p1 += p2
25046        let rows = run_program(|b| {
25047            let end = b.emit_label();
25048            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25049            let r = b.alloc_reg();
25050            b.emit_op(Opcode::Integer, 100, r, 0, P4::None, 0);
25051            b.emit_op(Opcode::AddImm, r, 50, 0, P4::None, 0);
25052            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25053            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25054            b.resolve_label(end);
25055        });
25056        assert_eq!(rows[0], vec![SqliteValue::Integer(150)]);
25057    }
25058
25059    #[test]
25060    fn test_add_imm_negative() {
25061        let rows = run_program(|b| {
25062            let end = b.emit_label();
25063            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25064            let r = b.alloc_reg();
25065            b.emit_op(Opcode::Integer, 100, r, 0, P4::None, 0);
25066            b.emit_op(Opcode::AddImm, r, -30, 0, P4::None, 0);
25067            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25068            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25069            b.resolve_label(end);
25070        });
25071        assert_eq!(rows[0], vec![SqliteValue::Integer(70)]);
25072    }
25073
25074    // ── Bitwise Operations ─────────────────────────────────────────────
25075
25076    #[test]
25077    fn test_bit_and() {
25078        // 0xFF & 0x0F = 0x0F (15)
25079        let rows = run_program(|b| {
25080            let end = b.emit_label();
25081            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25082            let r1 = b.alloc_reg();
25083            let r2 = b.alloc_reg();
25084            let r3 = b.alloc_reg();
25085            b.emit_op(Opcode::Integer, 0xFF, r1, 0, P4::None, 0);
25086            b.emit_op(Opcode::Integer, 0x0F, r2, 0, P4::None, 0);
25087            b.emit_op(Opcode::BitAnd, r1, r2, r3, P4::None, 0);
25088            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25089            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25090            b.resolve_label(end);
25091        });
25092        assert_eq!(rows[0], vec![SqliteValue::Integer(0x0F)]);
25093    }
25094
25095    #[test]
25096    fn test_bit_or() {
25097        // 0xF0 | 0x0F = 0xFF (255)
25098        let rows = run_program(|b| {
25099            let end = b.emit_label();
25100            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25101            let r1 = b.alloc_reg();
25102            let r2 = b.alloc_reg();
25103            let r3 = b.alloc_reg();
25104            b.emit_op(Opcode::Integer, 0xF0, r1, 0, P4::None, 0);
25105            b.emit_op(Opcode::Integer, 0x0F, r2, 0, P4::None, 0);
25106            b.emit_op(Opcode::BitOr, r1, r2, r3, P4::None, 0);
25107            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25108            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25109            b.resolve_label(end);
25110        });
25111        assert_eq!(rows[0], vec![SqliteValue::Integer(0xFF)]);
25112    }
25113
25114    #[test]
25115    fn test_shift_left() {
25116        // 1 << 8 = 256
25117        let rows = run_program(|b| {
25118            let end = b.emit_label();
25119            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25120            let r_amount = b.alloc_reg();
25121            let r_val = b.alloc_reg();
25122            let r_result = b.alloc_reg();
25123            b.emit_op(Opcode::Integer, 8, r_amount, 0, P4::None, 0);
25124            b.emit_op(Opcode::Integer, 1, r_val, 0, P4::None, 0);
25125            b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25126            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25127            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25128            b.resolve_label(end);
25129        });
25130        assert_eq!(rows[0], vec![SqliteValue::Integer(256)]);
25131    }
25132
25133    #[test]
25134    fn test_shift_right() {
25135        // 256 >> 4 = 16
25136        let rows = run_program(|b| {
25137            let end = b.emit_label();
25138            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25139            let r_amount = b.alloc_reg();
25140            let r_val = b.alloc_reg();
25141            let r_result = b.alloc_reg();
25142            b.emit_op(Opcode::Integer, 4, r_amount, 0, P4::None, 0);
25143            b.emit_op(Opcode::Int64, 0, r_val, 0, P4::Int64(256), 0);
25144            b.emit_op(Opcode::ShiftRight, r_amount, r_val, r_result, P4::None, 0);
25145            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25146            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25147            b.resolve_label(end);
25148        });
25149        assert_eq!(rows[0], vec![SqliteValue::Integer(16)]);
25150    }
25151
25152    #[test]
25153    fn test_shift_left_overflow_clamp() {
25154        // Shift >= 64 returns 0
25155        let rows = run_program(|b| {
25156            let end = b.emit_label();
25157            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25158            let r_amount = b.alloc_reg();
25159            let r_val = b.alloc_reg();
25160            let r_result = b.alloc_reg();
25161            b.emit_op(Opcode::Int64, 0, r_amount, 0, P4::Int64(64), 0);
25162            b.emit_op(Opcode::Integer, 1, r_val, 0, P4::None, 0);
25163            b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25164            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25165            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25166            b.resolve_label(end);
25167        });
25168        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25169    }
25170
25171    #[test]
25172    fn test_shift_negative_reverses() {
25173        // Negative shift amount reverses direction: <<(-2) == >>(2)
25174        let rows = run_program(|b| {
25175            let end = b.emit_label();
25176            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25177            let r_amount = b.alloc_reg();
25178            let r_val = b.alloc_reg();
25179            let r_result = b.alloc_reg();
25180            b.emit_op(Opcode::Int64, 0, r_amount, 0, P4::Int64(-2), 0);
25181            b.emit_op(Opcode::Integer, 8, r_val, 0, P4::None, 0);
25182            b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25183            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25184            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25185            b.resolve_label(end);
25186        });
25187        // 8 >> 2 = 2
25188        assert_eq!(rows[0], vec![SqliteValue::Integer(2)]);
25189    }
25190
25191    #[test]
25192    fn test_bit_not() {
25193        // ~0 = -1 in two's complement
25194        let rows = run_program(|b| {
25195            let end = b.emit_label();
25196            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25197            let r1 = b.alloc_reg();
25198            let r2 = b.alloc_reg();
25199            b.emit_op(Opcode::Integer, 0, r1, 0, P4::None, 0);
25200            b.emit_op(Opcode::BitNot, r1, r2, 0, P4::None, 0);
25201            b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
25202            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25203            b.resolve_label(end);
25204        });
25205        assert_eq!(rows[0], vec![SqliteValue::Integer(-1)]);
25206    }
25207
25208    #[test]
25209    fn test_bitwise_null_propagation() {
25210        let rows = run_program(|b| {
25211            let end = b.emit_label();
25212            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25213            let r_null = b.alloc_reg();
25214            let r_val = b.alloc_reg();
25215            let r_and = b.alloc_reg();
25216            let r_or = b.alloc_reg();
25217            let r_not = b.alloc_reg();
25218            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25219            b.emit_op(Opcode::Integer, 0xFF, r_val, 0, P4::None, 0);
25220            b.emit_op(Opcode::BitAnd, r_null, r_val, r_and, P4::None, 0);
25221            b.emit_op(Opcode::BitOr, r_null, r_val, r_or, P4::None, 0);
25222            b.emit_op(Opcode::BitNot, r_null, r_not, 0, P4::None, 0);
25223            b.emit_op(Opcode::ResultRow, r_and, 1, 0, P4::None, 0);
25224            b.emit_op(Opcode::ResultRow, r_or, 1, 0, P4::None, 0);
25225            b.emit_op(Opcode::ResultRow, r_not, 1, 0, P4::None, 0);
25226            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25227            b.resolve_label(end);
25228        });
25229        assert_eq!(rows[0], vec![SqliteValue::Null]);
25230        assert_eq!(rows[1], vec![SqliteValue::Null]);
25231        assert_eq!(rows[2], vec![SqliteValue::Null]);
25232    }
25233
25234    // ── String Operations ──────────────────────────────────────────────
25235
25236    #[test]
25237    fn test_concat_two_strings() {
25238        let rows = run_program(|b| {
25239            let end = b.emit_label();
25240            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25241            let r1 = b.alloc_reg();
25242            let r2 = b.alloc_reg();
25243            let r3 = b.alloc_reg();
25244            b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("hello ".to_owned()), 0);
25245            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("world".to_owned()), 0);
25246            // Concat: p3 = p2 || p1 (note operand order)
25247            b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
25248            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25249            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25250            b.resolve_label(end);
25251        });
25252        assert_eq!(rows[0], vec![SqliteValue::Text("hello world".into())]);
25253    }
25254
25255    #[test]
25256    fn test_concat_empty_string() {
25257        let rows = run_program(|b| {
25258            let end = b.emit_label();
25259            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25260            let r1 = b.alloc_reg();
25261            let r2 = b.alloc_reg();
25262            let r3 = b.alloc_reg();
25263            b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("test".to_owned()), 0);
25264            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str(String::new()), 0);
25265            b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
25266            b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25267            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25268            b.resolve_label(end);
25269        });
25270        assert_eq!(rows[0], vec![SqliteValue::Text("test".into())]);
25271    }
25272
25273    // ── Comparison Ops (all 6 + NULL) ──────────────────────────────────
25274
25275    #[test]
25276    fn test_eq_jump_taken() {
25277        let rows = run_program(|b| {
25278            let end = b.emit_label();
25279            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25280            let r1 = b.alloc_reg();
25281            let r2 = b.alloc_reg();
25282            let r_out = b.alloc_reg();
25283            b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
25284            b.emit_op(Opcode::Integer, 42, r2, 0, P4::None, 0);
25285            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25286            let taken = b.emit_label();
25287            // Eq: if p3 == p1, jump to p2 → if r2 == r1, jump
25288            b.emit_jump_to_label(Opcode::Eq, r1, r2, taken, P4::None, 0);
25289            let done = b.emit_label();
25290            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25291            b.resolve_label(taken);
25292            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25293            b.resolve_label(done);
25294            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25295            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25296            b.resolve_label(end);
25297        });
25298        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25299    }
25300
25301    #[test]
25302    fn test_ne_jump_taken() {
25303        let rows = run_program(|b| {
25304            let end = b.emit_label();
25305            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25306            let r1 = b.alloc_reg();
25307            let r2 = b.alloc_reg();
25308            let r_out = b.alloc_reg();
25309            b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
25310            b.emit_op(Opcode::Integer, 20, r2, 0, P4::None, 0);
25311            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25312            let taken = b.emit_label();
25313            b.emit_jump_to_label(Opcode::Ne, r1, r2, taken, P4::None, 0);
25314            let done = b.emit_label();
25315            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25316            b.resolve_label(taken);
25317            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25318            b.resolve_label(done);
25319            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25320            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25321            b.resolve_label(end);
25322        });
25323        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25324    }
25325
25326    #[test]
25327    fn test_lt_jump_taken() {
25328        let rows = run_program(|b| {
25329            let end = b.emit_label();
25330            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25331            let r_big = b.alloc_reg();
25332            let r_small = b.alloc_reg();
25333            let r_out = b.alloc_reg();
25334            b.emit_op(Opcode::Integer, 100, r_big, 0, P4::None, 0);
25335            b.emit_op(Opcode::Integer, 5, r_small, 0, P4::None, 0);
25336            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25337            // Lt: if p3 < p1, jump → if r_small < r_big
25338            let taken = b.emit_label();
25339            b.emit_jump_to_label(Opcode::Lt, r_big, r_small, taken, P4::None, 0);
25340            let done = b.emit_label();
25341            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25342            b.resolve_label(taken);
25343            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25344            b.resolve_label(done);
25345            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25346            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25347            b.resolve_label(end);
25348        });
25349        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25350    }
25351
25352    #[test]
25353    fn test_le_with_equal_values() {
25354        let rows = run_program(|b| {
25355            let end = b.emit_label();
25356            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25357            let r1 = b.alloc_reg();
25358            let r2 = b.alloc_reg();
25359            let r_out = b.alloc_reg();
25360            b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
25361            b.emit_op(Opcode::Integer, 7, r2, 0, P4::None, 0);
25362            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25363            let taken = b.emit_label();
25364            b.emit_jump_to_label(Opcode::Le, r1, r2, taken, P4::None, 0);
25365            let done = b.emit_label();
25366            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25367            b.resolve_label(taken);
25368            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25369            b.resolve_label(done);
25370            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25371            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25372            b.resolve_label(end);
25373        });
25374        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25375    }
25376
25377    #[test]
25378    fn test_ge_with_greater_value() {
25379        let rows = run_program(|b| {
25380            let end = b.emit_label();
25381            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25382            let r_big = b.alloc_reg();
25383            let r_small = b.alloc_reg();
25384            let r_out = b.alloc_reg();
25385            b.emit_op(Opcode::Integer, 5, r_small, 0, P4::None, 0);
25386            b.emit_op(Opcode::Integer, 100, r_big, 0, P4::None, 0);
25387            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25388            let taken = b.emit_label();
25389            // Ge: if p3 >= p1 → if r_big >= r_small
25390            b.emit_jump_to_label(Opcode::Ge, r_small, r_big, taken, P4::None, 0);
25391            let done = b.emit_label();
25392            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25393            b.resolve_label(taken);
25394            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25395            b.resolve_label(done);
25396            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25397            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25398            b.resolve_label(end);
25399        });
25400        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25401    }
25402
25403    #[test]
25404    fn test_comparison_null_no_jump() {
25405        // Standard SQL: NULL = 5 → no jump (NULL result)
25406        let rows = run_program(|b| {
25407            let end = b.emit_label();
25408            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25409            let r_null = b.alloc_reg();
25410            let r_five = b.alloc_reg();
25411            let r_out = b.alloc_reg();
25412            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25413            b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25414            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25415            let taken = b.emit_label();
25416            b.emit_jump_to_label(Opcode::Eq, r_five, r_null, taken, P4::None, 0);
25417            let done = b.emit_label();
25418            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25419            b.resolve_label(taken);
25420            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25421            b.resolve_label(done);
25422            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25423            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25424            b.resolve_label(end);
25425        });
25426        // Should NOT jump: NULL = 5 is NULL (not true)
25427        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25428    }
25429
25430    #[test]
25431    fn test_ne_nulleq_one_null() {
25432        // IS NOT semantics: NULL IS NOT 5 → true (jump)
25433        let rows = run_program(|b| {
25434            let end = b.emit_label();
25435            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25436            let r_null = b.alloc_reg();
25437            let r_five = b.alloc_reg();
25438            let r_out = b.alloc_reg();
25439            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25440            b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25441            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25442            let taken = b.emit_label();
25443            b.emit_jump_to_label(Opcode::Ne, r_five, r_null, taken, P4::None, 0x80);
25444            let done = b.emit_label();
25445            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25446            b.resolve_label(taken);
25447            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25448            b.resolve_label(done);
25449            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25450            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25451            b.resolve_label(end);
25452        });
25453        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25454    }
25455
25456    #[test]
25457    fn test_comparison_jumpifnull_flag() {
25458        // JUMPIFNULL (0x10): jump to P2 when either operand is NULL.
25459        let rows = run_program(|b| {
25460            let end = b.emit_label();
25461            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25462            let r_null = b.alloc_reg();
25463            let r_five = b.alloc_reg();
25464            let r_out = b.alloc_reg();
25465            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25466            b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25467            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25468            let taken = b.emit_label();
25469            // Eq with JUMPIFNULL (0x10): NULL = 5 should jump
25470            b.emit_jump_to_label(Opcode::Eq, r_five, r_null, taken, P4::None, 0x10);
25471            let done = b.emit_label();
25472            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25473            b.resolve_label(taken);
25474            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25475            b.resolve_label(done);
25476            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25477            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25478            b.resolve_label(end);
25479        });
25480        // With JUMPIFNULL, NULL = 5 should jump to P2
25481        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25482    }
25483
25484    #[test]
25485    fn test_comparison_storep2_non_null() {
25486        // STOREP2 (0x20): store boolean result in P2 instead of jumping.
25487        let rows = run_program(|b| {
25488            let end = b.emit_label();
25489            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25490            let r_a = b.alloc_reg();
25491            let r_b = b.alloc_reg();
25492            let o_eq = b.alloc_reg();
25493            let o_ne = b.alloc_reg();
25494            let o_lt = b.alloc_reg();
25495            b.emit_op(Opcode::Integer, 5, r_a, 0, P4::None, 0);
25496            b.emit_op(Opcode::Integer, 10, r_b, 0, P4::None, 0);
25497            // Eq with STOREP2: 5 == 10 → store 0
25498            b.emit_op(Opcode::Eq, r_b, o_eq, r_a, P4::None, 0x20);
25499            // Ne with STOREP2: 5 != 10 → store 1
25500            b.emit_op(Opcode::Ne, r_b, o_ne, r_a, P4::None, 0x20);
25501            // Lt with STOREP2: 5 < 10 → store 1
25502            b.emit_op(Opcode::Lt, r_b, o_lt, r_a, P4::None, 0x20);
25503            b.emit_op(Opcode::ResultRow, o_eq, 3, 0, P4::None, 0);
25504            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25505            b.resolve_label(end);
25506        });
25507        assert_eq!(
25508            rows[0],
25509            vec![
25510                SqliteValue::Integer(0), // 5 == 10 → false
25511                SqliteValue::Integer(1), // 5 != 10 → true
25512                SqliteValue::Integer(1), // 5 < 10 → true
25513            ]
25514        );
25515    }
25516
25517    #[test]
25518    fn test_comparison_storep2_null_gives_null() {
25519        // STOREP2 (0x20) with NULL operand: store NULL in P2.
25520        let rows = run_program(|b| {
25521            let end = b.emit_label();
25522            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25523            let r_null = b.alloc_reg();
25524            let r_five = b.alloc_reg();
25525            let o1 = b.alloc_reg();
25526            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25527            b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25528            // Eq with STOREP2: NULL == 5 → store NULL
25529            b.emit_op(Opcode::Eq, r_five, o1, r_null, P4::None, 0x20);
25530            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
25531            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25532            b.resolve_label(end);
25533        });
25534        assert_eq!(rows[0], vec![SqliteValue::Null]);
25535    }
25536
25537    // ── Logic Edge Cases ───────────────────────────────────────────────
25538
25539    #[test]
25540    fn test_not_null_is_null() {
25541        let rows = run_program(|b| {
25542            let end = b.emit_label();
25543            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25544            let r_null = b.alloc_reg();
25545            let r_out = b.alloc_reg();
25546            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25547            b.emit_op(Opcode::Not, r_null, r_out, 0, P4::None, 0);
25548            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25549            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25550            b.resolve_label(end);
25551        });
25552        assert_eq!(rows[0], vec![SqliteValue::Null]);
25553    }
25554
25555    #[test]
25556    fn test_not_zero_is_one() {
25557        let rows = run_program(|b| {
25558            let end = b.emit_label();
25559            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25560            let r_zero = b.alloc_reg();
25561            let r_out = b.alloc_reg();
25562            b.emit_op(Opcode::Integer, 0, r_zero, 0, P4::None, 0);
25563            b.emit_op(Opcode::Not, r_zero, r_out, 0, P4::None, 0);
25564            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25565            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25566            b.resolve_label(end);
25567        });
25568        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25569    }
25570
25571    #[test]
25572    fn test_not_nonzero_is_zero() {
25573        let rows = run_program(|b| {
25574            let end = b.emit_label();
25575            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25576            let r_val = b.alloc_reg();
25577            let r_out = b.alloc_reg();
25578            b.emit_op(Opcode::Integer, 42, r_val, 0, P4::None, 0);
25579            b.emit_op(Opcode::Not, r_val, r_out, 0, P4::None, 0);
25580            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25581            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25582            b.resolve_label(end);
25583        });
25584        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25585    }
25586
25587    // ── Conditional Jumps ──────────────────────────────────────────────
25588
25589    #[test]
25590    fn test_if_true_jumps() {
25591        let rows = run_program(|b| {
25592            let end = b.emit_label();
25593            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25594            let r_cond = b.alloc_reg();
25595            let r_out = b.alloc_reg();
25596            b.emit_op(Opcode::Integer, 1, r_cond, 0, P4::None, 0);
25597            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25598            let taken = b.emit_label();
25599            b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25600            let done = b.emit_label();
25601            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25602            b.resolve_label(taken);
25603            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25604            b.resolve_label(done);
25605            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25606            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25607            b.resolve_label(end);
25608        });
25609        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25610    }
25611
25612    #[test]
25613    fn test_if_false_no_jump() {
25614        let rows = run_program(|b| {
25615            let end = b.emit_label();
25616            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25617            let r_cond = b.alloc_reg();
25618            let r_out = b.alloc_reg();
25619            b.emit_op(Opcode::Integer, 0, r_cond, 0, P4::None, 0);
25620            b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
25621            let taken = b.emit_label();
25622            b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25623            let done = b.emit_label();
25624            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25625            b.resolve_label(taken);
25626            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25627            b.resolve_label(done);
25628            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25629            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25630            b.resolve_label(end);
25631        });
25632        // If with false → no jump → r_out stays 99
25633        assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
25634    }
25635
25636    #[test]
25637    fn test_if_null_no_jump() {
25638        let rows = run_program(|b| {
25639            let end = b.emit_label();
25640            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25641            let r_cond = b.alloc_reg();
25642            let r_out = b.alloc_reg();
25643            b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25644            b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
25645            let taken = b.emit_label();
25646            b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25647            let done = b.emit_label();
25648            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25649            b.resolve_label(taken);
25650            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25651            b.resolve_label(done);
25652            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25653            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25654            b.resolve_label(end);
25655        });
25656        // If with NULL → no jump → r_out stays 99
25657        assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
25658    }
25659
25660    #[test]
25661    fn test_ifnot_false_jumps() {
25662        let rows = run_program(|b| {
25663            let end = b.emit_label();
25664            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25665            let r_cond = b.alloc_reg();
25666            let r_out = b.alloc_reg();
25667            b.emit_op(Opcode::Integer, 0, r_cond, 0, P4::None, 0);
25668            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25669            let taken = b.emit_label();
25670            b.emit_jump_to_label(Opcode::IfNot, r_cond, 0, taken, P4::None, 0);
25671            let done = b.emit_label();
25672            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25673            b.resolve_label(taken);
25674            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25675            b.resolve_label(done);
25676            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25677            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25678            b.resolve_label(end);
25679        });
25680        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25681    }
25682
25683    #[test]
25684    fn test_ifnot_null_p3_zero_no_jump() {
25685        // IfNot with NULL and p3=0 → no jump (SQLite: p3 controls NULL behavior)
25686        let rows = run_program(|b| {
25687            let end = b.emit_label();
25688            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25689            let r_cond = b.alloc_reg();
25690            let r_out = b.alloc_reg();
25691            b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25692            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25693            let taken = b.emit_label();
25694            b.emit_jump_to_label(Opcode::IfNot, r_cond, 0, taken, P4::None, 0);
25695            let done = b.emit_label();
25696            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25697            b.resolve_label(taken);
25698            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25699            b.resolve_label(done);
25700            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25701            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25702            b.resolve_label(end);
25703        });
25704        // p3=0: NULL → don't jump → r_out stays 0
25705        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25706    }
25707
25708    #[test]
25709    fn test_ifnot_null_p3_one_jumps() {
25710        // IfNot with NULL and p3=1 → jump (SQLite: p3!=0 means jump on NULL)
25711        let rows = run_program(|b| {
25712            let end = b.emit_label();
25713            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25714            let r_cond = b.alloc_reg();
25715            let r_out = b.alloc_reg();
25716            b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25717            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25718            let taken = b.emit_label();
25719            b.emit_jump_to_label(Opcode::IfNot, r_cond, 1, taken, P4::None, 0);
25720            let done = b.emit_label();
25721            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25722            b.resolve_label(taken);
25723            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25724            b.resolve_label(done);
25725            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25726            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25727            b.resolve_label(end);
25728        });
25729        // p3=1: NULL → jump → r_out = 1
25730        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25731    }
25732
25733    #[test]
25734    fn test_if_null_p3_one_jumps() {
25735        // If with NULL and p3=1 → jump (SQLite: p3!=0 means jump on NULL)
25736        let rows = run_program(|b| {
25737            let end = b.emit_label();
25738            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25739            let r_cond = b.alloc_reg();
25740            let r_out = b.alloc_reg();
25741            b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25742            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25743            let taken = b.emit_label();
25744            b.emit_jump_to_label(Opcode::If, r_cond, 1, taken, P4::None, 0);
25745            let done = b.emit_label();
25746            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25747            b.resolve_label(taken);
25748            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25749            b.resolve_label(done);
25750            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25751            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25752            b.resolve_label(end);
25753        });
25754        // p3=1: NULL → jump → r_out = 1
25755        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25756    }
25757
25758    #[test]
25759    fn test_once_fires_only_once() {
25760        // Once falls through on first pass (runs the body), jumps to p2 on
25761        // subsequent passes (skips the body).  This matches C SQLite semantics.
25762        let rows = run_program(|b| {
25763            let end = b.emit_label();
25764            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25765            let r_counter = b.alloc_reg();
25766            b.emit_op(Opcode::Integer, 0, r_counter, 0, P4::None, 0);
25767
25768            let loop_start = b.emit_label();
25769            b.resolve_label(loop_start);
25770            // Once: first pass → fall through (run init code below),
25771            //        second+ pass → jump to `skip_init`.
25772            let skip_init = b.emit_label();
25773            b.emit_jump_to_label(Opcode::Once, 0, 0, skip_init, P4::None, 0);
25774            // Init code (runs only on first pass): increment counter.
25775            b.emit_op(Opcode::AddImm, r_counter, 1, 0, P4::None, 0);
25776            b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
25777            // Second pass lands here — skip the init, output result.
25778            b.resolve_label(skip_init);
25779            b.emit_op(Opcode::ResultRow, r_counter, 1, 0, P4::None, 0);
25780            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25781            b.resolve_label(end);
25782        });
25783        // First pass: Once falls through → counter becomes 1 → loop back.
25784        // Second pass: Once jumps to skip_init → output counter=1.
25785        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25786    }
25787
25788    #[test]
25789    fn test_open_autoindex_without_memdb_supports_idxinsert_and_found() {
25790        let rows = run_program(|b| {
25791            let end = b.emit_label();
25792            let found = b.emit_label();
25793            let done = b.emit_label();
25794            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25795
25796            let cursor_id = 1;
25797            let r_insert_val = b.alloc_reg();
25798            let r_insert_key = b.alloc_reg();
25799            let r_probe_val = b.alloc_reg();
25800            let r_probe_key = b.alloc_reg();
25801            let r_out = b.alloc_reg();
25802
25803            b.emit_op(Opcode::OpenAutoindex, cursor_id, 1, 0, P4::None, 0);
25804            b.emit_op(Opcode::Integer, 7, r_insert_val, 0, P4::None, 0);
25805            b.emit_op(
25806                Opcode::MakeRecord,
25807                r_insert_val,
25808                1,
25809                r_insert_key,
25810                P4::None,
25811                0,
25812            );
25813            b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25814
25815            b.emit_op(Opcode::Integer, 7, r_probe_val, 0, P4::None, 0);
25816            b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
25817            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25818            b.emit_jump_to_label(Opcode::Found, cursor_id, r_probe_key, found, P4::None, 0);
25819            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25820            b.resolve_label(found);
25821            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25822            b.resolve_label(done);
25823
25824            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25825            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25826            b.resolve_label(end);
25827        });
25828        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
25829    }
25830
25831    #[test]
25832    fn test_open_autoindex_honors_nocase_collation_for_found() {
25833        let rows = run_program(|b| {
25834            let end = b.emit_label();
25835            let found = b.emit_label();
25836            let done = b.emit_label();
25837            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25838
25839            let cursor_id = 1;
25840            let r_insert_val = b.alloc_reg();
25841            let r_insert_key = b.alloc_reg();
25842            let r_probe_val = b.alloc_reg();
25843            let r_probe_key = b.alloc_reg();
25844            let r_out = b.alloc_reg();
25845
25846            b.emit_op(
25847                Opcode::OpenAutoindex,
25848                cursor_id,
25849                1,
25850                0,
25851                P4::Collation("NOCASE".to_owned()),
25852                0,
25853            );
25854            b.emit_op(
25855                Opcode::String8,
25856                0,
25857                r_insert_val,
25858                0,
25859                P4::Str("alpha".to_owned()),
25860                0,
25861            );
25862            b.emit_op(
25863                Opcode::MakeRecord,
25864                r_insert_val,
25865                1,
25866                r_insert_key,
25867                P4::None,
25868                0,
25869            );
25870            b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25871
25872            b.emit_op(
25873                Opcode::String8,
25874                0,
25875                r_probe_val,
25876                0,
25877                P4::Str("ALPHA".to_owned()),
25878                0,
25879            );
25880            b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
25881            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25882            b.emit_jump_to_label(Opcode::Found, cursor_id, r_probe_key, found, P4::None, 0);
25883            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25884            b.resolve_label(found);
25885            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25886            b.resolve_label(done);
25887
25888            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25889            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25890            b.resolve_label(end);
25891        });
25892        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
25893    }
25894
25895    #[test]
25896    fn test_column_reads_from_key_only_autoindex_without_trailing_rowid() {
25897        let rows = run_program(|b| {
25898            let end = b.emit_label();
25899            let done = b.emit_label();
25900            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25901
25902            let cursor_id = 1;
25903            let r_insert_val = b.alloc_reg();
25904            let r_insert_key = b.alloc_reg();
25905            let r_out = b.alloc_reg();
25906
25907            b.emit_op(Opcode::OpenAutoindex, cursor_id, 1, 0, P4::None, 0);
25908            b.emit_op(
25909                Opcode::String8,
25910                0,
25911                r_insert_val,
25912                0,
25913                P4::Str("alpha".to_owned()),
25914                0,
25915            );
25916            b.emit_op(
25917                Opcode::MakeRecord,
25918                r_insert_val,
25919                1,
25920                r_insert_key,
25921                P4::None,
25922                0,
25923            );
25924            b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25925            b.emit_jump_to_label(Opcode::Rewind, cursor_id, 0, done, P4::None, 0);
25926            b.emit_op(Opcode::Column, cursor_id, 0, r_out, P4::None, 0);
25927            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25928            b.resolve_label(done);
25929            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25930            b.resolve_label(end);
25931        });
25932
25933        assert_eq!(rows, vec![vec![SqliteValue::Text("alpha".into())]]);
25934    }
25935
25936    // ── Type Coercion ──────────────────────────────────────────────────
25937
25938    #[test]
25939    fn test_cast_integer_to_text() {
25940        let rows = run_program(|b| {
25941            let end = b.emit_label();
25942            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25943            let r = b.alloc_reg();
25944            b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
25945            // Cast to TEXT: p2 = 'B' (66)
25946            b.emit_op(Opcode::Cast, r, 66, 0, P4::None, 0);
25947            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25948            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25949            b.resolve_label(end);
25950        });
25951        assert_eq!(rows[0], vec![SqliteValue::Text("42".into())]);
25952    }
25953
25954    #[test]
25955    fn test_cast_text_to_integer() {
25956        let rows = run_program(|b| {
25957            let end = b.emit_label();
25958            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25959            let r = b.alloc_reg();
25960            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("123".to_owned()), 0);
25961            // Cast to INTEGER: p2 = 'D' (68)
25962            b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0);
25963            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25964            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25965            b.resolve_label(end);
25966        });
25967        assert_eq!(rows[0], vec![SqliteValue::Integer(123)]);
25968    }
25969
25970    #[test]
25971    fn test_cast_to_real() {
25972        let rows = run_program(|b| {
25973            let end = b.emit_label();
25974            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25975            let r = b.alloc_reg();
25976            b.emit_op(Opcode::Integer, 5, r, 0, P4::None, 0);
25977            // Cast to REAL: p2 = 'E' (69)
25978            b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0);
25979            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25980            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25981            b.resolve_label(end);
25982        });
25983        assert_eq!(rows[0], vec![SqliteValue::Float(5.0)]);
25984    }
25985
25986    #[test]
25987    fn test_cast_to_blob() {
25988        let rows = run_program(|b| {
25989            let end = b.emit_label();
25990            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25991            let r = b.alloc_reg();
25992            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("hi".to_owned()), 0);
25993            // Cast to BLOB: p2 = 'A' (65)
25994            b.emit_op(Opcode::Cast, r, 65, 0, P4::None, 0);
25995            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25996            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25997            b.resolve_label(end);
25998        });
25999        assert_eq!(rows[0], vec![SqliteValue::Blob(b"hi".to_vec().into())]);
26000    }
26001
26002    #[test]
26003    fn test_cast_text_sci_notation_to_integer() {
26004        // SQLite integer casts ignore exponent syntax in text and consume only
26005        // the signed integer prefix.
26006        let rows = run_program(|b| {
26007            let end = b.emit_label();
26008            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26009            let r = b.alloc_reg();
26010            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("1.5e2abc".to_owned()), 0);
26011            b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0); // 'D' = INTEGER
26012            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26013            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26014            b.resolve_label(end);
26015        });
26016        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26017    }
26018
26019    #[test]
26020    fn test_cast_text_huge_exponent_to_integer_ignores_exponent() {
26021        let rows = run_program(|b| {
26022            let end = b.emit_label();
26023            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26024            let r = b.alloc_reg();
26025            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("1e999".to_owned()), 0);
26026            b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0); // 'D' = INTEGER
26027            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26028            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26029            b.resolve_label(end);
26030        });
26031        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26032    }
26033
26034    #[test]
26035    fn test_cast_text_prefix_to_real() {
26036        // Regression: CAST('3.14abc' AS REAL) must yield 3.14, not 0.0.
26037        let rows = run_program(|b| {
26038            let end = b.emit_label();
26039            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26040            let r = b.alloc_reg();
26041            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("3.14abc".to_owned()), 0);
26042            b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0); // 'E' = REAL
26043            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26044            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26045            b.resolve_label(end);
26046        });
26047        assert_eq!(rows[0], vec![SqliteValue::Float(3.14)]);
26048    }
26049
26050    #[test]
26051    fn test_cast_sci_notation_to_real() {
26052        // CAST('2.5e3xyz' AS REAL) must yield 2500.0.
26053        let rows = run_program(|b| {
26054            let end = b.emit_label();
26055            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26056            let r = b.alloc_reg();
26057            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("2.5e3xyz".to_owned()), 0);
26058            b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0); // 'E' = REAL
26059            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26060            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26061            b.resolve_label(end);
26062        });
26063        assert_eq!(rows[0], vec![SqliteValue::Float(2500.0)]);
26064    }
26065
26066    #[test]
26067    fn test_cast_text_prefix_to_numeric() {
26068        let rows = run_program(|b| {
26069            let end = b.emit_label();
26070            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26071            let r1 = b.alloc_reg();
26072            let r2 = b.alloc_reg();
26073            let r3 = b.alloc_reg();
26074            b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("123abc".to_owned()), 0);
26075            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("1.5e2abc".to_owned()), 0);
26076            b.emit_op(Opcode::String8, 0, r3, 0, P4::Str("abc".to_owned()), 0);
26077            b.emit_op(Opcode::Cast, r1, 67, 0, P4::None, 0); // 'C' = NUMERIC
26078            b.emit_op(Opcode::Cast, r2, 67, 0, P4::None, 0);
26079            b.emit_op(Opcode::Cast, r3, 67, 0, P4::None, 0);
26080            b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
26081            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26082            b.resolve_label(end);
26083        });
26084        assert_eq!(
26085            rows[0],
26086            vec![
26087                SqliteValue::Integer(123),
26088                SqliteValue::Integer(150),
26089                SqliteValue::Integer(0),
26090            ]
26091        );
26092    }
26093
26094    #[test]
26095    fn test_must_be_int_accepts_integer() {
26096        let rows = run_program(|b| {
26097            let end = b.emit_label();
26098            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26099            let r = b.alloc_reg();
26100            b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26101            // MustBeInt: p2=0 means error on non-int, but 42 is int → passes
26102            b.emit_op(Opcode::MustBeInt, r, 0, 0, P4::None, 0);
26103            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26104            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26105            b.resolve_label(end);
26106        });
26107        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26108    }
26109
26110    #[test]
26111    fn test_must_be_int_jumps_on_non_int() {
26112        // MustBeInt with p2 > 0: jump to p2 instead of error.
26113        let rows = run_program(|b| {
26114            let end = b.emit_label();
26115            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26116            let r = b.alloc_reg();
26117            let r_out = b.alloc_reg();
26118            b.emit_op(Opcode::String8, 0, r, 0, P4::Str("not_int".to_owned()), 0);
26119            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
26120            let fallback = b.emit_label();
26121            b.emit_jump_to_label(Opcode::MustBeInt, r, 0, fallback, P4::None, 0);
26122            let done = b.emit_label();
26123            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
26124            b.resolve_label(fallback);
26125            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
26126            b.resolve_label(done);
26127            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
26128            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26129            b.resolve_label(end);
26130        });
26131        // Non-int triggers jump → r_out = 1
26132        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26133    }
26134
26135    #[test]
26136    fn test_real_affinity_converts_int() {
26137        let rows = run_program(|b| {
26138            let end = b.emit_label();
26139            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26140            let r = b.alloc_reg();
26141            b.emit_op(Opcode::Integer, 7, r, 0, P4::None, 0);
26142            b.emit_op(Opcode::RealAffinity, r, 0, 0, P4::None, 0);
26143            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26144            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26145            b.resolve_label(end);
26146        });
26147        assert_eq!(rows[0], vec![SqliteValue::Float(7.0)]);
26148    }
26149
26150    #[test]
26151    fn test_real_affinity_no_op_on_float() {
26152        // RealAffinity on a float is a no-op.
26153        let rows = run_program(|b| {
26154            let end = b.emit_label();
26155            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26156            let r = b.alloc_reg();
26157            b.emit_op(Opcode::Real, 0, r, 0, P4::Real(std::f64::consts::PI), 0);
26158            b.emit_op(Opcode::RealAffinity, r, 0, 0, P4::None, 0);
26159            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26160            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26161            b.resolve_label(end);
26162        });
26163        assert_eq!(rows[0], vec![SqliteValue::Float(std::f64::consts::PI)]);
26164    }
26165
26166    // ── Error Handling ─────────────────────────────────────────────────
26167
26168    #[test]
26169    fn test_halt_if_null_triggers() {
26170        let mut b = ProgramBuilder::new();
26171        let end = b.emit_label();
26172        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26173        let r = b.alloc_reg();
26174        b.emit_op(Opcode::Null, 0, r, 0, P4::None, 0);
26175        b.emit_op(
26176            Opcode::HaltIfNull,
26177            19,
26178            0,
26179            r,
26180            P4::Str("NOT NULL constraint failed".to_owned()),
26181            0,
26182        );
26183        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26184        b.resolve_label(end);
26185        let prog = b.finish().unwrap();
26186        let mut engine = VdbeEngine::new(prog.register_count());
26187        let outcome = run_async(engine.execute(&prog)).unwrap();
26188        assert_eq!(
26189            outcome,
26190            ExecOutcome::Error {
26191                code: 19,
26192                message: "NOT NULL constraint failed".to_owned(),
26193            }
26194        );
26195    }
26196
26197    #[test]
26198    fn test_halt_if_null_passes_non_null() {
26199        let rows = run_program(|b| {
26200            let end = b.emit_label();
26201            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26202            let r = b.alloc_reg();
26203            b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26204            b.emit_op(
26205                Opcode::HaltIfNull,
26206                19,
26207                0,
26208                r,
26209                P4::Str("should not fire".to_owned()),
26210                0,
26211            );
26212            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26213            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26214            b.resolve_label(end);
26215        });
26216        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26217    }
26218
26219    #[test]
26220    fn test_typecheck_reports_sqlite_constraint_datatype_marker() {
26221        let mut b = ProgramBuilder::new();
26222        let end = b.emit_label();
26223        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26224        let r = b.alloc_reg();
26225        b.emit_op(Opcode::String8, 0, r, 0, P4::Str("bad".to_owned()), 0);
26226        // Encoded TypeCheck P4: "I\ttbl\tcol_a"
26227        b.emit_op(
26228            Opcode::TypeCheck,
26229            r,
26230            1,
26231            0,
26232            P4::Str("I\ttbl\tcol_a".to_owned()),
26233            0,
26234        );
26235        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26236        b.resolve_label(end);
26237
26238        let prog = b.finish().expect("program should build");
26239        let mut engine = VdbeEngine::new(prog.register_count());
26240        let err = run_async(engine.execute(&prog))
26241            .expect_err("typecheck should fail for TEXT into INTEGER STRICT slot");
26242        let err_text = err.to_string();
26243        assert!(
26244            err_text.contains("cannot store"),
26245            "error should mention 'cannot store': {err_text}"
26246        );
26247        assert!(
26248            err_text.contains("tbl.col_a"),
26249            "error should mention column: {err_text}"
26250        );
26251        assert_eq!(err.error_code(), ErrorCode::Constraint);
26252        assert_eq!(err.extended_error_code(), 3091); // SQLITE_CONSTRAINT_DATATYPE
26253    }
26254
26255    // ── Miscellaneous Opcodes ──────────────────────────────────────────
26256
26257    #[test]
26258    fn test_zero_or_null_opcode() {
26259        let rows = run_program(|b| {
26260            let end = b.emit_label();
26261            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26262            let non_null_lhs = b.alloc_reg();
26263            let non_null_rhs = b.alloc_reg();
26264            let null_lhs = b.alloc_reg();
26265            let null_rhs = b.alloc_reg();
26266            let o1 = b.alloc_reg();
26267            let o2 = b.alloc_reg();
26268            let o3 = b.alloc_reg();
26269            b.emit_op(Opcode::Integer, 7, non_null_lhs, 0, P4::None, 0);
26270            b.emit_op(Opcode::Integer, 11, non_null_rhs, 0, P4::None, 0);
26271            b.emit_op(Opcode::Null, 0, null_lhs, 0, P4::None, 0);
26272            b.emit_op(Opcode::Null, 0, null_rhs, 0, P4::None, 0);
26273            b.emit_op(
26274                Opcode::ZeroOrNull,
26275                non_null_lhs,
26276                o1,
26277                non_null_rhs,
26278                P4::None,
26279                0,
26280            );
26281            b.emit_op(Opcode::ZeroOrNull, null_lhs, o2, non_null_rhs, P4::None, 0);
26282            b.emit_op(Opcode::ZeroOrNull, non_null_lhs, o3, null_rhs, P4::None, 0);
26283            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26284            b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26285            b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26286            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26287            b.resolve_label(end);
26288        });
26289        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
26290        assert_eq!(rows[1], vec![SqliteValue::Null]);
26291        assert_eq!(rows[2], vec![SqliteValue::Null]);
26292    }
26293
26294    #[test]
26295    fn test_is_true_opcode() {
26296        let rows = run_program(|b| {
26297            let end = b.emit_label();
26298            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26299            let r_true = b.alloc_reg();
26300            let r_false = b.alloc_reg();
26301            let r_null = b.alloc_reg();
26302            let o1 = b.alloc_reg();
26303            let o2 = b.alloc_reg();
26304            let o3 = b.alloc_reg();
26305            b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26306            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26307            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26308            b.emit_op(Opcode::IsTrue, r_true, o1, 0, P4::None, 0);
26309            b.emit_op(Opcode::IsTrue, r_false, o2, 0, P4::None, 0);
26310            b.emit_op(Opcode::IsTrue, r_null, o3, 0, P4::None, 0);
26311            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26312            b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26313            b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26314            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26315            b.resolve_label(end);
26316        });
26317        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); // 42 is true
26318        assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); // 0 is false
26319        assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); // NULL is not true
26320    }
26321
26322    #[test]
26323    fn test_is_true_is_false_semantics() {
26324        // IS FALSE: P3=1, P4=1  →  NULL→0, truthy→0, falsy→1
26325        let rows = run_program(|b| {
26326            let end = b.emit_label();
26327            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26328            let r_true = b.alloc_reg();
26329            let r_false = b.alloc_reg();
26330            let r_null = b.alloc_reg();
26331            let o1 = b.alloc_reg();
26332            let o2 = b.alloc_reg();
26333            let o3 = b.alloc_reg();
26334            b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26335            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26336            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26337            b.emit_op(Opcode::IsTrue, r_true, o1, 1, P4::Int(1), 0);
26338            b.emit_op(Opcode::IsTrue, r_false, o2, 1, P4::Int(1), 0);
26339            b.emit_op(Opcode::IsTrue, r_null, o3, 1, P4::Int(1), 0);
26340            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26341            b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26342            b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26343            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26344            b.resolve_label(end);
26345        });
26346        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); // 42 IS FALSE → 0
26347        assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); // 0 IS FALSE → 1
26348        assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); // NULL IS FALSE → 0
26349    }
26350
26351    #[test]
26352    fn test_is_true_is_not_true_semantics() {
26353        // IS NOT TRUE: P3=0, P4=1  →  NULL→1, truthy→0, falsy→1
26354        let rows = run_program(|b| {
26355            let end = b.emit_label();
26356            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26357            let r_true = b.alloc_reg();
26358            let r_false = b.alloc_reg();
26359            let r_null = b.alloc_reg();
26360            let o1 = b.alloc_reg();
26361            let o2 = b.alloc_reg();
26362            let o3 = b.alloc_reg();
26363            b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26364            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26365            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26366            b.emit_op(Opcode::IsTrue, r_true, o1, 0, P4::Int(1), 0);
26367            b.emit_op(Opcode::IsTrue, r_false, o2, 0, P4::Int(1), 0);
26368            b.emit_op(Opcode::IsTrue, r_null, o3, 0, P4::Int(1), 0);
26369            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26370            b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26371            b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26372            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26373            b.resolve_label(end);
26374        });
26375        assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); // 42 IS NOT TRUE → 0
26376        assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); // 0 IS NOT TRUE → 1
26377        assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); // NULL IS NOT TRUE → 1
26378    }
26379
26380    #[test]
26381    fn test_is_true_is_not_false_semantics() {
26382        // IS NOT FALSE: P3=1, P4=0  →  NULL→1, truthy→1, falsy→0
26383        let rows = run_program(|b| {
26384            let end = b.emit_label();
26385            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26386            let r_true = b.alloc_reg();
26387            let r_false = b.alloc_reg();
26388            let r_null = b.alloc_reg();
26389            let o1 = b.alloc_reg();
26390            let o2 = b.alloc_reg();
26391            let o3 = b.alloc_reg();
26392            b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26393            b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26394            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26395            b.emit_op(Opcode::IsTrue, r_true, o1, 1, P4::Int(0), 0);
26396            b.emit_op(Opcode::IsTrue, r_false, o2, 1, P4::Int(0), 0);
26397            b.emit_op(Opcode::IsTrue, r_null, o3, 1, P4::Int(0), 0);
26398            b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26399            b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26400            b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26401            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26402            b.resolve_label(end);
26403        });
26404        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); // 42 IS NOT FALSE → 1
26405        assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); // 0 IS NOT FALSE → 0
26406        assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); // NULL IS NOT FALSE → 1
26407    }
26408
26409    #[test]
26410    fn test_noop_does_nothing() {
26411        let rows = run_program(|b| {
26412            let end = b.emit_label();
26413            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26414            let r = b.alloc_reg();
26415            b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26416            b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26417            b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26418            b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26419            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26420            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26421            b.resolve_label(end);
26422        });
26423        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26424    }
26425
26426    #[test]
26427    fn test_result_row_three_columns() {
26428        let rows = run_program(|b| {
26429            let end = b.emit_label();
26430            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26431            let r1 = b.alloc_reg();
26432            let r2 = b.alloc_reg();
26433            let r3 = b.alloc_reg();
26434            b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
26435            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("two".to_owned()), 0);
26436            b.emit_op(Opcode::Real, 0, r3, 0, P4::Real(3.0), 0);
26437            b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
26438            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26439            b.resolve_label(end);
26440        });
26441        assert_eq!(
26442            rows[0],
26443            vec![
26444                SqliteValue::Integer(1),
26445                SqliteValue::Text("two".into()),
26446                SqliteValue::Float(3.0),
26447            ]
26448        );
26449    }
26450
26451    #[test]
26452    fn test_multiple_result_rows() {
26453        let rows = run_program(|b| {
26454            let end = b.emit_label();
26455            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26456            let r = b.alloc_reg();
26457            b.emit_op(Opcode::Integer, 1, r, 0, P4::None, 0);
26458            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26459            b.emit_op(Opcode::Integer, 2, r, 0, P4::None, 0);
26460            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26461            b.emit_op(Opcode::Integer, 3, r, 0, P4::None, 0);
26462            b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26463            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26464            b.resolve_label(end);
26465        });
26466        assert_eq!(rows.len(), 3);
26467        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26468        assert_eq!(rows[1], vec![SqliteValue::Integer(2)]);
26469        assert_eq!(rows[2], vec![SqliteValue::Integer(3)]);
26470    }
26471
26472    #[test]
26473    fn test_gosub_nested() {
26474        // Test nested Gosub: main calls sub1, which calls sub2.
26475        let rows = run_program(|b| {
26476            let end = b.emit_label();
26477            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26478            let r_ret1 = b.alloc_reg();
26479            let r_ret2 = b.alloc_reg();
26480            let r_val = b.alloc_reg();
26481
26482            // Main: call sub1
26483            let sub1 = b.emit_label();
26484            b.emit_jump_to_label(Opcode::Gosub, r_ret1, 0, sub1, P4::None, 0);
26485            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26486            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26487
26488            // sub1: set r_val=10, call sub2, add 1
26489            b.resolve_label(sub1);
26490            b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
26491            let sub2 = b.emit_label();
26492            b.emit_jump_to_label(Opcode::Gosub, r_ret2, 0, sub2, P4::None, 0);
26493            b.emit_op(Opcode::AddImm, r_val, 1, 0, P4::None, 0);
26494            b.emit_op(Opcode::Return, r_ret1, 0, 0, P4::None, 0);
26495
26496            // sub2: multiply r_val by 5
26497            b.resolve_label(sub2);
26498            let r_five = b.alloc_reg();
26499            b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
26500            b.emit_op(Opcode::Multiply, r_five, r_val, r_val, P4::None, 0);
26501            b.emit_op(Opcode::Return, r_ret2, 0, 0, P4::None, 0);
26502
26503            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26504            b.resolve_label(end);
26505        });
26506        // 10 * 5 + 1 = 51
26507        assert_eq!(rows[0], vec![SqliteValue::Integer(51)]);
26508    }
26509
26510    #[test]
26511    fn test_coroutine_yield_resume() {
26512        // Producer coroutine yields 3 values; consumer resumes and emits rows.
26513        let rows = run_program(|b| {
26514            let end = b.emit_label();
26515            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26516
26517            let r_co = b.alloc_reg();
26518            let r_val = b.alloc_reg();
26519
26520            // Patch target addresses after both blocks are emitted.
26521            let init_addr = b.emit_op(Opcode::InitCoroutine, r_co, 0, 0, P4::None, 0);
26522            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
26523            let consumer_start = b.current_addr() as i32;
26524            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26525            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26526            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26527            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26528            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26529            b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26530            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26531
26532            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
26533            let producer_start = b.current_addr() as i32;
26534            b.emit_op(Opcode::Integer, 100, r_val, 0, P4::None, 0);
26535            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26536            b.emit_op(Opcode::Integer, 200, r_val, 0, P4::None, 0);
26537            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26538            b.emit_op(Opcode::Integer, 300, r_val, 0, P4::None, 0);
26539            b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26540            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26541
26542            if let Some(init_op) = b.op_at_mut(init_addr) {
26543                init_op.p2 = consumer_start;
26544                init_op.p3 = producer_start;
26545            }
26546
26547            b.resolve_label(end);
26548        });
26549        assert_eq!(rows.len(), 3);
26550        assert_eq!(rows[0], vec![SqliteValue::Integer(100)]);
26551        assert_eq!(rows[1], vec![SqliteValue::Integer(200)]);
26552        assert_eq!(rows[2], vec![SqliteValue::Integer(300)]);
26553    }
26554
26555    #[test]
26556    fn test_make_record_encodes_values() {
26557        // MakeRecord packs source registers into the SQLite record format blob.
26558        let rows = run_program(|b| {
26559            let end = b.emit_label();
26560            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26561            let r1 = b.alloc_reg();
26562            let r2 = b.alloc_reg();
26563            let r_rec = b.alloc_reg();
26564            b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
26565            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("a".to_owned()), 0);
26566            b.emit_op(Opcode::MakeRecord, r1, 2, r_rec, P4::None, 0);
26567            b.emit_op(Opcode::ResultRow, r_rec, 1, 0, P4::None, 0);
26568            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26569            b.resolve_label(end);
26570        });
26571        let produced_blob = rows.first().and_then(|row| row.first());
26572        assert!(
26573            matches!(produced_blob, Some(SqliteValue::Blob(_))),
26574            "MakeRecord should produce a blob"
26575        );
26576        let decoded = decode_record(&rows[0][0]).unwrap();
26577        assert_eq!(
26578            decoded,
26579            vec![SqliteValue::Integer(1), SqliteValue::Text("a".into())]
26580        );
26581    }
26582
26583    #[test]
26584    fn test_make_record_negative_source_register_stays_null() {
26585        let rows = run_program(|b| {
26586            let end = b.emit_label();
26587            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26588
26589            let r_record = b.alloc_reg();
26590            b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
26591            b.emit_op(Opcode::MakeRecord, -1, 1, r_record, P4::None, 0);
26592            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26593            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26594            b.resolve_label(end);
26595        });
26596
26597        let decoded = decode_record(&rows[0][0]).expect("record should decode");
26598        assert_eq!(decoded, vec![SqliteValue::Null]);
26599    }
26600
26601    #[test]
26602    fn test_make_record_precomputed_header_matches_generic_path() {
26603        let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26604            fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26605            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26606            fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
26607        ]);
26608
26609        let generic_rows = run_program(|b| {
26610            let end = b.emit_label();
26611            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26612            let r_rowid = b.alloc_reg();
26613            let r_int = b.alloc_reg();
26614            let r_real = b.alloc_reg();
26615            let r_record = b.alloc_reg();
26616            b.emit_op(Opcode::Integer, 77, r_rowid, 0, P4::None, 0);
26617            b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
26618            b.emit_op(Opcode::Real, 0, r_real, 0, P4::Real(2.5), 0);
26619            b.emit_op(
26620                Opcode::MakeRecord,
26621                r_rowid,
26622                3,
26623                r_record,
26624                P4::Affinity("XDE".to_owned()),
26625                0,
26626            );
26627            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26628            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26629            b.resolve_label(end);
26630        });
26631
26632        let precomputed_rows = run_program(|b| {
26633            let end = b.emit_label();
26634            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26635            let r_rowid = b.alloc_reg();
26636            let r_int = b.alloc_reg();
26637            let r_real = b.alloc_reg();
26638            let r_record = b.alloc_reg();
26639            b.emit_op(Opcode::Integer, 77, r_rowid, 0, P4::None, 0);
26640            b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
26641            b.emit_op(Opcode::Real, 0, r_real, 0, P4::Real(2.5), 0);
26642            b.emit_op(
26643                Opcode::MakeRecord,
26644                r_rowid,
26645                3,
26646                r_record,
26647                P4::PrecomputedHeader(precomputed.clone()),
26648                0,
26649            );
26650            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26651            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26652            b.resolve_label(end);
26653        });
26654
26655        assert_eq!(precomputed_rows, generic_rows);
26656        let decoded = decode_record(&precomputed_rows[0][0]).expect("record should decode");
26657        assert_eq!(
26658            decoded,
26659            vec![
26660                SqliteValue::Null,
26661                SqliteValue::Integer(5),
26662                SqliteValue::Float(2.5),
26663            ]
26664        );
26665    }
26666
26667    #[test]
26668    fn test_make_record_precomputed_header_falls_back_on_column_count_mismatch() {
26669        let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26670            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26671            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26672            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26673        ]);
26674
26675        let rows = run_program(|b| {
26676            let end = b.emit_label();
26677            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26678            let r_a = b.alloc_reg();
26679            let r_b = b.alloc_reg();
26680            let r_c = b.alloc_reg();
26681            let r_record = b.alloc_reg();
26682            b.emit_op(Opcode::Integer, 11, r_a, 0, P4::None, 0);
26683            b.emit_op(Opcode::Integer, 22, r_b, 0, P4::None, 0);
26684            b.emit_op(Opcode::Integer, 33, r_c, 0, P4::None, 0);
26685            b.emit_op(
26686                Opcode::MakeRecord,
26687                r_a,
26688                2,
26689                r_record,
26690                P4::PrecomputedHeader(precomputed),
26691                0,
26692            );
26693            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26694            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26695            b.resolve_label(end);
26696        });
26697
26698        let decoded = decode_record(&rows[0][0]).expect("record should decode");
26699        assert_eq!(
26700            decoded,
26701            vec![SqliteValue::Integer(11), SqliteValue::Integer(22)],
26702            "stale precomputed headers must not widen the record shape",
26703        );
26704    }
26705
26706    #[test]
26707    fn test_make_record_affinity_shape_follows_p2_column_count() {
26708        let _guard = VDBE_OBSERVABILITY_LOCK
26709            .lock()
26710            .unwrap_or_else(|e| e.into_inner());
26711
26712        let rows = run_program(|b| {
26713            let end = b.emit_label();
26714            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26715            let r_a = b.alloc_reg();
26716            let r_b = b.alloc_reg();
26717            let r_c = b.alloc_reg();
26718            let r_record_short_affinity = b.alloc_reg();
26719            let r_record_long_affinity = b.alloc_reg();
26720            b.emit_op(Opcode::Integer, 11, r_a, 0, P4::None, 0);
26721            b.emit_op(Opcode::Integer, 22, r_b, 0, P4::None, 0);
26722            b.emit_op(Opcode::Integer, 33, r_c, 0, P4::None, 0);
26723            b.emit_op(
26724                Opcode::MakeRecord,
26725                r_a,
26726                2,
26727                r_record_short_affinity,
26728                P4::Affinity("X".to_owned()),
26729                0,
26730            );
26731            b.emit_op(
26732                Opcode::MakeRecord,
26733                r_a,
26734                1,
26735                r_record_long_affinity,
26736                P4::Affinity("XDE".to_owned()),
26737                0,
26738            );
26739            b.emit_op(
26740                Opcode::ResultRow,
26741                r_record_short_affinity,
26742                2,
26743                0,
26744                P4::None,
26745                0,
26746            );
26747            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26748            b.resolve_label(end);
26749        });
26750
26751        let short_affinity =
26752            decode_record(&rows[0][0]).expect("short-affinity record should decode");
26753        assert_eq!(
26754            short_affinity,
26755            vec![SqliteValue::Null, SqliteValue::Integer(22)],
26756            "a short affinity string must not shrink the P2 register range",
26757        );
26758        let long_affinity = decode_record(&rows[0][1]).expect("long-affinity record should decode");
26759        assert_eq!(
26760            long_affinity,
26761            vec![SqliteValue::Null],
26762            "a long affinity string must not widen the P2 register range",
26763        );
26764    }
26765
26766    #[test]
26767    fn test_compiled_simple_insert_applies_make_record_null_placeholders() {
26768        let mut affinity_values = vec![
26769            SqliteValue::Integer(77),
26770            SqliteValue::Text("kept".into()),
26771            SqliteValue::Integer(9),
26772        ];
26773        VdbeEngine::apply_make_record_null_placeholders(
26774            &mut affinity_values,
26775            &P4::Affinity("XBD".to_owned()),
26776        );
26777        assert_eq!(
26778            affinity_values,
26779            vec![
26780                SqliteValue::Null,
26781                SqliteValue::Text("kept".into()),
26782                SqliteValue::Integer(9),
26783            ],
26784            "compiled INSERT must preserve MakeRecord X-affinity IPK placeholders",
26785        );
26786
26787        let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26788            fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26789            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26790        ]);
26791        let mut precomputed_values = vec![SqliteValue::Integer(88), SqliteValue::Integer(5)];
26792        VdbeEngine::apply_make_record_null_placeholders(
26793            &mut precomputed_values,
26794            &P4::PrecomputedHeader(precomputed),
26795        );
26796        assert_eq!(
26797            precomputed_values,
26798            vec![SqliteValue::Null, SqliteValue::Integer(5)],
26799            "compiled INSERT must preserve MakeRecord precomputed-header IPK placeholders",
26800        );
26801    }
26802
26803    #[test]
26804    fn test_compiled_record_builder_precomputed_header_matches_generic_and_logs_microbench() {
26805        let header = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26806            fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26807            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26808            fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
26809        ]);
26810        let builder = CompiledRecordBuilder::PrecomputedHeader(header);
26811        let mut values = vec![
26812            SqliteValue::Null,
26813            SqliteValue::Integer(200),
26814            SqliteValue::Float(2.5),
26815        ];
26816        let expected = serialize_record(&values);
26817
26818        let plan = build_compiled_record_write_plan(values.as_slice(), &builder);
26819        assert_eq!(plan.exact_size(), expected.len());
26820        let mut fused = vec![0; plan.exact_size()];
26821        plan.write_into_slice(&mut fused)
26822            .expect("precomputed record plan should write");
26823        assert_eq!(fused, expected);
26824
26825        let mut fallback_buf = Vec::new();
26826        serialize_compiled_record_into_vec(values.as_slice(), &builder, &mut fallback_buf);
26827        assert_eq!(fallback_buf, expected);
26828
26829        let iterations = 20_000usize;
26830        let mut generic_dst = Vec::new();
26831        let generic_start = Instant::now();
26832        for i in 0..iterations {
26833            values[1] = SqliteValue::Integer(200 + i64::try_from(i % 100).unwrap_or(0));
26834            let plan = fsqlite_types::record::plan_record_iter_serialization(values.iter());
26835            generic_dst.resize(plan.exact_size(), 0);
26836            plan.write_into_slice(generic_dst.as_mut_slice())
26837                .expect("generic plan should write");
26838            std::hint::black_box(&generic_dst);
26839        }
26840        let generic_elapsed = generic_start.elapsed();
26841
26842        let mut fused_dst = Vec::new();
26843        let fused_start = Instant::now();
26844        for i in 0..iterations {
26845            values[1] = SqliteValue::Integer(200 + i64::try_from(i % 100).unwrap_or(0));
26846            let plan = build_compiled_record_write_plan(values.as_slice(), &builder);
26847            fused_dst.resize(plan.exact_size(), 0);
26848            plan.write_into_slice(fused_dst.as_mut_slice())
26849                .expect("precomputed plan should write");
26850            std::hint::black_box(&fused_dst);
26851        }
26852        let fused_elapsed = fused_start.elapsed();
26853
26854        assert_eq!(fused_dst, generic_dst);
26855        eprintln!(
26856            "bd-q7qj6 record assembly microbench: iterations={iterations}, generic_plan_ns={}, fused_precomputed_ns={}, record_bytes={}",
26857            generic_elapsed.as_nanos(),
26858            fused_elapsed.as_nanos(),
26859            fused_dst.len()
26860        );
26861    }
26862
26863    #[test]
26864    fn test_make_record_sideband_is_invalidated_by_register_overwrite() {
26865        let rows = run_program(|b| {
26866            let end = b.emit_label();
26867            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26868            let r_value = b.alloc_reg();
26869            let r_record = b.alloc_reg();
26870            b.emit_op(Opcode::Integer, 1, r_value, 0, P4::None, 0);
26871            b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
26872            b.emit_op(Opcode::Integer, 42, r_record, 0, P4::None, 0);
26873            b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26874            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26875            b.resolve_label(end);
26876        });
26877
26878        assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
26879    }
26880
26881    #[test]
26882    fn test_write_text_to_reg_reuses_unique_heap_buffer() {
26883        let mut engine = VdbeEngine::new(2);
26884
26885        engine.write_text_to_reg(
26886            1,
26887            "this string is definitely longer than twenty three bytes",
26888        );
26889        let first_reg = engine.get_reg(1);
26890        assert!(
26891            matches!(first_reg, SqliteValue::Text(_)),
26892            "register should contain text"
26893        );
26894        let SqliteValue::Text(first) = first_reg else {
26895            return;
26896        };
26897        let original_ptr = first.as_str().as_ptr();
26898
26899        engine.write_text_to_reg(1, "another long string that still fits the same allocation");
26900
26901        let updated_reg = engine.get_reg(1);
26902        assert!(
26903            matches!(updated_reg, SqliteValue::Text(_)),
26904            "register should still contain text"
26905        );
26906        let SqliteValue::Text(updated) = updated_reg else {
26907            return;
26908        };
26909        assert_eq!(updated.as_str().as_ptr(), original_ptr);
26910        assert_eq!(
26911            updated.as_str(),
26912            "another long string that still fits the same allocation"
26913        );
26914    }
26915
26916    #[test]
26917    fn test_write_blob_to_reg_reuses_unique_buffer() {
26918        let mut engine = VdbeEngine::new(2);
26919
26920        engine.write_blob_to_reg(1, &[1_u8, 2, 3, 4]);
26921        let first_reg = engine.get_reg(1);
26922        assert!(
26923            matches!(first_reg, SqliteValue::Blob(_)),
26924            "register should contain blob"
26925        );
26926        let SqliteValue::Blob(first) = first_reg else {
26927            return;
26928        };
26929        let original_ptr = Arc::as_ptr(first);
26930
26931        engine.write_blob_to_reg(1, &[9_u8, 8, 7, 6]);
26932
26933        let updated_reg = engine.get_reg(1);
26934        assert!(
26935            matches!(updated_reg, SqliteValue::Blob(_)),
26936            "register should still contain blob"
26937        );
26938        let SqliteValue::Blob(updated) = updated_reg else {
26939            return;
26940        };
26941        assert_eq!(Arc::as_ptr(updated), original_ptr);
26942        assert_eq!(updated.as_ref(), &[9_u8, 8, 7, 6]);
26943    }
26944
26945    #[test]
26946    fn test_set_reg_returns_reusable_values_to_pool() {
26947        use fsqlite_types::value::{pool_clear, pool_len};
26948
26949        pool_clear();
26950        let mut engine = VdbeEngine::new(2);
26951
26952        engine.set_reg(
26953            1,
26954            SqliteValue::Text("this string is definitely longer than twenty three bytes".into()),
26955        );
26956        assert_eq!(pool_len(), 0);
26957
26958        engine.set_reg(1, SqliteValue::Integer(7));
26959        assert_eq!(pool_len(), 1);
26960
26961        pool_clear();
26962    }
26963
26964    #[test]
26965    fn test_set_reg_skips_shared_values_without_reusable_backing_storage() {
26966        use fsqlite_types::value::{pool_clear, pool_len};
26967
26968        pool_clear();
26969        let mut engine = VdbeEngine::new(2);
26970
26971        let shared_text =
26972            Arc::<str>::from("this string is definitely longer than twenty three bytes");
26973        engine.set_reg(
26974            1,
26975            SqliteValue::Text(SmallText::from_arc(Arc::clone(&shared_text))),
26976        );
26977        engine.set_reg(1, SqliteValue::Integer(7));
26978        assert_eq!(
26979            pool_len(),
26980            0,
26981            "shared text values should not fill the reusable slab on register overwrite",
26982        );
26983
26984        let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
26985        engine.set_reg(1, SqliteValue::Blob(Arc::clone(&shared_blob)));
26986        engine.set_reg(1, SqliteValue::Integer(8));
26987        assert_eq!(
26988            pool_len(),
26989            0,
26990            "shared blob values should not fill the reusable slab on register overwrite",
26991        );
26992
26993        pool_clear();
26994    }
26995
26996    #[test]
26997    fn test_make_record_retains_statement_scratch_capacity() {
26998        let mut builder = ProgramBuilder::new();
26999        let end = builder.emit_label();
27000        builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27001        let r_value = builder.alloc_reg();
27002        let r_record = builder.alloc_reg();
27003        builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str("x".repeat(256)), 0);
27004        builder.emit_op(
27005            Opcode::MakeRecord,
27006            r_value,
27007            1,
27008            r_record,
27009            P4::Affinity("B".to_owned()),
27010            0,
27011        );
27012        builder.emit_op(
27013            Opcode::String8,
27014            0,
27015            r_value,
27016            0,
27017            P4::Str("tiny".to_owned()),
27018            0,
27019        );
27020        builder.emit_op(
27021            Opcode::MakeRecord,
27022            r_value,
27023            1,
27024            r_record,
27025            P4::Affinity("B".to_owned()),
27026            0,
27027        );
27028        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27029        builder.resolve_label(end);
27030
27031        let program = builder.finish().expect("program should build");
27032        let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
27033        let mut engine = VdbeEngine::new(program.register_count());
27034
27035        let outcome = run_async(engine.execute(&program)).expect("program should execute");
27036        assert!(matches!(outcome, ExecOutcome::Done));
27037        assert!(
27038            engine.make_record_lookaside.capacity() >= expected_capacity,
27039            "MakeRecord scratch should retain its pre-sized capacity across the statement"
27040        );
27041        assert!(
27042            !engine.make_record_lookaside.is_empty(),
27043            "final MakeRecord should keep record bytes in the sideband until consumed"
27044        );
27045        let materialized = engine.clone_reg_materialized(r_record);
27046        assert!(
27047            engine.make_record_lookaside.is_empty(),
27048            "statement scratch should be length-reset after sideband materialization"
27049        );
27050        let decoded = decode_record(&materialized).expect("record should decode");
27051        assert_eq!(decoded, vec![SqliteValue::Text("tiny".into())]);
27052    }
27053
27054    #[test]
27055    fn test_function_negative_argument_register_stays_null() {
27056        let rows = run_program_with_functions(|b| {
27057            let end = b.emit_label();
27058            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27059
27060            let r_out = b.alloc_reg();
27061            b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27062            b.emit_op(
27063                Opcode::Function,
27064                0,
27065                -1,
27066                r_out,
27067                P4::FuncName("typeof".to_owned()),
27068                1,
27069            );
27070            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27071            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27072            b.resolve_label(end);
27073        });
27074
27075        assert_eq!(rows, vec![vec![SqliteValue::Text("null".into())]]);
27076    }
27077
27078    #[test]
27079    fn test_result_row_negative_start_register_stays_null() {
27080        let rows = run_program(|b| {
27081            let end = b.emit_label();
27082            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27083
27084            b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27085            b.emit_op(Opcode::ResultRow, -1, 1, 0, P4::None, 0);
27086            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27087            b.resolve_label(end);
27088        });
27089
27090        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
27091    }
27092
27093    #[test]
27094    fn test_move_negative_source_register_stays_null() {
27095        let rows = run_program(|b| {
27096            let end = b.emit_label();
27097            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27098
27099            let r_dest = b.alloc_reg();
27100            b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27101            b.emit_op(Opcode::Move, -1, r_dest, 1, P4::None, 0);
27102            b.emit_op(Opcode::ResultRow, r_dest, 1, 0, P4::None, 0);
27103            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27104            b.resolve_label(end);
27105        });
27106
27107        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
27108    }
27109
27110    #[test]
27111    fn test_complex_expression_chain() {
27112        // Test: ((10 + 20) * 3 - 5) / 2 = (90 - 5) / 2 = 85 / 2 = 42
27113        let rows = run_program(|b| {
27114            let end = b.emit_label();
27115            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27116            let r10 = b.alloc_reg();
27117            let r20 = b.alloc_reg();
27118            let r3 = b.alloc_reg();
27119            let r5 = b.alloc_reg();
27120            let r2 = b.alloc_reg();
27121            let t1 = b.alloc_reg();
27122            let t2 = b.alloc_reg();
27123            let t3 = b.alloc_reg();
27124            b.emit_op(Opcode::Integer, 10, r10, 0, P4::None, 0);
27125            b.emit_op(Opcode::Integer, 20, r20, 0, P4::None, 0);
27126            b.emit_op(Opcode::Integer, 3, r3, 0, P4::None, 0);
27127            b.emit_op(Opcode::Integer, 5, r5, 0, P4::None, 0);
27128            b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
27129            b.emit_op(Opcode::Add, r10, r20, t1, P4::None, 0); // 30
27130            b.emit_op(Opcode::Multiply, r3, t1, t2, P4::None, 0); // 90
27131            b.emit_op(Opcode::Subtract, r5, t2, t2, P4::None, 0); // 85
27132            b.emit_op(Opcode::Divide, r2, t2, t3, P4::None, 0); // 42
27133            b.emit_op(Opcode::ResultRow, t3, 1, 0, P4::None, 0);
27134            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27135            b.resolve_label(end);
27136        });
27137        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
27138    }
27139
27140    #[test]
27141    fn test_string_comparison() {
27142        // String comparison: 'abc' < 'abd' → true
27143        let rows = run_program(|b| {
27144            let end = b.emit_label();
27145            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27146            let r1 = b.alloc_reg();
27147            let r2 = b.alloc_reg();
27148            let r_out = b.alloc_reg();
27149            b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("abd".to_owned()), 0);
27150            b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("abc".to_owned()), 0);
27151            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
27152            let taken = b.emit_label();
27153            // Lt: if p3 (r2="abc") < p1 (r1="abd"), jump
27154            b.emit_jump_to_label(Opcode::Lt, r1, r2, taken, P4::None, 0);
27155            let done = b.emit_label();
27156            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27157            b.resolve_label(taken);
27158            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
27159            b.resolve_label(done);
27160            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27161            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27162            b.resolve_label(end);
27163        });
27164        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
27165    }
27166
27167    #[test]
27168    fn test_mixed_type_comparison() {
27169        // Integer vs Float comparison: 5 == 5.0
27170        let rows = run_program(|b| {
27171            let end = b.emit_label();
27172            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27173            let r_int = b.alloc_reg();
27174            let r_float = b.alloc_reg();
27175            let r_out = b.alloc_reg();
27176            b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
27177            b.emit_op(Opcode::Real, 0, r_float, 0, P4::Real(5.0), 0);
27178            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
27179            let taken = b.emit_label();
27180            b.emit_jump_to_label(Opcode::Eq, r_int, r_float, taken, P4::None, 0);
27181            let done = b.emit_label();
27182            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27183            b.resolve_label(taken);
27184            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
27185            b.resolve_label(done);
27186            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27187            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27188            b.resolve_label(end);
27189        });
27190        assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
27191    }
27192
27193    // ── bd-1s7a: Storage cursor acceptance tests ───────────────────────
27194
27195    /// Build and execute a program with a MemDatabase + storage cursors enabled.
27196    fn run_with_storage_cursors(
27197        db: MemDatabase,
27198        build: impl FnOnce(&mut ProgramBuilder),
27199    ) -> Vec<Vec<SqliteValue>> {
27200        let mut b = ProgramBuilder::new();
27201        build(&mut b);
27202        let prog = b.finish().expect("program should build");
27203        let mut engine = VdbeEngine::new(prog.register_count());
27204        engine.enable_storage_read_cursors(true);
27205        engine.set_database(db);
27206        // These tests exercise the MemPageStore path without a real pager txn.
27207        engine.set_reject_mem_fallback(false);
27208        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27209        assert_eq!(outcome, ExecOutcome::Done);
27210        engine
27211            .take_results()
27212            .into_iter()
27213            .map(|v| v.into_vec())
27214            .collect()
27215    }
27216
27217    #[test]
27218    fn test_vdbe_openread_uses_btree_cursor_backend() {
27219        // Insert rows into a MemDatabase, then verify OpenRead routes through
27220        // the storage cursor path (not MemCursor) when enabled.
27221        let mut db = MemDatabase::new();
27222        let root = db.create_table(2);
27223        let table = db.get_table_mut(root).unwrap();
27224        table.insert(
27225            1,
27226            vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
27227        );
27228        table.insert(
27229            2,
27230            vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
27231        );
27232
27233        let rows = run_with_storage_cursors(db, |b| {
27234            let end = b.emit_label();
27235            let done = b.emit_label();
27236            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27237            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(2), 0);
27238            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27239
27240            let body = b.current_addr();
27241            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27242            b.emit_op(Opcode::Column, 0, 1, 2, P4::None, 0);
27243            b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
27244
27245            let next_target =
27246                i32::try_from(body).expect("program counter should fit into i32 for tests");
27247            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27248
27249            b.resolve_label(done);
27250            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27251            b.resolve_label(end);
27252        });
27253
27254        assert_eq!(rows.len(), 2, "should return 2 rows via storage cursor");
27255        assert_eq!(rows[0][0], SqliteValue::Integer(10));
27256        assert_eq!(rows[0][1], SqliteValue::Text("a".into()));
27257        assert_eq!(rows[1][0], SqliteValue::Integer(20));
27258        assert_eq!(rows[1][1], SqliteValue::Text("b".into()));
27259    }
27260
27261    #[test]
27262    fn test_select_uses_storage_cursor_not_memdb_for_persisted_table() {
27263        // With storage cursors enabled, verify the engine uses StorageCursor
27264        // (the read path) rather than MemCursor for OpenRead.
27265        let mut db = MemDatabase::new();
27266        let root = db.create_table(1);
27267        let table = db.get_table_mut(root).unwrap();
27268        table.insert(1, vec![SqliteValue::Integer(42)]);
27269
27270        let rows = run_with_storage_cursors(db, |b| {
27271            let end = b.emit_label();
27272            let done = b.emit_label();
27273            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27274            // OpenRead with storage cursors enabled should use StorageCursor.
27275            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
27276            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27277
27278            let body = b.current_addr();
27279            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27280            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27281            let next_target =
27282                i32::try_from(body).expect("program counter should fit into i32 for tests");
27283            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27284
27285            b.resolve_label(done);
27286            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27287            b.resolve_label(end);
27288        });
27289
27290        assert_eq!(rows.len(), 1);
27291        assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
27292    }
27293
27294    #[test]
27295    fn test_count_uses_exact_cardinality_for_storage_cursor() {
27296        let mut db = MemDatabase::new();
27297        let root = db.create_table(1);
27298        let table = db.get_table_mut(root).unwrap();
27299        table.insert(1, vec![SqliteValue::Integer(10)]);
27300        table.insert(2, vec![SqliteValue::Integer(20)]);
27301        table.insert(3, vec![SqliteValue::Integer(30)]);
27302
27303        let rows = run_with_storage_cursors(db, |b| {
27304            let end = b.emit_label();
27305            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27306            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
27307            b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27308            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27309            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27310            b.resolve_label(end);
27311        });
27312
27313        assert_eq!(rows, vec![vec![SqliteValue::Integer(3)]]);
27314    }
27315
27316    #[test]
27317    fn test_count_on_writable_storage_cursor_ignores_hydrated_memdb_fast_path() {
27318        let mut db = MemDatabase::new();
27319        let root = db.create_table(1);
27320        let table = db.get_table_mut(root).unwrap();
27321        table.insert(1, vec![SqliteValue::Integer(10)]);
27322        table.insert(2, vec![SqliteValue::Integer(20)]);
27323        table.insert(3, vec![SqliteValue::Integer(30)]);
27324
27325        let mut b = ProgramBuilder::new();
27326        let end = b.emit_label();
27327        let done = b.emit_label();
27328        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27329        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27330        b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27331        b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27332        b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27333        b.emit_op(Opcode::Count, 0, 2, 0, P4::None, 0);
27334        b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27335        b.resolve_label(done);
27336        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27337        b.resolve_label(end);
27338
27339        let prog = b.finish().expect("program should build");
27340        let mut engine = VdbeEngine::new(prog.register_count());
27341        engine.enable_storage_cursors(true);
27342        engine.set_database(db);
27343        engine.set_memdb_rows_loaded(true);
27344        engine.set_reject_mem_fallback(false);
27345
27346        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27347        assert_eq!(outcome, ExecOutcome::Done);
27348        let rows: Vec<_> = engine
27349            .take_results()
27350            .into_iter()
27351            .map(|row| row.into_vec())
27352            .collect();
27353        assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27354    }
27355
27356    #[test]
27357    fn test_count_on_mem_cursor_uses_memdb_rows() {
27358        let mut db = MemDatabase::new();
27359        let root = db.create_table(1);
27360        let table = db.get_table_mut(root).unwrap();
27361        table.insert(1, vec![SqliteValue::Integer(10)]);
27362        table.insert(2, vec![SqliteValue::Integer(20)]);
27363
27364        let mut b = ProgramBuilder::new();
27365        let end = b.emit_label();
27366        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27367        b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27368        b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27369        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27370        b.resolve_label(end);
27371
27372        let prog = b.finish().expect("program should build");
27373        let mut engine = VdbeEngine::new(prog.register_count());
27374        engine.set_database(db);
27375        engine.set_memdb_rows_loaded(true);
27376        engine.cursors.insert(0, MemCursor::new(root, false));
27377
27378        let rows = execute_program_with_engine(&mut engine, &prog);
27379
27380        assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27381    }
27382
27383    #[test]
27384    fn test_count_on_readonly_storage_cursor_ignores_stale_memdb_shortcut() {
27385        let mut db = MemDatabase::new();
27386        let root = db.create_table(1);
27387
27388        let mut b = ProgramBuilder::new();
27389        let end = b.emit_label();
27390        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27391        b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27392        b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27393        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27394        b.resolve_label(end);
27395
27396        let prog = b.finish().expect("program should build");
27397        let mut engine = VdbeEngine::new(prog.register_count());
27398        engine.enable_storage_cursors(true);
27399        engine.set_database(db);
27400        engine.set_memdb_rows_loaded(true);
27401        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
27402        engine.set_reject_mem_fallback(false);
27403
27404        assert!(run_async(engine.open_storage_cursor(0, root, true)));
27405        engine.cursor_root_pages.insert(0, root);
27406        {
27407            let sc = engine
27408                .storage_cursors
27409                .get_mut(&0)
27410                .expect("storage cursor should exist");
27411            run_async(sc.cursor.table_insert(
27412                &sc.cx,
27413                1,
27414                &encode_record(&[SqliteValue::Integer(10)]),
27415            ))
27416            .unwrap();
27417            run_async(sc.cursor.table_insert(
27418                &sc.cx,
27419                2,
27420                &encode_record(&[SqliteValue::Integer(20)]),
27421            ))
27422            .unwrap();
27423            sc.writable = false;
27424        }
27425
27426        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27427        assert_eq!(outcome, ExecOutcome::Done);
27428        let rows: Vec<_> = engine
27429            .take_results()
27430            .into_iter()
27431            .map(|row| row.into_vec())
27432            .collect();
27433        assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27434
27435        let db = engine.take_database().expect("database should exist");
27436        let table = db.get_table(root).expect("table should exist");
27437        assert_eq!(table.rows.len(), 0, "attached MemDatabase stays stale here");
27438    }
27439
27440    // ── bd-3iw8 / bd-25c6: Storage cursor WRITE path tests ────────────
27441
27442    /// Build and execute a write program with storage cursors enabled.
27443    /// Returns both the result rows and the final MemDatabase state.
27444    fn run_write_with_storage_cursors(
27445        db: MemDatabase,
27446        build: impl FnOnce(&mut ProgramBuilder),
27447    ) -> (Vec<Vec<SqliteValue>>, MemDatabase) {
27448        let mut b = ProgramBuilder::new();
27449        build(&mut b);
27450        let prog = b.finish().expect("program should build");
27451        let mut engine = VdbeEngine::new(prog.register_count());
27452        engine.enable_storage_cursors(true);
27453        engine.set_database(db);
27454        // These tests exercise the MemPageStore path without a real pager txn.
27455        engine.set_reject_mem_fallback(false);
27456        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27457        assert_eq!(outcome, ExecOutcome::Done);
27458        let results: Vec<_> = engine
27459            .take_results()
27460            .into_iter()
27461            .map(|v| v.into_vec())
27462            .collect();
27463        let db = engine.take_database().expect("database should exist");
27464        (results, db)
27465    }
27466
27467    #[test]
27468    fn test_openwrite_uses_storage_cursor_backend() {
27469        // Verify OpenWrite routes through StorageCursor when enabled.
27470        let mut db = MemDatabase::new();
27471        let root = db.create_table(1);
27472        let table = db.get_table_mut(root).unwrap();
27473        table.insert(1, vec![SqliteValue::Integer(100)]);
27474
27475        let mut b = ProgramBuilder::new();
27476        let end = b.emit_label();
27477        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27478        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27479        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27480        b.resolve_label(end);
27481
27482        let prog = b.finish().expect("program should build");
27483        let mut engine = VdbeEngine::new(prog.register_count());
27484        engine.enable_storage_cursors(true);
27485        engine.set_database(db);
27486        engine.set_reject_mem_fallback(false);
27487        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27488        assert_eq!(outcome, ExecOutcome::Done);
27489        // Verify the cursor was opened as a storage cursor, not a MemCursor.
27490        assert!(
27491            engine.storage_cursors.contains_key(&0),
27492            "OpenWrite should route through StorageCursor"
27493        );
27494        assert!(!engine.cursors.contains_key(&0));
27495        // Verify it's marked writable.
27496        assert!(
27497            engine
27498                .storage_cursors
27499                .get(&0)
27500                .is_some_and(|cursor| cursor.writable)
27501        );
27502    }
27503
27504    #[test]
27505    fn test_insert_via_storage_cursor_write_path() {
27506        // Phase 5B.2 (bd-1yi8): INSERT goes ONLY through StorageCursor
27507        // (B-tree write path), NOT synced to MemDatabase.
27508        // Read-back uses the SAME cursor (Rewind) since the MemPageStore
27509        // is per-cursor and not shared across Close/OpenRead.
27510        let mut db = MemDatabase::new();
27511        let root = db.create_table(2);
27512
27513        let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
27514            let end = b.emit_label();
27515            let done = b.emit_label();
27516            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27517
27518            // OpenWrite cursor 0 on root page.
27519            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27520
27521            // NewRowid → r1.
27522            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
27523
27524            // Build record: r2=42, r3="hello" → MakeRecord → r4.
27525            b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
27526            b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("hello".to_owned()), 0);
27527            b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
27528
27529            // Insert(cursor=0, record=r4, rowid=r1).
27530            b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
27531
27532            // Read back via same cursor: Rewind then Column/ResultRow.
27533            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27534
27535            let body = b.current_addr();
27536            b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27537            b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27538            b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27539            let next_target =
27540                i32::try_from(body).expect("program counter should fit into i32 for tests");
27541            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27542
27543            b.resolve_label(done);
27544            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27545            b.resolve_label(end);
27546        });
27547
27548        // Write-through: MemDatabase should NOT have the row.
27549        let table = final_db.get_table(root).expect("table should exist");
27550        assert_eq!(
27551            table.rows.len(),
27552            0,
27553            "MemDatabase must not be synced in write-through mode"
27554        );
27555
27556        // Data readable from B-tree via same cursor.
27557        assert_eq!(
27558            rows.len(),
27559            1,
27560            "should read back exactly one row from B-tree"
27561        );
27562        assert_eq!(rows[0][0], SqliteValue::Integer(42));
27563        assert_eq!(rows[0][1], SqliteValue::Text("hello".into()));
27564    }
27565
27566    #[test]
27567    fn test_fused_append_insert_honors_make_record_p4_metadata() {
27568        let mut db = MemDatabase::new();
27569        let root = db.create_table(2);
27570
27571        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27572            let end = b.emit_label();
27573            let done = b.emit_label();
27574            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27575
27576            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27577            b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
27578            b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("fused".to_owned()), 0);
27579            b.emit_op(
27580                Opcode::FusedAppendInsert,
27581                0,
27582                2,
27583                2,
27584                P4::Affinity("XD".to_owned()),
27585                2,
27586            );
27587
27588            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27589            let body = b.current_addr();
27590            b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27591            b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27592            b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27593            let next_target =
27594                i32::try_from(body).expect("program counter should fit into i32 for tests");
27595            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27596
27597            b.resolve_label(done);
27598            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27599            b.resolve_label(end);
27600        });
27601
27602        assert_eq!(
27603            rows,
27604            vec![vec![SqliteValue::Null, SqliteValue::Text("fused".into())]],
27605            "FusedAppendInsert must preserve MakeRecord X-affinity placeholder semantics",
27606        );
27607    }
27608
27609    #[test]
27610    fn test_fused_append_insert_honors_precomputed_record_header() {
27611        let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
27612            fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
27613            fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
27614        ]);
27615        let mut db = MemDatabase::new();
27616        let root = db.create_table(2);
27617
27618        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27619            let end = b.emit_label();
27620            let done = b.emit_label();
27621            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27622
27623            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27624            b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
27625            b.emit_op(Opcode::Integer, 7, 3, 0, P4::None, 0);
27626            b.emit_op(
27627                Opcode::FusedAppendInsert,
27628                0,
27629                2,
27630                2,
27631                P4::PrecomputedHeader(precomputed),
27632                2,
27633            );
27634
27635            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27636            let body = b.current_addr();
27637            b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27638            b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27639            b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27640            let next_target =
27641                i32::try_from(body).expect("program counter should fit into i32 for tests");
27642            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27643
27644            b.resolve_label(done);
27645            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27646            b.resolve_label(end);
27647        });
27648
27649        assert_eq!(
27650            rows,
27651            vec![vec![SqliteValue::Null, SqliteValue::Integer(7)]],
27652            "FusedAppendInsert must preserve MakeRecord precomputed-header placeholders",
27653        );
27654    }
27655
27656    #[test]
27657    fn test_delete_via_storage_cursor_write_path() {
27658        // Insert a row into MemDatabase, open a writable StorageCursor,
27659        // position on it, delete it, and verify data is removed from the
27660        // B-tree while MemDatabase remains unchanged (write-through mode).
27661        let mut db = MemDatabase::new();
27662        let root = db.create_table(1);
27663        let table = db.get_table_mut(root).unwrap();
27664        table.insert(1, vec![SqliteValue::Integer(10)]);
27665        table.insert(2, vec![SqliteValue::Integer(20)]);
27666        table.insert(3, vec![SqliteValue::Integer(30)]);
27667
27668        let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
27669            let end = b.emit_label();
27670            let done = b.emit_label();
27671            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27672
27673            // Open writable cursor.
27674            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27675
27676            // Seek to rowid=2 (register 1). Jump to end if not found.
27677            b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27678            b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27679
27680            // Delete the current row.
27681            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27682
27683            // Read back rowids from B-tree to verify rowid=2 was deleted.
27684            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27685            let body = b.current_addr();
27686            b.emit_op(Opcode::Rowid, 0, 2, 0, P4::None, 0);
27687            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27688            let next_target =
27689                i32::try_from(body).expect("program counter should fit into i32 for tests");
27690            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27691
27692            b.resolve_label(done);
27693            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27694            b.resolve_label(end);
27695        });
27696
27697        assert_eq!(
27698            rows,
27699            vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)],],
27700            "B-tree cursor should observe rowid=2 deleted"
27701        );
27702
27703        // MemDatabase should remain unchanged in write-through mode.
27704        let table = final_db.get_table(root).expect("table should exist");
27705        assert_eq!(table.rows.len(), 3);
27706        let rowids: Vec<i64> = table.rows.iter().map(|r| r.rowid).collect();
27707        assert!(rowids.contains(&1));
27708        assert!(rowids.contains(&2));
27709        assert!(rowids.contains(&3));
27710    }
27711
27712    #[test]
27713    fn test_delete_invalidates_cached_storage_row_before_successor_column() {
27714        let mut db = MemDatabase::new();
27715        let root = db.create_table(1);
27716        let table = db.get_table_mut(root).unwrap();
27717        table.insert(1, vec![SqliteValue::Integer(10)]);
27718        table.insert(2, vec![SqliteValue::Integer(20)]);
27719        table.insert(3, vec![SqliteValue::Integer(30)]);
27720
27721        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27722            let end = b.emit_label();
27723            let done = b.emit_label();
27724            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27725            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27726            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27727
27728            // Prime the per-row column cache on rowid=1/value=10.
27729            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27730
27731            // Delete the current row. The cursor now lands on the successor.
27732            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27733
27734            // Without cache invalidation this would incorrectly return 10 again.
27735            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
27736            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27737            b.resolve_label(done);
27738            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27739            b.resolve_label(end);
27740        });
27741
27742        assert_eq!(rows, vec![vec![SqliteValue::Integer(20)]]);
27743    }
27744
27745    #[test]
27746    fn test_storage_cursor_delete_write_boundary_invalidates_decode_cache_once() {
27747        let _guard = VDBE_OBSERVABILITY_LOCK
27748            .lock()
27749            .unwrap_or_else(|e| e.into_inner());
27750        let prev_metrics_enabled = vdbe_metrics_enabled();
27751        reset_vdbe_metrics();
27752        set_vdbe_metrics_enabled(true);
27753
27754        let mut db = MemDatabase::new();
27755        let root = db.create_table(1);
27756        let table = db.get_table_mut(root).unwrap();
27757        table.insert(1, vec![SqliteValue::Integer(10)]);
27758        table.insert(2, vec![SqliteValue::Integer(20)]);
27759
27760        let before = vdbe_metrics_snapshot();
27761        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27762            let end = b.emit_label();
27763            let done = b.emit_label();
27764            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27765            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27766            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27767
27768            // Prime the cache on the current row, then mutate through the same
27769            // cursor and read the successor. The stale row image must not survive
27770            // across the write boundary.
27771            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27772            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27773            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
27774            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27775            b.resolve_label(done);
27776            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27777            b.resolve_label(end);
27778        });
27779        let after = vdbe_metrics_snapshot();
27780
27781        assert_eq!(rows, vec![vec![SqliteValue::Integer(20)]]);
27782        assert_eq!(
27783            after.decode_cache_invalidations_write_total
27784                - before.decode_cache_invalidations_write_total,
27785            1
27786        );
27787        assert_eq!(
27788            after.decode_cache_invalidations_position_total
27789                - before.decode_cache_invalidations_position_total,
27790            0
27791        );
27792        assert_eq!(
27793            after.decode_cache_hits_total - before.decode_cache_hits_total,
27794            0
27795        );
27796        assert_eq!(
27797            after.decode_cache_misses_total - before.decode_cache_misses_total,
27798            2
27799        );
27800
27801        set_vdbe_metrics_enabled(prev_metrics_enabled);
27802    }
27803
27804    #[test]
27805    fn test_delete_then_prev_then_next_advances_correctly() {
27806        // Regression: after Delete marks pending_next_after_delete, a
27807        // subsequent Prev must clear that pending state. Otherwise the next
27808        // Next call can incorrectly "stay put" and repeat the same row.
27809        let mut db = MemDatabase::new();
27810        let root = db.create_table(1);
27811        let table = db.get_table_mut(root).unwrap();
27812        table.insert(1, vec![SqliteValue::Integer(10)]);
27813        table.insert(2, vec![SqliteValue::Integer(20)]);
27814        table.insert(3, vec![SqliteValue::Integer(30)]);
27815
27816        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27817            let end = b.emit_label();
27818            let done = b.emit_label();
27819            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27820            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27821
27822            // Seek rowid=2 and delete it. Cursor should land on successor.
27823            b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27824            b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27825            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27826
27827            // Step backward once (to rowid=1) and emit it.
27828            let prev_ok = b.emit_label();
27829            b.emit_jump_to_label(Opcode::Prev, 0, 0, prev_ok, P4::None, 0);
27830            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27831            b.resolve_label(prev_ok);
27832            b.emit_op(Opcode::Rowid, 0, 2, 0, P4::None, 0);
27833            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27834
27835            // Now step forward once. Correct behavior is rowid=3 (not 1).
27836            let next_ok = b.emit_label();
27837            b.emit_jump_to_label(Opcode::Next, 0, 0, next_ok, P4::None, 0);
27838            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27839            b.resolve_label(next_ok);
27840            b.emit_op(Opcode::Rowid, 0, 3, 0, P4::None, 0);
27841            b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27842
27843            b.resolve_label(done);
27844            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27845            b.resolve_label(end);
27846        });
27847
27848        assert_eq!(
27849            rows,
27850            vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)]],
27851            "Delete->Prev->Next should land on rowids 1 then 3 without repeating row 1"
27852        );
27853    }
27854
27855    #[test]
27856    fn test_delete_then_notexists_then_next_advances_from_probe_position() {
27857        // Regression: NotExists on storage cursor repositions via table_move_to.
27858        // If pending_next_after_delete is left stale, a following Next can
27859        // incorrectly repeat the probe row instead of advancing.
27860        let mut db = MemDatabase::new();
27861        let root = db.create_table(1);
27862        let table = db.get_table_mut(root).unwrap();
27863        table.insert(1, vec![SqliteValue::Integer(10)]);
27864        table.insert(2, vec![SqliteValue::Integer(20)]);
27865        table.insert(3, vec![SqliteValue::Integer(30)]);
27866
27867        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27868            let end = b.emit_label();
27869            let done = b.emit_label();
27870            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27871            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27872
27873            // Delete rowid=2.
27874            b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27875            b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27876            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27877
27878            // Probe rowid=1 via NotExists (falls through when row exists).
27879            let probe_missing = b.emit_label();
27880            b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
27881            b.emit_jump_to_label(Opcode::NotExists, 0, 2, probe_missing, P4::None, 0);
27882
27883            // Emit current probe position (rowid=1).
27884            b.emit_op(Opcode::Rowid, 0, 3, 0, P4::None, 0);
27885            b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27886
27887            // Next should advance to rowid=3 (not repeat rowid=1).
27888            let next_ok = b.emit_label();
27889            b.emit_jump_to_label(Opcode::Next, 0, 0, next_ok, P4::None, 0);
27890            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27891            b.resolve_label(next_ok);
27892            b.emit_op(Opcode::Rowid, 0, 4, 0, P4::None, 0);
27893            b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
27894            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27895
27896            // Missing-probe path (not expected in this fixture).
27897            b.resolve_label(probe_missing);
27898            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27899
27900            b.resolve_label(done);
27901            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27902            b.resolve_label(end);
27903        });
27904
27905        assert_eq!(
27906            rows,
27907            vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)]],
27908            "Delete->NotExists->Next should advance from probe row 1 to row 3"
27909        );
27910    }
27911
27912    #[test]
27913    fn test_delete_then_newrowid_then_next_reports_no_successor() {
27914        // Regression: NewRowid on storage cursor repositions with `last()`.
27915        // If pending_next_after_delete is stale, Next can incorrectly report
27916        // a successor row when already at the end.
27917        let mut db = MemDatabase::new();
27918        let root = db.create_table(1);
27919        let table = db.get_table_mut(root).unwrap();
27920        table.insert(1, vec![SqliteValue::Integer(10)]);
27921        table.insert(2, vec![SqliteValue::Integer(20)]);
27922        table.insert(3, vec![SqliteValue::Integer(30)]);
27923
27924        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27925            let end = b.emit_label();
27926            let done = b.emit_label();
27927            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27928            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27929
27930            // Delete rowid=2 (cursor lands on successor rowid=3).
27931            b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27932            b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27933            b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27934
27935            // NewRowid probes max rowid via last(); this should clear stale
27936            // pending delete state for cursor 0.
27937            b.emit_op(Opcode::NewRowid, 0, 2, 0, P4::None, 0);
27938
27939            // At end of table, Next must report no successor.
27940            let has_next = b.emit_label();
27941            b.emit_jump_to_label(Opcode::Next, 0, 0, has_next, P4::None, 0);
27942            b.emit_op(Opcode::Integer, 0, 3, 0, P4::None, 0);
27943            b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27944            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27945
27946            // Unexpected path if stale pending state causes false positive.
27947            b.resolve_label(has_next);
27948            b.emit_op(Opcode::Integer, 1, 3, 0, P4::None, 0);
27949            b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27950            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27951
27952            b.resolve_label(done);
27953            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27954            b.resolve_label(end);
27955        });
27956
27957        assert_eq!(
27958            rows,
27959            vec![vec![SqliteValue::Integer(0)]],
27960            "Delete->NewRowid->Next should report no successor at end-of-table"
27961        );
27962    }
27963
27964    #[test]
27965    fn test_newrowid_with_storage_cursor_allocates_correctly() {
27966        // Verify NewRowid allocates sequential rowids when using storage cursors.
27967        let mut db = MemDatabase::new();
27968        let root = db.create_table(1);
27969        let table = db.get_table_mut(root).unwrap();
27970        table.insert(5, vec![SqliteValue::Integer(50)]);
27971
27972        let (rows, _) = run_write_with_storage_cursors(db, |b| {
27973            let end = b.emit_label();
27974            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27975
27976            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27977
27978            // Allocate two new rowids and output them.
27979            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
27980            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27981            b.emit_op(Opcode::NewRowid, 0, 2, 0, P4::None, 0);
27982            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27983
27984            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27985            b.resolve_label(end);
27986        });
27987
27988        // The table had rowid 5 → next_rowid should be 6, then 7.
27989        assert_eq!(rows.len(), 2);
27990        assert_eq!(rows[0][0], SqliteValue::Integer(6));
27991        assert_eq!(rows[1][0], SqliteValue::Integer(7));
27992    }
27993
27994    #[test]
27995    fn test_newrowid_concurrent_flag_uses_snapshot_independent_path() {
27996        // Phase 5B.2 (bd-1yi8): with storage cursors, NewRowid reads max
27997        // rowid from B-tree regardless of p3 (concurrent flag). The p3
27998        // flag only affects the MemDatabase fallback (Phase 4 cursors).
27999        fn setup_db_with_stale_counter() -> (MemDatabase, i32) {
28000            let mut db = MemDatabase::new();
28001            let root = db.create_table(1);
28002            let table = db.get_table_mut(root).expect("table should exist");
28003            table.insert(10, vec![SqliteValue::Integer(10)]);
28004            table.insert(11, vec![SqliteValue::Integer(11)]);
28005            // Simulate stale local counter state from an old snapshot.
28006            table.next_rowid = 1;
28007            (db, root)
28008        }
28009
28010        let (db_serialized, root) = setup_db_with_stale_counter();
28011        let (rows_serialized, _) = run_write_with_storage_cursors(db_serialized, |b| {
28012            let end = b.emit_label();
28013            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28014            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28015            // Serialized path (`p3 = 0`) — with storage cursors, reads
28016            // max rowid from B-tree (11), returns 12.
28017            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28018            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28019            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28020            b.resolve_label(end);
28021        });
28022
28023        let (db_concurrent, root) = setup_db_with_stale_counter();
28024        let (rows_concurrent, _) = run_write_with_storage_cursors(db_concurrent, |b| {
28025            let end = b.emit_label();
28026            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28027            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28028            // Concurrent path (`p3 != 0`) — same B-tree path, same result.
28029            b.emit_op(Opcode::NewRowid, 0, 1, 1, P4::None, 0);
28030            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28031            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28032            b.resolve_label(end);
28033        });
28034
28035        // Both paths read max rowid (11) from B-tree → return 12.
28036        assert_eq!(rows_serialized, vec![vec![SqliteValue::Integer(12)]]);
28037        assert_eq!(rows_concurrent, vec![vec![SqliteValue::Integer(12)]]);
28038    }
28039
28040    #[test]
28041    fn test_memdb_concurrent_rowid_uses_visible_max_not_stale_counter() {
28042        let mut db = MemDatabase::new();
28043        let root = db.create_table(1);
28044        let table = db.get_table_mut(root).expect("table should exist");
28045        table.insert(10, vec![SqliteValue::Integer(10)]);
28046        table.insert(11, vec![SqliteValue::Integer(11)]);
28047        assert!(table.delete_by_rowid(11), "max rowid should be removed");
28048        table.next_rowid = 1;
28049
28050        let rowid = db.alloc_rowid_concurrent(root);
28051        assert_eq!(rowid, 11, "concurrent rowid should reuse max_visible+1");
28052        assert_eq!(
28053            db.get_table(root)
28054                .expect("table should exist")
28055                .next_rowid_hint(),
28056            12,
28057            "next_rowid should advance from the visible max"
28058        );
28059    }
28060
28061    #[test]
28062    fn test_memdb_delete_rowid_records_undo() {
28063        let mut db = MemDatabase::new();
28064        let root = db.create_table(2);
28065        db.get_table_mut(root)
28066            .expect("table should exist")
28067            .add_unique_column_group(vec![0]);
28068        db.upsert_row(
28069            root,
28070            1,
28071            vec![SqliteValue::Integer(10), SqliteValue::Integer(100)],
28072        );
28073
28074        db.begin_undo();
28075        let token = db.undo_version();
28076        assert!(db.delete_rowid(root, 1), "delete should find the row");
28077        let table = db.get_table(root).expect("table should exist");
28078        assert_eq!(table.row_values_by_rowid(1), None);
28079        assert!(
28080            table
28081                .find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)])
28082                .is_empty(),
28083            "delete should remove the unique-index entry"
28084        );
28085
28086        db.rollback_to(token);
28087        let table = db.get_table(root).expect("table should exist");
28088        let restored = vec![SqliteValue::Integer(10), SqliteValue::Integer(100)];
28089        assert_eq!(table.row_values_by_rowid(1), Some(restored.as_slice()));
28090        assert_eq!(
28091            table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28092            vec![1],
28093            "rollback should restore the unique-index entry"
28094        );
28095    }
28096
28097    #[test]
28098    fn test_memdb_insert_auto_row_records_undo_and_patches_rowid_alias() {
28099        let mut db = MemDatabase::new();
28100        let root = db.create_table(2);
28101        db.upsert_row(
28102            root,
28103            5,
28104            vec![SqliteValue::Integer(5), SqliteValue::Text("old".into())],
28105        );
28106
28107        db.begin_undo();
28108        let token = db.undo_version();
28109        let rowid = db
28110            .insert_auto_row(
28111                root,
28112                vec![SqliteValue::Null, SqliteValue::Text("new".into())],
28113                Some(0),
28114            )
28115            .expect("table should exist");
28116        assert_eq!(rowid, 6);
28117        let table = db.get_table(root).expect("table should exist");
28118        assert_eq!(table.next_rowid_hint(), 7);
28119        assert_eq!(
28120            table.row_values_by_rowid(6),
28121            Some([SqliteValue::Integer(6), SqliteValue::Text("new".into())].as_slice())
28122        );
28123
28124        db.rollback_to(token);
28125        let table = db.get_table(root).expect("table should exist");
28126        assert_eq!(table.next_rowid_hint(), 6);
28127        assert_eq!(table.row_values_by_rowid(6), None);
28128        assert_eq!(
28129            table.row_values_by_rowid(5),
28130            Some([SqliteValue::Integer(5), SqliteValue::Text("old".into())].as_slice())
28131        );
28132    }
28133
28134    #[test]
28135    fn test_memdb_replace_secondary_unique_rollback_restores_deleted_and_replaced_rows() {
28136        let mut db = MemDatabase::new();
28137        let root = db.create_table(2);
28138        db.get_table_mut(root)
28139            .expect("table should exist")
28140            .add_unique_column_group(vec![1]);
28141        db.upsert_row(
28142            root,
28143            1,
28144            vec![SqliteValue::Integer(1), SqliteValue::Text("alpha".into())],
28145        );
28146        db.upsert_row(
28147            root,
28148            2,
28149            vec![SqliteValue::Integer(2), SqliteValue::Text("beta".into())],
28150        );
28151
28152        db.begin_undo();
28153        let token = db.undo_version();
28154        assert!(
28155            db.delete_rowid(root, 1),
28156            "REPLACE should delete the secondary-unique conflict"
28157        );
28158        db.upsert_row(
28159            root,
28160            2,
28161            vec![SqliteValue::Integer(2), SqliteValue::Text("alpha".into())],
28162        );
28163        let table = db.get_table(root).expect("table should exist");
28164        assert_eq!(table.row_values_by_rowid(1), None);
28165        assert_eq!(
28166            table.row_values_by_rowid(2),
28167            Some([SqliteValue::Integer(2), SqliteValue::Text("alpha".into())].as_slice())
28168        );
28169        assert_eq!(
28170            table.find_unique_conflicts(&[
28171                SqliteValue::Integer(99),
28172                SqliteValue::Text("alpha".into())
28173            ]),
28174            vec![2]
28175        );
28176
28177        db.rollback_to(token);
28178        let table = db.get_table(root).expect("table should exist");
28179        assert_eq!(table.next_rowid_hint(), 3);
28180        assert_eq!(
28181            table.row_values_by_rowid(1),
28182            Some([SqliteValue::Integer(1), SqliteValue::Text("alpha".into())].as_slice())
28183        );
28184        assert_eq!(
28185            table.row_values_by_rowid(2),
28186            Some([SqliteValue::Integer(2), SqliteValue::Text("beta".into())].as_slice())
28187        );
28188        assert_eq!(
28189            table.find_unique_conflicts(&[
28190                SqliteValue::Integer(99),
28191                SqliteValue::Text("alpha".into())
28192            ]),
28193            vec![1]
28194        );
28195        assert_eq!(
28196            table.find_unique_conflicts(&[
28197                SqliteValue::Integer(99),
28198                SqliteValue::Text("beta".into())
28199            ]),
28200            vec![2]
28201        );
28202    }
28203
28204    #[test]
28205    fn test_memtable_rowid_lookup_uses_dense_offset_without_breaking_sparse_rows() {
28206        let mut table = MemTable::new(1);
28207        table.insert(10, vec![SqliteValue::Integer(10)]);
28208        table.insert(11, vec![SqliteValue::Integer(11)]);
28209        table.insert(12, vec![SqliteValue::Integer(12)]);
28210
28211        assert_eq!(table.find_by_rowid(11), Some(1));
28212        assert_eq!(
28213            table.row_values_by_rowid(11),
28214            Some(&[SqliteValue::Integer(11)][..])
28215        );
28216
28217        assert!(table.delete_by_rowid(11), "middle row should be removed");
28218        assert_eq!(table.find_by_rowid(11), None);
28219        assert_eq!(
28220            table.row_values_by_rowid(12),
28221            Some(&[SqliteValue::Integer(12)][..]),
28222            "sparse tables must still fall back to binary search"
28223        );
28224    }
28225
28226    #[test]
28227    fn test_memtable_rowid_range_bounds_use_dense_offset_with_sparse_fallback() {
28228        let mut table = MemTable::new(1);
28229        for rowid in 10..15 {
28230            table.insert(rowid, vec![SqliteValue::Integer(rowid)]);
28231        }
28232
28233        assert_eq!(table.count_rowid_range(11, 14), 3);
28234        assert_eq!(table.count_rowid_range(i64::MIN, 12), 2);
28235        assert_eq!(table.count_rowid_range(13, i64::MAX), 2);
28236        let dense_values = table
28237            .iter_rows_in_rowid_range(11, 14)
28238            .map(|(rowid, values)| (rowid, values.first().cloned()))
28239            .collect::<Vec<_>>();
28240        assert_eq!(
28241            dense_values,
28242            vec![
28243                (11, Some(SqliteValue::Integer(11))),
28244                (12, Some(SqliteValue::Integer(12))),
28245                (13, Some(SqliteValue::Integer(13))),
28246            ]
28247        );
28248
28249        assert!(table.delete_by_rowid(12), "middle row should be removed");
28250        assert_eq!(table.count_rowid_range(11, 14), 2);
28251        assert_eq!(table.count_rowid_range(i64::MIN, 12), 2);
28252        assert_eq!(table.count_rowid_range(13, i64::MAX), 2);
28253        let sparse_values = table
28254            .iter_rows_in_rowid_range(11, 14)
28255            .map(|(rowid, values)| (rowid, values.first().cloned()))
28256            .collect::<Vec<_>>();
28257        assert_eq!(
28258            sparse_values,
28259            vec![
28260                (11, Some(SqliteValue::Integer(11))),
28261                (13, Some(SqliteValue::Integer(13))),
28262            ]
28263        );
28264    }
28265
28266    #[test]
28267    fn test_memtable_unique_conflict_index_tracks_insert_delete_and_update() {
28268        let mut table = MemTable::new(2);
28269        table.add_unique_column_group(vec![0]);
28270        table.insert(1, vec![SqliteValue::Integer(10), SqliteValue::Integer(100)]);
28271        table.insert(2, vec![SqliteValue::Integer(20), SqliteValue::Integer(200)]);
28272
28273        assert_eq!(
28274            table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28275            vec![1],
28276            "lookup should find the conflicting rowid via the unique index"
28277        );
28278
28279        assert!(table.delete_by_rowid(1), "delete should succeed");
28280        assert!(
28281            table
28282                .find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)])
28283                .is_empty(),
28284            "deleted rows must be removed from the unique index"
28285        );
28286
28287        table.insert(2, vec![SqliteValue::Integer(10), SqliteValue::Integer(200)]);
28288        assert_eq!(
28289            table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28290            vec![2],
28291            "updates must refresh the unique index entry"
28292        );
28293
28294        assert!(
28295            table
28296                .find_unique_conflicts(&[SqliteValue::Null, SqliteValue::Integer(999)])
28297                .is_empty(),
28298            "NULL keys must remain non-conflicting under SQLite UNIQUE semantics"
28299        );
28300    }
28301
28302    #[test]
28303    fn test_memtable_unique_conflict_index_honors_nocase_collation() {
28304        let mut table = MemTable::new(1);
28305        table.add_unique_column_group_with_collations(vec![0], vec![Some("NOCASE".to_owned())]);
28306        table.insert(1, vec![SqliteValue::Text("Alice".into())]);
28307
28308        assert_eq!(
28309            table.find_unique_conflicts(&[SqliteValue::Text("alice".into())]),
28310            vec![1],
28311            "builtin NOCASE unique indexes must normalize case-equivalent text"
28312        );
28313    }
28314
28315    #[test]
28316    fn test_engine_database_attachment_propagates_custom_collation_registry_for_memtable_uniques() {
28317        struct DashlessNoCaseCollation;
28318
28319        impl fsqlite_func::collation::CollationFunction for DashlessNoCaseCollation {
28320            fn name(&self) -> &str {
28321                "DASHLESS_NOCASE"
28322            }
28323
28324            fn compare(&self, left: &[u8], right: &[u8]) -> Ordering {
28325                let normalize = |bytes: &[u8]| {
28326                    bytes
28327                        .iter()
28328                        .filter(|&&byte| byte != b'-' && byte != b'_' && byte != b' ')
28329                        .map(u8::to_ascii_lowercase)
28330                        .collect::<Vec<_>>()
28331                };
28332                normalize(left).cmp(&normalize(right))
28333            }
28334        }
28335
28336        let registry = Arc::new(Mutex::new(CollationRegistry::new()));
28337        registry
28338            .lock()
28339            .unwrap_or_else(|err| err.into_inner())
28340            .register(DashlessNoCaseCollation);
28341
28342        let mut db = MemDatabase::new();
28343        let root = db.create_table(1);
28344        {
28345            let table = db.get_table_mut(root).expect("table should exist");
28346            table.add_unique_column_group_with_collations(
28347                vec![0],
28348                vec![Some("DASHLESS_NOCASE".to_owned())],
28349            );
28350            table.insert(1, vec![SqliteValue::Text("Alpha-Beta".into())]);
28351        }
28352
28353        let mut engine = VdbeEngine::new(1);
28354        engine.set_database(db);
28355        engine.set_collation_registry(Arc::clone(&registry));
28356
28357        let db = engine
28358            .take_database()
28359            .expect("database should still be attached");
28360        let table = db.get_table(root).expect("table should exist");
28361        assert_eq!(
28362            table.find_unique_conflicts(&[SqliteValue::Text("alpha beta".into())]),
28363            vec![1],
28364            "custom-collation UNIQUE fallback must use the engine-provided registry"
28365        );
28366    }
28367
28368    // ── bd-1yi8: INSERT write-through tests ────────────────────────────
28369
28370    #[test]
28371    fn test_insert_write_through_no_memdb_sync() {
28372        // Verify INSERT with storage cursor does NOT write to MemDatabase.
28373        let mut db = MemDatabase::new();
28374        let root = db.create_table(1);
28375
28376        let (_, final_db) = run_write_with_storage_cursors(db, |b| {
28377            let end = b.emit_label();
28378            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28379            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28380            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28381            b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
28382            b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28383            b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28384            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28385            b.resolve_label(end);
28386        });
28387
28388        let table = final_db.get_table(root).expect("table should exist");
28389        assert_eq!(table.rows.len(), 0, "write-through must skip MemDatabase");
28390    }
28391
28392    #[test]
28393    fn test_insert_write_through_marks_root_page_dirty_for_lazy_memdb_sync() {
28394        let mut db = MemDatabase::new();
28395        let root = db.create_table(1);
28396
28397        let mut b = ProgramBuilder::new();
28398        let end = b.emit_label();
28399        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28400        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28401        b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28402        b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
28403        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28404        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28405        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28406        b.resolve_label(end);
28407
28408        let prog = b.finish().expect("program should build");
28409        let mut engine = VdbeEngine::new(prog.register_count());
28410        engine.enable_storage_cursors(true);
28411        engine.set_database(db);
28412        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28413        engine.set_reject_mem_fallback(false);
28414
28415        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28416        assert_eq!(outcome, ExecOutcome::Done);
28417        assert!(engine.has_dirty_root_pages());
28418        assert!(engine.dirty_root_pages().contains(&root));
28419        assert_eq!(engine.dirty_root_pages().len(), 1);
28420        assert!(
28421            !engine.storage_cursor_memdb_count_shortcuts_safe(),
28422            "storage-backed INSERT should leave MemDB fast paths disabled until reload"
28423        );
28424
28425        let db = engine.take_database().expect("database should exist");
28426        let table = db.get_table(root).expect("table should exist");
28427        assert_eq!(
28428            table.rows.len(),
28429            0,
28430            "write-through must keep MemDatabase stale"
28431        );
28432    }
28433
28434    #[test]
28435    fn test_lazy_dirty_flag_set_on_insert() {
28436        let mut db = MemDatabase::new();
28437        let root = db.create_table(1);
28438
28439        let mut b = ProgramBuilder::new();
28440        let end = b.emit_label();
28441        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28442        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28443        b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28444        b.emit_op(Opcode::Integer, 77, 2, 0, P4::None, 0);
28445        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28446        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28447        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28448        b.resolve_label(end);
28449
28450        let prog = b.finish().expect("program should build");
28451        let mut engine = VdbeEngine::new(prog.register_count());
28452        engine.enable_storage_cursors(true);
28453        engine.set_database(db);
28454        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28455        engine.set_reject_mem_fallback(false);
28456
28457        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28458        assert_eq!(outcome, ExecOutcome::Done);
28459        assert!(engine.has_dirty_root_pages());
28460        assert!(engine.dirty_root_pages().contains(&root));
28461        assert_eq!(engine.dirty_root_pages().len(), 1);
28462        assert!(
28463            !engine.storage_cursor_memdb_count_shortcuts_safe(),
28464            "storage-backed INSERT must mark the MemDatabase mirror stale"
28465        );
28466
28467        let db = engine.take_database().expect("database should exist");
28468        let table = db.get_table(root).expect("table should exist");
28469        assert_eq!(
28470            table.rows.len(),
28471            0,
28472            "lazy dirty tracking must leave MemDatabase row mirroring deferred"
28473        );
28474    }
28475
28476    #[test]
28477    fn test_lazy_dirty_read_from_btree() {
28478        let mut db = MemDatabase::new();
28479        let root = db.create_table(1);
28480
28481        let mut b = ProgramBuilder::new();
28482        let end = b.emit_label();
28483        let done = b.emit_label();
28484        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28485        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28486        b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28487        b.emit_op(Opcode::Integer, 41, 2, 0, P4::None, 0);
28488        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28489        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28490        b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28491        let body = b.current_addr();
28492        b.emit_op(Opcode::Column, 0, 0, 4, P4::None, 0);
28493        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
28494        let next_target =
28495            i32::try_from(body).expect("program counter should fit into i32 for tests");
28496        b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28497        b.resolve_label(done);
28498        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28499        b.resolve_label(end);
28500
28501        let prog = b.finish().expect("program should build");
28502        let mut engine = VdbeEngine::new(prog.register_count());
28503        engine.enable_storage_cursors(true);
28504        engine.set_database(db);
28505        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28506        engine.set_reject_mem_fallback(false);
28507
28508        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28509        assert_eq!(outcome, ExecOutcome::Done);
28510        let rows: Vec<_> = engine
28511            .take_results()
28512            .into_iter()
28513            .map(|row| row.into_vec())
28514            .collect();
28515        assert_eq!(
28516            rows,
28517            vec![vec![SqliteValue::Integer(41)]],
28518            "same-statement reads must come from the storage-backed B-tree when the MemDatabase mirror is stale"
28519        );
28520        assert!(engine.has_dirty_root_pages());
28521        assert!(engine.dirty_root_pages().contains(&root));
28522
28523        let db = engine.take_database().expect("database should exist");
28524        let table = db.get_table(root).expect("table should exist");
28525        assert_eq!(
28526            table.rows.len(),
28527            0,
28528            "B-tree fallback reads must not eagerly repopulate the stale MemDatabase mirror"
28529        );
28530    }
28531
28532    #[test]
28533    fn test_lazy_dirty_clean_table_scan_then_dirty_table_fallback() {
28534        let mut db = MemDatabase::new();
28535        let dirty_root = db.create_table(1);
28536        let clean_root = db.create_table(1);
28537        db.get_table_mut(clean_root)
28538            .expect("clean table should exist")
28539            .insert(1, vec![SqliteValue::Integer(7)]);
28540
28541        let mut b = ProgramBuilder::new();
28542        let end = b.emit_label();
28543        let clean_done = b.emit_label();
28544        let dirty_done = b.emit_label();
28545        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28546        b.emit_op(Opcode::OpenWrite, 0, dirty_root, 0, P4::Int(1), 0);
28547        b.emit_op(Opcode::OpenRead, 1, clean_root, 0, P4::Int(1), 0);
28548
28549        b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28550        b.emit_op(Opcode::Integer, 41, 2, 0, P4::None, 0);
28551        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28552        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28553
28554        b.emit_jump_to_label(Opcode::Rewind, 1, 0, clean_done, P4::None, 0);
28555        let clean_body = b.current_addr();
28556        b.emit_op(Opcode::Column, 1, 0, 4, P4::None, 0);
28557        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
28558        let clean_next =
28559            i32::try_from(clean_body).expect("program counter should fit into i32 for tests");
28560        b.emit_op(Opcode::Next, 1, clean_next, 0, P4::None, 0);
28561        b.resolve_label(clean_done);
28562
28563        b.emit_jump_to_label(Opcode::Rewind, 0, 0, dirty_done, P4::None, 0);
28564        let dirty_body = b.current_addr();
28565        b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
28566        b.emit_op(Opcode::ResultRow, 5, 1, 0, P4::None, 0);
28567        let dirty_next =
28568            i32::try_from(dirty_body).expect("program counter should fit into i32 for tests");
28569        b.emit_op(Opcode::Next, 0, dirty_next, 0, P4::None, 0);
28570        b.resolve_label(dirty_done);
28571
28572        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28573        b.resolve_label(end);
28574
28575        let prog = b.finish().expect("program should build");
28576        let mut engine = VdbeEngine::new(prog.register_count());
28577        engine.enable_storage_cursors(true);
28578        engine.set_database(db);
28579        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28580        engine.set_reject_mem_fallback(false);
28581
28582        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28583        assert_eq!(outcome, ExecOutcome::Done);
28584        let rows: Vec<_> = engine
28585            .take_results()
28586            .into_iter()
28587            .map(|row| row.into_vec())
28588            .collect();
28589        assert_eq!(
28590            rows,
28591            vec![
28592                vec![SqliteValue::Integer(7)],
28593                vec![SqliteValue::Integer(41)],
28594            ],
28595            "same-statement reads should preserve clean-table visibility while still sourcing dirty-table rows from the storage-backed B-tree"
28596        );
28597        assert!(engine.has_dirty_root_pages());
28598        assert!(engine.dirty_root_pages().contains(&dirty_root));
28599        assert!(
28600            !engine.dirty_root_pages().contains(&clean_root),
28601            "reading a clean table must not mark it dirty"
28602        );
28603        assert_eq!(
28604            engine.dirty_root_pages().len(),
28605            1,
28606            "only the written table should remain dirty"
28607        );
28608
28609        let db = engine.take_database().expect("database should exist");
28610        assert_eq!(
28611            db.get_table(dirty_root)
28612                .expect("dirty table should exist")
28613                .rows
28614                .len(),
28615            0,
28616            "lazy dirty tracking must keep the written table stale in the MemDatabase mirror"
28617        );
28618        assert_eq!(
28619            db.get_table(clean_root)
28620                .expect("clean table should exist")
28621                .rows
28622                .len(),
28623            1,
28624            "clean-table reads must leave the untouched MemDatabase mirror intact"
28625        );
28626    }
28627
28628    #[test]
28629    fn test_lazy_dirty_multiple_tables() {
28630        let mut db = MemDatabase::new();
28631        let root_a = db.create_table(1);
28632        let root_b = db.create_table(1);
28633
28634        let mut b = ProgramBuilder::new();
28635        let end = b.emit_label();
28636        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28637        b.emit_op(Opcode::OpenWrite, 0, root_a, 0, P4::Int(1), 0);
28638        b.emit_op(Opcode::OpenWrite, 1, root_b, 0, P4::Int(1), 0);
28639
28640        b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28641        b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28642        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28643        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28644
28645        b.emit_op(Opcode::NewRowid, 1, 4, 0, P4::None, 0);
28646        b.emit_op(Opcode::Integer, 20, 5, 0, P4::None, 0);
28647        b.emit_op(Opcode::MakeRecord, 5, 1, 6, P4::None, 0);
28648        b.emit_op(Opcode::Insert, 1, 6, 4, P4::None, 0);
28649
28650        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28651        b.resolve_label(end);
28652
28653        let prog = b.finish().expect("program should build");
28654        let mut engine = VdbeEngine::new(prog.register_count());
28655        engine.enable_storage_cursors(true);
28656        engine.set_database(db);
28657        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28658        engine.set_reject_mem_fallback(false);
28659
28660        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28661        assert_eq!(outcome, ExecOutcome::Done);
28662        assert!(engine.has_dirty_root_pages());
28663        assert!(engine.dirty_root_pages().contains(&root_a));
28664        assert!(engine.dirty_root_pages().contains(&root_b));
28665        assert_eq!(
28666            engine.dirty_root_pages().len(),
28667            2,
28668            "lazy dirty tracking must record every dirty table root separately"
28669        );
28670
28671        let db = engine.take_database().expect("database should exist");
28672        assert_eq!(
28673            db.get_table(root_a)
28674                .expect("table A should exist")
28675                .rows
28676                .len(),
28677            0,
28678            "table A should remain stale in MemDatabase until a later reload"
28679        );
28680        assert_eq!(
28681            db.get_table(root_b)
28682                .expect("table B should exist")
28683                .rows
28684                .len(),
28685            0,
28686            "table B should remain stale in MemDatabase until a later reload"
28687        );
28688    }
28689
28690    #[test]
28691    fn test_lazy_dirty_update_after_insert() {
28692        let mut db = MemDatabase::new();
28693        let root = db.create_table(1);
28694
28695        let mut b = ProgramBuilder::new();
28696        let end = b.emit_label();
28697        let done = b.emit_label();
28698        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28699        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28700
28701        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28702        b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28703        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28704        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28705
28706        b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
28707        b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
28708        b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 5);
28709
28710        b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28711        let body = b.current_addr();
28712        b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
28713        b.emit_op(Opcode::ResultRow, 6, 1, 0, P4::None, 0);
28714        let next_target =
28715            i32::try_from(body).expect("program counter should fit into i32 for tests");
28716        b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28717        b.resolve_label(done);
28718        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28719        b.resolve_label(end);
28720
28721        let prog = b.finish().expect("program should build");
28722        let mut engine = VdbeEngine::new(prog.register_count());
28723        engine.enable_storage_cursors(true);
28724        engine.set_database(db);
28725        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28726        engine.set_reject_mem_fallback(false);
28727
28728        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28729        assert_eq!(outcome, ExecOutcome::Done);
28730        let rows: Vec<_> = engine
28731            .take_results()
28732            .into_iter()
28733            .map(|row| row.into_vec())
28734            .collect();
28735        assert_eq!(
28736            rows,
28737            vec![vec![SqliteValue::Integer(99)]],
28738            "a later storage-backed REPLACE must be visible through the B-tree fallback even while the MemDatabase mirror remains stale"
28739        );
28740        assert!(engine.has_dirty_root_pages());
28741        assert!(engine.dirty_root_pages().contains(&root));
28742        assert_eq!(engine.dirty_root_pages().len(), 1);
28743
28744        let db = engine.take_database().expect("database should exist");
28745        let table = db.get_table(root).expect("table should exist");
28746        assert_eq!(
28747            table.rows.len(),
28748            0,
28749            "lazy dirty tracking must not eagerly maintain the MemDatabase row mirror across update-like writes"
28750        );
28751    }
28752
28753    #[test]
28754    fn test_lazy_dirty_delete_after_insert() {
28755        let mut db = MemDatabase::new();
28756        let root = db.create_table(1);
28757
28758        let mut b = ProgramBuilder::new();
28759        let end = b.emit_label();
28760        let after_delete = b.emit_label();
28761        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28762        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28763
28764        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28765        b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28766        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28767        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28768
28769        b.emit_op(Opcode::Integer, 1, 4, 0, P4::None, 0);
28770        b.emit_jump_to_label(Opcode::SeekRowid, 0, 4, after_delete, P4::None, 0);
28771        b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
28772        b.resolve_label(after_delete);
28773
28774        b.emit_op(Opcode::Count, 0, 5, 0, P4::None, 0);
28775        b.emit_op(Opcode::ResultRow, 5, 1, 0, P4::None, 0);
28776        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28777        b.resolve_label(end);
28778
28779        let prog = b.finish().expect("program should build");
28780        let mut engine = VdbeEngine::new(prog.register_count());
28781        engine.enable_storage_cursors(true);
28782        engine.set_database(db);
28783        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28784        engine.set_reject_mem_fallback(false);
28785
28786        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28787        assert_eq!(outcome, ExecOutcome::Done);
28788        let rows: Vec<_> = engine
28789            .take_results()
28790            .into_iter()
28791            .map(|row| row.into_vec())
28792            .collect();
28793        assert_eq!(
28794            rows,
28795            vec![vec![SqliteValue::Integer(0)]],
28796            "storage-backed COUNT must observe the delete even while MemDatabase mirroring is deferred"
28797        );
28798        assert!(engine.has_dirty_root_pages());
28799        assert!(engine.dirty_root_pages().contains(&root));
28800        assert_eq!(engine.dirty_root_pages().len(), 1);
28801
28802        let db = engine.take_database().expect("database should exist");
28803        let table = db.get_table(root).expect("table should exist");
28804        assert_eq!(
28805            table.rows.len(),
28806            0,
28807            "lazy dirty tracking must leave the MemDatabase empty until a later bulk reload"
28808        );
28809    }
28810
28811    #[test]
28812    fn test_insert_new_rowid_from_btree() {
28813        // Verify NewRowid reads max from B-tree, not MemDatabase counter.
28814        let mut db = MemDatabase::new();
28815        let root = db.create_table(1);
28816        let table = db.get_table_mut(root).unwrap();
28817        // Insert rows 1..=3 into MemTable (these get copied to B-tree at
28818        // cursor open time via MemPageStore fallback).
28819        table.insert(1, vec![SqliteValue::Integer(10)]);
28820        table.insert(2, vec![SqliteValue::Integer(20)]);
28821        table.insert(3, vec![SqliteValue::Integer(30)]);
28822        // Reset counter to simulate stale state.
28823        table.next_rowid = 1;
28824
28825        let (rows, _) = run_write_with_storage_cursors(db, |b| {
28826            let end = b.emit_label();
28827            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28828            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28829            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28830            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28831            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28832            b.resolve_label(end);
28833        });
28834
28835        // B-tree max rowid is 3 → should return 4, NOT 1.
28836        assert_eq!(rows, vec![vec![SqliteValue::Integer(4)]]);
28837    }
28838
28839    #[test]
28840    fn test_insert_multiple_rows_write_through() {
28841        // Insert multiple rows via B-tree and read them all back.
28842        let mut db = MemDatabase::new();
28843        let root = db.create_table(1);
28844
28845        let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
28846            let end = b.emit_label();
28847            let done = b.emit_label();
28848            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28849            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28850
28851            // Insert row 1: value=100
28852            b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28853            b.emit_op(Opcode::Integer, 100, 2, 0, P4::None, 0);
28854            b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28855            b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28856
28857            // Insert row 2: value=200
28858            b.emit_op(Opcode::NewRowid, 0, 4, 0, P4::None, 0);
28859            b.emit_op(Opcode::Integer, 200, 5, 0, P4::None, 0);
28860            b.emit_op(Opcode::MakeRecord, 5, 1, 6, P4::None, 0);
28861            b.emit_op(Opcode::Insert, 0, 6, 4, P4::None, 0);
28862
28863            // Insert row 3: value=300
28864            b.emit_op(Opcode::NewRowid, 0, 7, 0, P4::None, 0);
28865            b.emit_op(Opcode::Integer, 300, 8, 0, P4::None, 0);
28866            b.emit_op(Opcode::MakeRecord, 8, 1, 9, P4::None, 0);
28867            b.emit_op(Opcode::Insert, 0, 9, 7, P4::None, 0);
28868
28869            // Read back via Rewind/Column/Next loop.
28870            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28871            let body = b.current_addr();
28872            b.emit_op(Opcode::Column, 0, 0, 10, P4::None, 0);
28873            b.emit_op(Opcode::ResultRow, 10, 1, 0, P4::None, 0);
28874            let next_target =
28875                i32::try_from(body).expect("program counter should fit into i32 for tests");
28876            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28877
28878            b.resolve_label(done);
28879            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28880            b.resolve_label(end);
28881        });
28882
28883        // MemDatabase should be empty (write-through).
28884        let table = final_db.get_table(root).expect("table should exist");
28885        assert_eq!(table.rows.len(), 0);
28886
28887        // All 3 rows readable from B-tree.
28888        assert_eq!(rows.len(), 3);
28889        assert_eq!(rows[0][0], SqliteValue::Integer(100));
28890        assert_eq!(rows[1][0], SqliteValue::Integer(200));
28891        assert_eq!(rows[2][0], SqliteValue::Integer(300));
28892    }
28893
28894    #[test]
28895    fn test_insert_replace_upsert_via_btree() {
28896        // Insert same rowid twice with OE_REPLACE — second insert should overwrite.
28897        let mut db = MemDatabase::new();
28898        let root = db.create_table(1);
28899
28900        let (rows, _) = run_write_with_storage_cursors(db, |b| {
28901            let end = b.emit_label();
28902            let done = b.emit_label();
28903            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28904            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28905
28906            // Insert rowid=1 with value=10.
28907            b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28908            b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28909            b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28910            b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28911
28912            // Insert rowid=1 again with value=99 (OE_REPLACE upsert).
28913            b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
28914            b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
28915            b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 5);
28916
28917            // Read back.
28918            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28919            let body = b.current_addr();
28920            b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
28921            b.emit_op(Opcode::ResultRow, 6, 1, 0, P4::None, 0);
28922            let next_target =
28923                i32::try_from(body).expect("program counter should fit into i32 for tests");
28924            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28925
28926            b.resolve_label(done);
28927            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28928            b.resolve_label(end);
28929        });
28930
28931        // Only one row with the updated value.
28932        assert_eq!(rows.len(), 1);
28933        assert_eq!(rows[0][0], SqliteValue::Integer(99));
28934    }
28935
28936    #[test]
28937    fn test_native_replace_row_deletes_conflict_victim_from_memdb_row_mirror() {
28938        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
28939
28940        let pager = MemoryMockMvccPager;
28941        let cx = Cx::new();
28942        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
28943
28944        let mut db = MemDatabase::new();
28945        let table_root = db.create_table(1);
28946        let index_root = 256;
28947        let table = db
28948            .get_table_mut(table_root)
28949            .expect("table mirror should exist");
28950        table.insert(1, vec![SqliteValue::Text("alice@example.com".into())]);
28951        table.insert(2, vec![SqliteValue::Text("bob@example.com".into())]);
28952        table.insert(3, vec![SqliteValue::Text("alice@example.com".into())]);
28953
28954        let mut engine = VdbeEngine::new(8);
28955        engine.set_database(db);
28956        engine.set_memdb_rows_loaded(true);
28957        engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28958        engine.set_transaction(txn);
28959        engine.table_index_meta = Arc::new(HashMap::from([(
28960            0,
28961            vec![IndexCursorMeta {
28962                cursor_id: 1,
28963                column_indices: vec![0],
28964            }]
28965            .into_boxed_slice(),
28966        )]));
28967
28968        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
28969        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
28970        engine.cursor_root_pages.insert(0, table_root);
28971        engine.cursor_root_pages.insert(1, index_root);
28972
28973        let row1 = encode_record(&[SqliteValue::Text("alice@example.com".into())]);
28974        let row2 = encode_record(&[SqliteValue::Text("bob@example.com".into())]);
28975        let row3 = encode_record(&[SqliteValue::Text("alice@example.com".into())]);
28976        let idx1 = encode_record(&[
28977            SqliteValue::Text("alice@example.com".into()),
28978            SqliteValue::Integer(1),
28979        ]);
28980        let idx2 = encode_record(&[
28981            SqliteValue::Text("bob@example.com".into()),
28982            SqliteValue::Integer(2),
28983        ]);
28984
28985        {
28986            let table_cursor = engine
28987                .storage_cursors
28988                .get_mut(&0)
28989                .expect("table storage cursor should exist");
28990            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
28991            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
28992            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 3, &row3)).unwrap();
28993        }
28994        {
28995            let index_cursor = engine
28996                .storage_cursors
28997                .get_mut(&1)
28998                .expect("index storage cursor should exist");
28999            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29000            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29001        }
29002
29003        run_async(engine.native_replace_row(0, 1)).unwrap();
29004
29005        let mirrored_table = engine
29006            .db
29007            .as_ref()
29008            .and_then(|db| db.get_table(table_root))
29009            .expect("mirrored table should still exist");
29010        let mirrored_rowids: Vec<i64> = mirrored_table.rows.iter().map(|row| row.rowid).collect();
29011        assert_eq!(
29012            mirrored_rowids,
29013            vec![1, 2, 3],
29014            "lazy dirty tracking should leave the MemDB mirror stale until the connection bulk-syncs it"
29015        );
29016        assert!(engine.has_dirty_root_pages());
29017        assert!(engine.dirty_root_pages().contains(&table_root));
29018        assert!(
29019            !engine.storage_cursor_memdb_count_shortcuts_safe(),
29020            "lazy delete mirroring must disable MemDB fast paths until reload"
29021        );
29022
29023        let index_cursor = engine
29024            .storage_cursors
29025            .get_mut(&1)
29026            .expect("index storage cursor should still exist");
29027        assert!(
29028            !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29029                .unwrap()
29030                .is_found(),
29031            "conflicting unique index entry should be removed from storage"
29032        );
29033        assert!(
29034            run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29035                .unwrap()
29036                .is_found(),
29037            "non-conflicting index entries must remain intact"
29038        );
29039    }
29040
29041    #[test]
29042    fn test_native_replace_row_scans_expression_index_for_victim_rowid() {
29043        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29044
29045        let pager = MemoryMockMvccPager;
29046        let cx = Cx::new();
29047        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29048        let mut db = MemDatabase::new();
29049        let table_root = db.create_table(2);
29050        let index_root = 256;
29051
29052        let mut engine = VdbeEngine::new(8);
29053        engine.set_database(db);
29054        engine.set_transaction(txn);
29055        engine.table_index_meta = Arc::new(HashMap::from([(
29056            0,
29057            vec![IndexCursorMeta {
29058                cursor_id: 1,
29059                column_indices: Vec::new(),
29060            }]
29061            .into_boxed_slice(),
29062        )]));
29063
29064        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29065        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29066        engine.cursor_root_pages.insert(0, table_root);
29067        engine.cursor_root_pages.insert(1, index_root);
29068
29069        let victim_row =
29070            encode_record(&[SqliteValue::Text("VICTIM".into()), SqliteValue::Integer(1)]);
29071        let keep_row = encode_record(&[SqliteValue::Text("KEEP".into()), SqliteValue::Integer(2)]);
29072        let victim_index =
29073            encode_record(&[SqliteValue::Text("victim".into()), SqliteValue::Integer(1)]);
29074        let keep_index =
29075            encode_record(&[SqliteValue::Text("keep".into()), SqliteValue::Integer(2)]);
29076
29077        {
29078            let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
29079            run_async(
29080                table_cursor
29081                    .cursor
29082                    .table_insert(&table_cursor.cx, 1, &victim_row),
29083            )
29084            .unwrap();
29085            run_async(
29086                table_cursor
29087                    .cursor
29088                    .table_insert(&table_cursor.cx, 2, &keep_row),
29089            )
29090            .unwrap();
29091        }
29092        {
29093            let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
29094            run_async(
29095                index_cursor
29096                    .cursor
29097                    .index_insert(&index_cursor.cx, &victim_index),
29098            )
29099            .unwrap();
29100            run_async(
29101                index_cursor
29102                    .cursor
29103                    .index_insert(&index_cursor.cx, &keep_index),
29104            )
29105            .unwrap();
29106        }
29107
29108        run_async(engine.native_replace_row(0, 1)).unwrap();
29109
29110        let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
29111        assert!(
29112            !run_async(
29113                index_cursor
29114                    .cursor
29115                    .index_move_to(&index_cursor.cx, &victim_index)
29116            )
29117            .unwrap()
29118            .is_found(),
29119            "expression-index cleanup must remove the victim's rowid-suffixed key"
29120        );
29121        assert!(
29122            run_async(
29123                index_cursor
29124                    .cursor
29125                    .index_move_to(&index_cursor.cx, &keep_index)
29126            )
29127            .unwrap()
29128            .is_found(),
29129            "rowid-suffix scanning must preserve other expression-index entries"
29130        );
29131    }
29132
29133    #[test]
29134    fn test_native_replace_row_deletes_index_entry_on_rowid_alias_column() {
29135        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29136
29137        let pager = MemoryMockMvccPager;
29138        let cx = Cx::new();
29139        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29140
29141        let mut db = MemDatabase::new();
29142        let table_root = db.create_table(2);
29143        let index_root = 256;
29144
29145        let mut engine = VdbeEngine::new(8);
29146        engine.set_database(db);
29147        engine.set_transaction(txn);
29148        engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29149        engine.table_index_meta = Arc::new(HashMap::from([(
29150            0,
29151            vec![IndexCursorMeta {
29152                cursor_id: 1,
29153                column_indices: vec![0],
29154            }]
29155            .into_boxed_slice(),
29156        )]));
29157
29158        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29159        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29160        engine.cursor_root_pages.insert(0, table_root);
29161        engine.cursor_root_pages.insert(1, index_root);
29162
29163        let row1 = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29164        let row2 = encode_record(&[SqliteValue::Null, SqliteValue::Text("keep".into())]);
29165        let idx1 = encode_record(&[SqliteValue::Integer(1), SqliteValue::Integer(1)]);
29166        let idx2 = encode_record(&[SqliteValue::Integer(2), SqliteValue::Integer(2)]);
29167
29168        {
29169            let table_cursor = engine
29170                .storage_cursors
29171                .get_mut(&0)
29172                .expect("table storage cursor should exist");
29173            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
29174            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
29175        }
29176        {
29177            let index_cursor = engine
29178                .storage_cursors
29179                .get_mut(&1)
29180                .expect("index storage cursor should exist");
29181            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29182            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29183        }
29184
29185        run_async(engine.native_replace_row(0, 1)).unwrap();
29186
29187        let index_cursor = engine
29188            .storage_cursors
29189            .get_mut(&1)
29190            .expect("index storage cursor should still exist");
29191        assert!(
29192            !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29193                .unwrap()
29194                .is_found(),
29195            "REPLACE cleanup must use the logical rowid alias, not the raw NULL payload slot"
29196        );
29197        assert!(
29198            run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29199                .unwrap()
29200                .is_found(),
29201            "non-conflicting rowid-alias index entries must remain intact"
29202        );
29203    }
29204
29205    #[test]
29206    fn test_native_replace_row_deletes_index_entry_after_multi_alter_short_rowid_alias_payload() {
29207        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29208
29209        let pager = MemoryMockMvccPager;
29210        let cx = Cx::new();
29211        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29212
29213        let mut db = MemDatabase::new();
29214        let table_root = db.create_table(4);
29215        let index_root = 256;
29216
29217        let mut engine = VdbeEngine::new(8);
29218        engine.set_database(db);
29219        engine.set_transaction(txn);
29220        engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29221        engine.set_table_column_count_by_root_page(HashMap::from([(table_root, 4)]));
29222        engine.table_index_meta = Arc::new(HashMap::from([(
29223            0,
29224            vec![IndexCursorMeta {
29225                cursor_id: 1,
29226                column_indices: vec![1],
29227            }]
29228            .into_boxed_slice(),
29229        )]));
29230
29231        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29232        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29233        engine.cursor_root_pages.insert(0, table_root);
29234        engine.cursor_root_pages.insert(1, index_root);
29235
29236        let row1 = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29237        let row2 = encode_record(&[SqliteValue::Null, SqliteValue::Text("keep".into())]);
29238        let idx1 = encode_record(&[SqliteValue::Text("old".into()), SqliteValue::Integer(1)]);
29239        let idx2 = encode_record(&[SqliteValue::Text("keep".into()), SqliteValue::Integer(2)]);
29240
29241        {
29242            let table_cursor = engine
29243                .storage_cursors
29244                .get_mut(&0)
29245                .expect("table storage cursor should exist");
29246            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
29247            run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
29248        }
29249        {
29250            let index_cursor = engine
29251                .storage_cursors
29252                .get_mut(&1)
29253                .expect("index storage cursor should exist");
29254            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29255            run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29256        }
29257
29258        run_async(engine.native_replace_row(0, 1)).unwrap();
29259
29260        let index_cursor = engine
29261            .storage_cursors
29262            .get_mut(&1)
29263            .expect("index storage cursor should still exist");
29264        assert!(
29265            !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29266                .unwrap()
29267                .is_found(),
29268            "REPLACE cleanup must not shift columns left when an old row keeps the NULL IPK placeholder"
29269        );
29270        assert!(
29271            run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29272                .unwrap()
29273                .is_found(),
29274            "non-conflicting short rowid-alias payload index entries must remain intact"
29275        );
29276    }
29277
29278    #[test]
29279    fn test_update_conflict_restore_reinserts_index_entry_on_rowid_alias_column() {
29280        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29281
29282        let pager = MemoryMockMvccPager;
29283        let cx = Cx::new();
29284        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29285
29286        let mut db = MemDatabase::new();
29287        let table_root = db.create_table(2);
29288        let index_root = 256;
29289
29290        let mut engine = VdbeEngine::new(8);
29291        engine.set_database(db);
29292        engine.set_transaction(txn);
29293        engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29294        engine.table_index_meta = Arc::new(HashMap::from([(
29295            0,
29296            vec![IndexCursorMeta {
29297                cursor_id: 1,
29298                column_indices: vec![0],
29299            }]
29300            .into_boxed_slice(),
29301        )]));
29302
29303        assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29304        assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29305        engine.cursor_root_pages.insert(0, table_root);
29306        engine.cursor_root_pages.insert(1, index_root);
29307
29308        let restored_row = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29309        let logical_idx = encode_record(&[SqliteValue::Integer(1), SqliteValue::Integer(1)]);
29310        let raw_payload_idx = encode_record(&[SqliteValue::Null, SqliteValue::Integer(1)]);
29311
29312        run_async(
29313            engine.restore_pending_update_after_conflict(PendingUpdateRestore::Storage {
29314                cursor_id: 0,
29315                rowid: 1,
29316                payload: restored_row,
29317            }),
29318        )
29319        .unwrap();
29320
29321        let table_cursor = engine
29322            .storage_cursors
29323            .get_mut(&0)
29324            .expect("table storage cursor should still exist");
29325        assert!(
29326            run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 1))
29327                .unwrap()
29328                .is_found(),
29329            "UPDATE conflict rollback should restore the original table row"
29330        );
29331
29332        let index_cursor = engine
29333            .storage_cursors
29334            .get_mut(&1)
29335            .expect("index storage cursor should still exist");
29336        assert!(
29337            run_async(
29338                index_cursor
29339                    .cursor
29340                    .index_move_to(&index_cursor.cx, &logical_idx)
29341            )
29342            .unwrap()
29343            .is_found(),
29344            "UPDATE conflict restore must use the logical rowid alias in index keys"
29345        );
29346        assert!(
29347            !run_async(
29348                index_cursor
29349                    .cursor
29350                    .index_move_to(&index_cursor.cx, &raw_payload_idx)
29351            )
29352            .unwrap()
29353            .is_found(),
29354            "UPDATE conflict restore must not index the raw NULL rowid-alias payload slot"
29355        );
29356    }
29357
29358    #[test]
29359    fn test_insert_after_missing_probe_disturbed_by_reposition_stays_sorted() {
29360        let mut db = MemDatabase::new();
29361        let root = db.create_table(1);
29362        let table = db.get_table_mut(root).unwrap();
29363        table.insert(10, vec![SqliteValue::Integer(100)]);
29364        table.insert(30, vec![SqliteValue::Integer(300)]);
29365
29366        let (rows, _) = run_write_with_storage_cursors(db, |b| {
29367            let end = b.emit_label();
29368            let done = b.emit_label();
29369            let probe_missing = b.emit_label();
29370            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29371            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29372
29373            // Probe the missing rowid=20, which leaves the cursor at its
29374            // successor/EOF insertion context.
29375            b.emit_op(Opcode::Integer, 20, 1, 0, P4::None, 0);
29376            b.emit_jump_to_label(Opcode::NotExists, 0, 1, probe_missing, P4::None, 0);
29377            b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
29378            b.resolve_label(probe_missing);
29379
29380            // Disturb the cursor by repositioning it to rowid=10 before the insert.
29381            b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
29382            b.emit_jump_to_label(Opcode::SeekRowid, 0, 2, done, P4::None, 0);
29383
29384            // Insert the previously probed rowid=20 with payload=200.
29385            b.emit_op(Opcode::Integer, 200, 3, 0, P4::None, 0);
29386            b.emit_op(Opcode::MakeRecord, 3, 1, 4, P4::None, 0);
29387            b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
29388
29389            b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
29390            let body = b.current_addr();
29391            b.emit_op(Opcode::Rowid, 0, 5, 0, P4::None, 0);
29392            b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
29393            b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
29394            let next_target =
29395                i32::try_from(body).expect("program counter should fit into i32 for tests");
29396            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
29397
29398            b.resolve_label(done);
29399            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29400            b.resolve_label(end);
29401        });
29402
29403        assert_eq!(
29404            rows,
29405            vec![
29406                vec![SqliteValue::Integer(10), SqliteValue::Integer(100)],
29407                vec![SqliteValue::Integer(20), SqliteValue::Integer(200)],
29408                vec![SqliteValue::Integer(30), SqliteValue::Integer(300)],
29409            ]
29410        );
29411    }
29412
29413    #[test]
29414    fn test_insert_default_conflict_errors_via_btree() {
29415        let _guard = VDBE_OBSERVABILITY_LOCK
29416            .lock()
29417            .unwrap_or_else(|e| e.into_inner());
29418        reset_vdbe_test_sideband_materialization_count();
29419
29420        // Default conflict mode (OE_ABORT) must raise constraint error.
29421        let mut db = MemDatabase::new();
29422        let root = db.create_table(1);
29423        let table = db.get_table_mut(root).expect("table should exist");
29424        table.insert(1, vec![SqliteValue::Integer(10)]);
29425
29426        let mut b = ProgramBuilder::new();
29427        let end = b.emit_label();
29428        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29429        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29430
29431        // Duplicate rowid=1 with default conflict handling.
29432        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
29433        b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
29434        b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
29435        b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 0);
29436
29437        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29438        b.resolve_label(end);
29439
29440        let prog = b.finish().expect("program should build");
29441        let expected_capacity = estimate_make_record_buffer_capacity(&prog, PageSize::DEFAULT);
29442        let mut engine = VdbeEngine::new(prog.register_count());
29443        engine.enable_storage_cursors(true);
29444        engine.set_database(db);
29445        engine.set_reject_mem_fallback(false);
29446
29447        let before = vdbe_test_sideband_materialization_count_snapshot();
29448        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
29449        let after = vdbe_test_sideband_materialization_count_snapshot();
29450        assert_eq!(
29451            outcome,
29452            ExecOutcome::Error {
29453                code: ErrorCode::Constraint as i32,
29454                message: "PRIMARY KEY constraint failed".to_owned(),
29455            }
29456        );
29457        assert_eq!(
29458            after - before,
29459            0,
29460            "duplicate Insert should consume MakeRecord sideband bytes without materializing"
29461        );
29462        assert!(
29463            engine.make_record_lookaside.is_empty(),
29464            "duplicate Insert should return a cleared scratch buffer"
29465        );
29466        assert!(
29467            engine.make_record_lookaside.capacity() >= expected_capacity,
29468            "duplicate Insert should keep the reusable sideband allocation"
29469        );
29470
29471        let db = engine.take_database().expect("database should exist");
29472        let table = db.get_table(root).expect("table should exist");
29473        assert_eq!(table.rows.len(), 1);
29474        assert_eq!(table.rows[0].values.as_slice(), &[SqliteValue::Integer(10)]);
29475    }
29476
29477    #[test]
29478    fn test_insert_default_conflict_errors_memdb_path() {
29479        // Same behavior must hold for the legacy MemDatabase cursor path.
29480        let mut db = MemDatabase::new();
29481        let root = db.create_table(1);
29482        let table = db.get_table_mut(root).expect("table should exist");
29483        table.insert(1, vec![SqliteValue::Integer(10)]);
29484
29485        let mut b = ProgramBuilder::new();
29486        let end = b.emit_label();
29487        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29488        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29489
29490        // Duplicate rowid=1 with default conflict handling.
29491        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
29492        b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
29493        b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
29494        b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 0);
29495
29496        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29497        b.resolve_label(end);
29498
29499        let prog = b.finish().expect("program should build");
29500        let mut engine = VdbeEngine::new(prog.register_count());
29501        engine.enable_storage_cursors(false);
29502        engine.set_database(db);
29503        engine.set_reject_mem_fallback(false);
29504
29505        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
29506        assert_eq!(
29507            outcome,
29508            ExecOutcome::Error {
29509                code: ErrorCode::Constraint as i32,
29510                message: "PRIMARY KEY constraint failed".to_owned(),
29511            }
29512        );
29513
29514        let db = engine.take_database().expect("database should exist");
29515        let table = db.get_table(root).expect("table should exist");
29516        assert_eq!(table.rows.len(), 1);
29517        assert_eq!(table.rows[0].values.as_slice(), &[SqliteValue::Integer(10)]);
29518    }
29519
29520    // ── bd-2a3y: TransactionPageIo / SharedTxnPageIo integration tests ──
29521
29522    #[test]
29523    fn test_set_transaction_enables_storage_cursors() {
29524        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29525
29526        let pager = MockMvccPager;
29527        let cx = Cx::new();
29528        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29529
29530        let mut engine = VdbeEngine::new(8);
29531        assert!(engine.storage_cursors_enabled);
29532
29533        // set_transaction should auto-enable storage cursors.
29534        engine.set_transaction(txn);
29535        assert!(engine.storage_cursors_enabled);
29536        assert!(engine.txn_page_io.is_some());
29537    }
29538
29539    #[test]
29540    fn test_storage_cursors_enabled_by_default() {
29541        let engine = VdbeEngine::new(8);
29542        assert!(engine.storage_cursors_enabled);
29543        assert!(engine.txn_page_io.is_none());
29544    }
29545
29546    #[test]
29547    fn test_take_transaction_returns_handle() {
29548        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29549
29550        let pager = MockMvccPager;
29551        let cx = Cx::new();
29552        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29553
29554        let mut engine = VdbeEngine::new(8);
29555        engine.set_transaction(txn);
29556
29557        // take_transaction should return the handle and clear cursors.
29558        let recovered = engine
29559            .take_transaction()
29560            .expect("take_transaction should succeed");
29561        assert!(recovered.is_some());
29562        assert!(engine.txn_page_io.is_none());
29563        assert!(engine.storage_cursors.is_empty());
29564    }
29565
29566    #[test]
29567    fn test_set_transaction_concurrent_refills_retained_shared_state() {
29568        use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29569        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29570        use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29571
29572        let pager = MockMvccPager;
29573        let cx = Cx::new();
29574        let txn1 = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29575        let txn2 = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29576
29577        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29578        let lock_table = Arc::new(InProcessPageLockTable::new());
29579        let commit_index = Arc::new(CommitIndex::new());
29580        let ((session1, handle1), (session2, handle2)) = {
29581            let mut guard = registry
29582                .lock()
29583                .unwrap_or_else(std::sync::PoisonError::into_inner);
29584            let session1 = guard
29585                .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29586                .expect("first session should register");
29587            let session2 = guard
29588                .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29589                .expect("second session should register");
29590            let handle1 = guard
29591                .handle(session1)
29592                .expect("first session handle should exist");
29593            let handle2 = guard
29594                .handle(session2)
29595                .expect("second session handle should exist");
29596            ((session1, handle1), (session2, handle2))
29597        };
29598
29599        let mut engine = VdbeEngine::new(8);
29600        engine.set_transaction_concurrent(
29601            txn1,
29602            session1,
29603            Arc::clone(&handle1),
29604            Arc::clone(&lock_table),
29605            Arc::clone(&commit_index),
29606            0,
29607        );
29608
29609        let mut retained = engine
29610            .txn_page_io
29611            .as_ref()
29612            .expect("engine should install shared txn state")
29613            .clone();
29614        let _drained = engine
29615            .drain_transaction()
29616            .expect("drain_transaction should leave retained state behind");
29617
29618        engine.set_transaction_concurrent(
29619            txn2,
29620            session2,
29621            Arc::clone(&handle2),
29622            Arc::clone(&lock_table),
29623            Arc::clone(&commit_index),
29624            0,
29625        );
29626
29627        run_async(retained.read_page_data(&cx, PageNumber::ONE))
29628            .expect("retained clone should see the refilled transaction instead of Drained");
29629        retained.record_write_witness(&cx, WitnessKey::Page(PageNumber::ONE));
29630
29631        let guard = registry
29632            .lock()
29633            .unwrap_or_else(std::sync::PoisonError::into_inner);
29634        let first = guard
29635            .get(session1)
29636            .expect("first session should remain registered");
29637        let second = guard
29638            .get(session2)
29639            .expect("second session should remain registered");
29640        assert!(
29641            !first
29642                .read_witness_keys()
29643                .contains(&WitnessKey::Page(PageNumber::ONE)),
29644            "retained clone must stop recording reads against the drained concurrent session"
29645        );
29646        assert!(
29647            !first
29648                .write_witness_keys()
29649                .contains(&WitnessKey::Page(PageNumber::ONE)),
29650            "retained clone must stop recording writes against the drained concurrent session"
29651        );
29652        assert!(
29653            second
29654                .read_witness_keys()
29655                .contains(&WitnessKey::Page(PageNumber::ONE)),
29656            "retained clone must record reads on the refilled concurrent session"
29657        );
29658        assert!(
29659            second
29660                .write_witness_keys()
29661                .contains(&WitnessKey::Page(PageNumber::ONE)),
29662            "retained clone must record writes on the refilled concurrent session"
29663        );
29664    }
29665
29666    #[test]
29667    fn test_shared_txn_page_io_btree_read_defers_to_precise_witnesses() {
29668        use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29669        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29670        use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29671
29672        let pager = MemoryMockMvccPager;
29673        let cx = Cx::new();
29674        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29675
29676        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29677        let lock_table = Arc::new(InProcessPageLockTable::new());
29678        let commit_index = Arc::new(CommitIndex::new());
29679        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
29680        let (session_id, handle) = {
29681            let mut guard = registry
29682                .lock()
29683                .unwrap_or_else(std::sync::PoisonError::into_inner);
29684            let session_id = guard
29685                .begin_concurrent(snapshot)
29686                .expect("session should register");
29687            let handle = guard
29688                .handle(session_id)
29689                .expect("session handle should exist");
29690            (session_id, handle)
29691        };
29692
29693        let page_io = SharedTxnPageIo::with_concurrent(
29694            txn,
29695            session_id,
29696            Arc::clone(&handle),
29697            Arc::clone(&lock_table),
29698            Arc::clone(&commit_index),
29699            0,
29700        );
29701
29702        let _ = run_async(page_io.read_btree_page_data(&cx, PageNumber::ONE))
29703            .expect("B-tree page read should succeed");
29704        {
29705            let guard = handle.lock();
29706            assert!(
29707                guard.read_witness_keys().is_empty(),
29708                "B-tree page loads must not publish coarse page read witnesses"
29709            );
29710        }
29711
29712        let cell_witness = WitnessKey::Cell {
29713            btree_root: PageNumber::ONE,
29714            leaf_page: PageNumber::ONE,
29715            tag: 42,
29716        };
29717        page_io.record_read_witness(&cx, cell_witness.clone());
29718
29719        let guard = handle.lock();
29720        assert!(
29721            guard.read_witness_keys().contains(&cell_witness),
29722            "cursor-supplied cell witness must be recorded on the concurrent handle"
29723        );
29724        assert!(
29725            !guard
29726                .read_witness_keys()
29727                .contains(&WitnessKey::Page(PageNumber::ONE)),
29728            "precise B-tree witness publication must not reintroduce coarse page reads"
29729        );
29730    }
29731
29732    #[test]
29733    fn test_set_transaction_clears_stale_concurrent_context_on_retained_reuse() {
29734        use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29735        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29736        use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29737
29738        let pager = MockMvccPager;
29739        let cx = Cx::new();
29740        let concurrent_txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29741        let immediate_txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29742
29743        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29744        let lock_table = Arc::new(InProcessPageLockTable::new());
29745        let commit_index = Arc::new(CommitIndex::new());
29746        let (session_id, handle) = {
29747            let mut guard = registry
29748                .lock()
29749                .unwrap_or_else(std::sync::PoisonError::into_inner);
29750            let session_id = guard
29751                .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29752                .expect("session should register");
29753            let handle = guard
29754                .handle(session_id)
29755                .expect("session handle should exist");
29756            (session_id, handle)
29757        };
29758
29759        let mut engine = VdbeEngine::new(8);
29760        engine.set_transaction_concurrent(
29761            concurrent_txn,
29762            session_id,
29763            Arc::clone(&handle),
29764            Arc::clone(&lock_table),
29765            Arc::clone(&commit_index),
29766            0,
29767        );
29768
29769        let mut retained = engine
29770            .txn_page_io
29771            .as_ref()
29772            .expect("engine should install shared txn state")
29773            .clone();
29774        let _drained = engine
29775            .drain_transaction()
29776            .expect("drain_transaction should leave retained state behind");
29777
29778        engine.set_transaction(immediate_txn);
29779
29780        run_async(retained.read_page_data(&cx, PageNumber::ONE)).expect(
29781            "plain set_transaction should refill the retained clone instead of leaving Drained",
29782        );
29783        retained.record_write_witness(&cx, WitnessKey::Page(PageNumber::ONE));
29784
29785        let guard = registry
29786            .lock()
29787            .unwrap_or_else(std::sync::PoisonError::into_inner);
29788        let session = guard
29789            .get(session_id)
29790            .expect("original concurrent session should remain registered");
29791        assert!(
29792            !session
29793                .read_witness_keys()
29794                .contains(&WitnessKey::Page(PageNumber::ONE)),
29795            "plain set_transaction must clear stale concurrent read tracking on retained clones"
29796        );
29797        assert!(
29798            !session
29799                .write_witness_keys()
29800                .contains(&WitnessKey::Page(PageNumber::ONE)),
29801            "plain set_transaction must clear stale concurrent write tracking on retained clones"
29802        );
29803    }
29804
29805    #[test]
29806    fn test_open_storage_cursor_prefers_txn_backend() {
29807        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29808
29809        let pager = MockMvccPager;
29810        let cx = Cx::new();
29811        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29812
29813        let mut db = MemDatabase::new();
29814        let root = db.create_table(1);
29815
29816        let mut engine = VdbeEngine::new(8);
29817        engine.set_database(db);
29818        engine.set_transaction(txn);
29819
29820        // open_storage_cursor should succeed using the Txn backend.
29821        let opened = run_async(engine.open_storage_cursor(0, root, false));
29822        assert!(opened);
29823
29824        // Verify the cursor exists in storage_cursors.
29825        assert!(engine.storage_cursors.contains_key(&0));
29826
29827        // Clean up: drop cursors before taking transaction.
29828        engine.storage_cursors.clear();
29829        let _txn = engine
29830            .take_transaction()
29831            .expect("take_transaction should succeed");
29832    }
29833
29834    #[test]
29835    fn test_open_storage_cursor_txn_index_honors_desc_key_metadata() {
29836        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29837        use fsqlite_types::record::{parse_record, serialize_record};
29838
29839        let pager = MemoryMockMvccPager;
29840        let cx = Cx::new();
29841        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29842        let root = 256;
29843
29844        let mut engine = VdbeEngine::new(8);
29845        engine.set_database(MemDatabase::new());
29846        engine.set_transaction(txn);
29847        engine.set_index_desc_flags_by_root_page(HashMap::from([(root, vec![true])]));
29848
29849        assert!(
29850            run_async(engine.open_storage_cursor(0, root, true)),
29851            "writable txn-backed index cursor should open on a fresh root page"
29852        );
29853
29854        let sc = engine.storage_cursors.get_mut(&0).unwrap();
29855        let early_key = serialize_record(&[SqliteValue::Integer(10), SqliteValue::Integer(1)]);
29856        let late_key = serialize_record(&[SqliteValue::Integer(20), SqliteValue::Integer(2)]);
29857        run_async(sc.cursor.index_insert(&sc.cx, &early_key)).unwrap();
29858        run_async(sc.cursor.index_insert(&sc.cx, &late_key)).unwrap();
29859
29860        assert!(run_async(sc.cursor.first(&sc.cx)).unwrap());
29861        let first_values = parse_record(&run_async(sc.cursor.payload(&sc.cx)).unwrap()).unwrap();
29862        assert_eq!(
29863            first_values,
29864            vec![SqliteValue::Integer(20), SqliteValue::Integer(2)],
29865            "descending index cursor should order the larger key first"
29866        );
29867
29868        assert!(run_async(sc.cursor.next(&sc.cx)).unwrap());
29869        let second_values = parse_record(&run_async(sc.cursor.payload(&sc.cx)).unwrap()).unwrap();
29870        assert_eq!(
29871            second_values,
29872            vec![SqliteValue::Integer(10), SqliteValue::Integer(1)],
29873            "descending index cursor should keep the smaller key after the larger one"
29874        );
29875
29876        engine.storage_cursors.clear();
29877        let _txn = engine
29878            .take_transaction()
29879            .expect("take_transaction should succeed");
29880    }
29881
29882    #[test]
29883    fn test_open_storage_cursor_txn_zero_page_honors_page1_reserved_bytes_layout() {
29884        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29885
29886        let pager = MemoryMockMvccPager;
29887        let cx = Cx::new();
29888        let mut txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29889        let root_pgno = PageNumber::new(256).unwrap();
29890
29891        let mut header = DatabaseHeader {
29892            page_size: PageSize::DEFAULT,
29893            reserved_per_page: 32,
29894            page_count: root_pgno.get(),
29895            ..DatabaseHeader::default()
29896        };
29897        header.version_valid_for = header.change_counter;
29898        let mut page_one = vec![0u8; PageSize::DEFAULT.as_usize()];
29899        page_one[..DATABASE_HEADER_SIZE].copy_from_slice(&header.to_bytes().unwrap());
29900        run_async(txn.write_page(&cx, PageNumber::ONE, &page_one)).unwrap();
29901
29902        let mut engine = VdbeEngine::new(8);
29903        engine.set_transaction(txn);
29904
29905        assert!(
29906            run_async(engine.open_storage_cursor(0, root_pgno.get() as i32, true)),
29907            "txn-backed writable cursor should open on a fresh reserved-byte root page"
29908        );
29909
29910        let storage_cursor = engine.storage_cursors.get(&0).unwrap();
29911        assert_eq!(
29912            storage_cursor.cursor.usable_size(),
29913            PageSize::DEFAULT.get() - 32
29914        );
29915        assert_eq!(storage_cursor.cursor.page_size(), PageSize::DEFAULT.get());
29916
29917        let root_page = run_async(
29918            engine
29919                .txn_page_io
29920                .as_ref()
29921                .unwrap()
29922                .read_page(&cx, root_pgno),
29923        )
29924        .unwrap();
29925        assert_eq!(root_page[0], BtreePageType::LeafTable as u8);
29926        assert_eq!(
29927            u16::from_be_bytes([root_page[5], root_page[6]]),
29928            (PageSize::DEFAULT.get() - 32) as u16,
29929            "fresh reserved-byte root pages must start their cell content area at usable_size"
29930        );
29931
29932        engine.storage_cursors.clear();
29933        let _txn = engine
29934            .take_transaction()
29935            .expect("take_transaction should succeed");
29936    }
29937
29938    #[test]
29939    fn test_open_storage_cursor_zero_page_init_uses_owned_passthrough_write() {
29940        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29941
29942        // bd-1dp9.6.2 single-lever, proof-preserving optimization guard:
29943        // initializing a freshly-allocated (zeroed) writable B-tree root page must
29944        // move its owned, exactly-page-size image straight through the owned
29945        // write-normalization lane (owned passthrough), never the borrowed
29946        // `write_page` lane that clones a second full-page image via `to_vec()`.
29947        let _guard = VDBE_OBSERVABILITY_LOCK
29948            .lock()
29949            .unwrap_or_else(|e| e.into_inner());
29950        let prev_metrics_enabled = vdbe_metrics_enabled();
29951        reset_vdbe_metrics();
29952        set_vdbe_metrics_enabled(true);
29953
29954        let pager = MemoryMockMvccPager;
29955        let cx = Cx::new();
29956        let mut txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29957        let root_pgno = PageNumber::new(2).unwrap();
29958
29959        let mut header = DatabaseHeader {
29960            page_size: PageSize::DEFAULT,
29961            reserved_per_page: 0,
29962            page_count: root_pgno.get(),
29963            ..DatabaseHeader::default()
29964        };
29965        header.version_valid_for = header.change_counter;
29966        let mut page_one = vec![0u8; PageSize::DEFAULT.as_usize()];
29967        page_one[..DATABASE_HEADER_SIZE].copy_from_slice(&header.to_bytes().unwrap());
29968        run_async(txn.write_page(&cx, PageNumber::ONE, &page_one)).unwrap();
29969
29970        let mut engine = VdbeEngine::new(8);
29971        engine.set_transaction(txn);
29972
29973        // Snapshot motion counters immediately before the root-init write so the
29974        // delta isolates exactly the zero-page initialization path.
29975        let before = vdbe_metrics_snapshot();
29976        assert!(
29977            run_async(engine.open_storage_cursor(0, root_pgno.get() as i32, true)),
29978            "txn-backed writable cursor should open on a fresh zeroed root page"
29979        );
29980        let after = vdbe_metrics_snapshot();
29981
29982        // The root-init write takes the owned-passthrough lane: at least one owned
29983        // passthrough, and zero borrowed/resized full-page copies on this path.
29984        assert!(
29985            after.page_data_motion.owned_passthrough_total
29986                - before.page_data_motion.owned_passthrough_total
29987                >= 1,
29988            "zeroed-root init must write its owned page image via owned passthrough"
29989        );
29990        assert_eq!(
29991            after.page_data_motion.borrowed_exact_size_copies_total
29992                - before.page_data_motion.borrowed_exact_size_copies_total,
29993            0,
29994            "zeroed-root init must not clone a full page image through the borrowed lane"
29995        );
29996        assert_eq!(
29997            after.page_data_motion.owned_resized_copies_total
29998                - before.page_data_motion.owned_resized_copies_total,
29999            0,
30000            "an exactly-page-size root image must never trigger a resize copy"
30001        );
30002
30003        // Behavior preservation: the initialized root page is still a correctly
30004        // formed empty leaf-table page (type flag + cell-content offset).
30005        let root_page = run_async(
30006            engine
30007                .txn_page_io
30008                .as_ref()
30009                .unwrap()
30010                .read_page(&cx, root_pgno),
30011        )
30012        .unwrap();
30013        assert_eq!(root_page[0], BtreePageType::LeafTable as u8);
30014        assert_eq!(
30015            u32::from(u16::from_be_bytes([root_page[5], root_page[6]])),
30016            PageSize::DEFAULT.get(),
30017            "fresh reserved-byte-free root pages start their cell content area at usable_size"
30018        );
30019
30020        engine.storage_cursors.clear();
30021        let _txn = engine
30022            .take_transaction()
30023            .expect("take_transaction should succeed");
30024        set_vdbe_metrics_enabled(prev_metrics_enabled);
30025    }
30026
30027    #[test]
30028    fn test_open_storage_cursor_records_root_page_for_manual_open() {
30029        let mut engine = VdbeEngine::new(8);
30030        let mut db = MemDatabase::new();
30031        let root_page = db.allocate_root_page();
30032        engine.enable_storage_cursors(true);
30033        engine.set_database(db);
30034        engine.set_reject_mem_fallback(false);
30035
30036        assert!(
30037            run_async(engine.open_storage_cursor(7, root_page, true)),
30038            "manual storage cursor open should succeed"
30039        );
30040        assert_eq!(
30041            engine.cursor_root_pages.get(&7),
30042            Some(&root_page),
30043            "manual open must publish root-page metadata for later index collation lookups"
30044        );
30045        assert_eq!(
30046            engine
30047                .storage_cursors
30048                .get(&7)
30049                .expect("storage cursor should exist")
30050                .root_page,
30051            root_page,
30052            "cursor metadata and published root-page map must stay in sync"
30053        );
30054    }
30055
30056    #[test]
30057    fn test_storage_cursor_no_conflict_scratch_matches_legacy_probe_semantics() {
30058        let _guard = VDBE_OBSERVABILITY_LOCK
30059            .lock()
30060            .unwrap_or_else(|e| e.into_inner());
30061        let (mut engine, conflict_key, miss_key) =
30062            build_storage_index_engine_with_duplicate_prefixes();
30063        let cursor = engine
30064            .storage_cursors
30065            .get_mut(&0)
30066            .expect("storage cursor should exist");
30067
30068        let legacy_conflict = legacy_storage_cursor_no_conflict_prefix_match(cursor, &conflict_key)
30069            .expect("legacy conflict probe should succeed");
30070        let scratch_conflict = run_async(storage_cursor_no_conflict_prefix_match(
30071            0,
30072            cursor,
30073            &conflict_key,
30074        ))
30075        .expect("scratch conflict probe should succeed");
30076        assert_eq!(scratch_conflict, legacy_conflict);
30077        assert!(scratch_conflict, "duplicate prefix should conflict");
30078
30079        let legacy_miss = legacy_storage_cursor_no_conflict_prefix_match(cursor, &miss_key)
30080            .expect("legacy miss probe should succeed");
30081        let scratch_miss = run_async(storage_cursor_no_conflict_prefix_match(
30082            0, cursor, &miss_key,
30083        ))
30084        .expect("scratch miss probe should succeed");
30085        assert_eq!(scratch_miss, legacy_miss);
30086        assert!(!scratch_miss, "distinct prefix should not conflict");
30087
30088        let malformed = vec![0x80];
30089        let legacy_malformed = legacy_storage_cursor_no_conflict_prefix_match(cursor, &malformed)
30090            .expect("legacy malformed probe should not raise");
30091        let scratch_malformed = run_async(storage_cursor_no_conflict_prefix_match(
30092            0, cursor, &malformed,
30093        ))
30094        .expect("scratch malformed probe should not raise");
30095        assert_eq!(scratch_malformed, legacy_malformed);
30096        assert!(
30097            !scratch_malformed,
30098            "malformed probes should not report conflicts"
30099        );
30100    }
30101
30102    #[test]
30103    fn test_storage_cursor_no_conflict_collated_probe_honors_nocase() {
30104        let _guard = VDBE_OBSERVABILITY_LOCK
30105            .lock()
30106            .unwrap_or_else(|e| e.into_inner());
30107        let (mut engine, conflict_key, miss_key) =
30108            build_storage_index_engine_with_nocase_prefixes();
30109        let coll_arc = Arc::clone(&engine.collation_registry);
30110        let coll_guard = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
30111        let root_page = engine
30112            .cursor_root_pages
30113            .get(&0)
30114            .copied()
30115            .expect("storage cursor should record its root page");
30116        let desc_flags = engine.index_desc_flags_for_root(root_page);
30117        let collations = engine.index_collations_for_root(root_page);
30118        let cursor = engine
30119            .storage_cursors
30120            .get_mut(&0)
30121            .expect("storage cursor should exist");
30122
30123        assert!(
30124            run_async(storage_cursor_no_conflict_prefix_match_collated(
30125                0,
30126                cursor,
30127                &conflict_key,
30128                &desc_flags,
30129                &collations,
30130                &coll_guard,
30131            ))
30132            .expect("collated conflict probe should succeed"),
30133            "NOCASE-equivalent text must be treated as a duplicate prefix"
30134        );
30135        assert!(
30136            !run_async(storage_cursor_no_conflict_prefix_match_collated(
30137                0,
30138                cursor,
30139                &miss_key,
30140                &desc_flags,
30141                &collations,
30142                &coll_guard,
30143            ))
30144            .expect("collated miss probe should succeed"),
30145            "distinct NOCASE text must remain insertable"
30146        );
30147    }
30148
30149    #[test]
30150    fn test_storage_cursor_no_conflict_reuses_payload_and_value_scratch() {
30151        let _guard = VDBE_OBSERVABILITY_LOCK
30152            .lock()
30153            .unwrap_or_else(|e| e.into_inner());
30154        let (mut engine, conflict_key, _) = build_storage_index_engine_with_duplicate_prefixes();
30155        let iterations = 256;
30156        let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
30157
30158        let run_legacy = |cursor: &mut StorageCursor| {
30159            fsqlite_types::record::reset_record_profile();
30160            fsqlite_btree::reset_btree_copy_profile();
30161            fsqlite_btree::set_btree_copy_profile_enabled(true);
30162            let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30163            let mut matches = 0usize;
30164            for _ in 0..iterations {
30165                if legacy_storage_cursor_no_conflict_prefix_match(cursor, &conflict_key)
30166                    .expect("legacy probe should succeed")
30167                {
30168                    matches += 1;
30169                }
30170            }
30171            (
30172                matches,
30173                fsqlite_types::record::record_profile_snapshot(),
30174                fsqlite_btree::btree_copy_profile_snapshot(),
30175            )
30176        };
30177
30178        let run_scratch = |cursor: &mut StorageCursor| {
30179            fsqlite_types::record::reset_record_profile();
30180            fsqlite_btree::reset_btree_copy_profile();
30181            fsqlite_btree::set_btree_copy_profile_enabled(true);
30182            let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30183            let mut matches = 0usize;
30184            let mut payload_ptr = None;
30185            let mut value_ptr = None;
30186            for _ in 0..iterations {
30187                if run_async(storage_cursor_no_conflict_prefix_match(
30188                    0,
30189                    cursor,
30190                    &conflict_key,
30191                ))
30192                .expect("scratch probe should succeed")
30193                {
30194                    matches += 1;
30195                }
30196                let payload_buf_ptr = cursor.payload_buf.as_ptr() as usize;
30197                let cur_vals_ptr = cursor.cur_vals_buf.as_ptr() as usize;
30198                if let Some(expected) = payload_ptr {
30199                    assert_eq!(
30200                        payload_buf_ptr, expected,
30201                        "payload scratch buffer should be reused across probes"
30202                    );
30203                } else {
30204                    payload_ptr = Some(payload_buf_ptr);
30205                }
30206                if let Some(expected) = value_ptr {
30207                    assert_eq!(
30208                        cur_vals_ptr, expected,
30209                        "value scratch buffer should be reused across probes"
30210                    );
30211                } else {
30212                    value_ptr = Some(cur_vals_ptr);
30213                }
30214            }
30215            (
30216                matches,
30217                fsqlite_types::record::record_profile_snapshot(),
30218                fsqlite_btree::btree_copy_profile_snapshot(),
30219            )
30220        };
30221
30222        let (legacy_matches, legacy_record_profile, legacy_copy_profile) = {
30223            let cursor = engine
30224                .storage_cursors
30225                .get_mut(&0)
30226                .expect("storage cursor should exist");
30227            run_legacy(cursor)
30228        };
30229        let (scratch_matches, scratch_record_profile, scratch_copy_profile) = {
30230            let cursor = engine
30231                .storage_cursors
30232                .get_mut(&0)
30233                .expect("storage cursor should exist");
30234            run_scratch(cursor)
30235        };
30236
30237        assert_eq!(legacy_matches, iterations);
30238        assert_eq!(scratch_matches, iterations);
30239        assert!(
30240            legacy_record_profile
30241                .callsite_breakdown
30242                .vdbe_engine
30243                .parse_record_calls
30244                >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30245            "legacy loop should fully parse both probe and entry on every iteration"
30246        );
30247        assert_eq!(
30248            scratch_record_profile
30249                .callsite_breakdown
30250                .vdbe_engine
30251                .parse_record_calls,
30252            0,
30253            "scratch path should eliminate full parse_record allocations"
30254        );
30255        assert!(
30256            scratch_record_profile
30257                .callsite_breakdown
30258                .vdbe_engine
30259                .parse_record_into_calls
30260                >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30261            "scratch path should decode probe and entry into reusable buffers"
30262        );
30263        assert!(
30264            legacy_copy_profile.owned_payload_materialization_calls
30265                >= u64::try_from(iterations).unwrap_or(u64::MAX),
30266            "legacy path should materialize an owned payload for every probe"
30267        );
30268        assert_eq!(
30269            scratch_copy_profile.owned_payload_materialization_calls, 0,
30270            "scratch path should avoid owned payload materialization"
30271        );
30272        assert!(
30273            scratch_copy_profile.local_payload_copy_calls
30274                >= u64::try_from(iterations).unwrap_or(u64::MAX),
30275            "scratch path should reuse the caller-owned payload buffer"
30276        );
30277
30278        eprintln!(
30279            concat!(
30280                "BD_DB300_4_3_2_CHURN_JSON=",
30281                "{{",
30282                "\"bead_id\":\"bd-db300.4.3.2\",",
30283                "\"hot_path\":\"storage_cursor_no_conflict_index_probe\",",
30284                "\"iterations\":{},",
30285                "\"legacy\":{{",
30286                "\"parse_record_calls\":{},",
30287                "\"parse_record_into_calls\":{},",
30288                "\"owned_payload_materialization_calls\":{},",
30289                "\"owned_payload_materialization_bytes\":{},",
30290                "\"local_payload_copy_calls\":{}",
30291                "}},",
30292                "\"scratch\":{{",
30293                "\"parse_record_calls\":{},",
30294                "\"parse_record_into_calls\":{},",
30295                "\"owned_payload_materialization_calls\":{},",
30296                "\"owned_payload_materialization_bytes\":{},",
30297                "\"local_payload_copy_calls\":{}",
30298                "}}",
30299                "}}"
30300            ),
30301            iterations,
30302            legacy_record_profile
30303                .callsite_breakdown
30304                .vdbe_engine
30305                .parse_record_calls,
30306            legacy_record_profile
30307                .callsite_breakdown
30308                .vdbe_engine
30309                .parse_record_into_calls,
30310            legacy_copy_profile.owned_payload_materialization_calls,
30311            legacy_copy_profile.owned_payload_materialization_bytes,
30312            legacy_copy_profile.local_payload_copy_calls,
30313            scratch_record_profile
30314                .callsite_breakdown
30315                .vdbe_engine
30316                .parse_record_calls,
30317            scratch_record_profile
30318                .callsite_breakdown
30319                .vdbe_engine
30320                .parse_record_into_calls,
30321            scratch_copy_profile.owned_payload_materialization_calls,
30322            scratch_copy_profile.owned_payload_materialization_bytes,
30323            scratch_copy_profile.local_payload_copy_calls,
30324        );
30325
30326        fsqlite_btree::set_btree_copy_profile_enabled(false);
30327    }
30328
30329    #[test]
30330    fn test_storage_cursor_no_conflict_mixed_boundaries_reduce_allocator_pressure_and_preserve_reset_safety()
30331     {
30332        let _guard = VDBE_OBSERVABILITY_LOCK
30333            .lock()
30334            .unwrap_or_else(|e| e.into_inner());
30335        let (mut engine, conflict_key, miss_key) =
30336            build_storage_index_engine_with_duplicate_prefixes();
30337        let malformed_key = vec![0x80];
30338        let conflict_target = [SqliteValue::Integer(7)];
30339        let miss_target = [SqliteValue::Integer(9)];
30340        let iterations = 128usize;
30341        let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
30342
30343        let run_legacy = |cursor: &mut StorageCursor| {
30344            fsqlite_types::record::reset_record_profile();
30345            fsqlite_btree::reset_btree_copy_profile();
30346            fsqlite_btree::set_btree_copy_profile_enabled(true);
30347            let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30348            let mut matches = 0usize;
30349            for _ in 0..iterations {
30350                for (probe, expected) in [
30351                    (conflict_key.as_slice(), true),
30352                    (miss_key.as_slice(), false),
30353                    (malformed_key.as_slice(), false),
30354                    (conflict_key.as_slice(), true),
30355                ] {
30356                    let actual = legacy_storage_cursor_no_conflict_prefix_match(cursor, probe)
30357                        .expect("legacy probe should succeed");
30358                    assert_eq!(actual, expected);
30359                    if actual {
30360                        matches += 1;
30361                    }
30362                }
30363            }
30364            (
30365                matches,
30366                fsqlite_types::record::record_profile_snapshot(),
30367                fsqlite_btree::btree_copy_profile_snapshot(),
30368            )
30369        };
30370
30371        let run_scratch = |cursor: &mut StorageCursor| {
30372            fsqlite_types::record::reset_record_profile();
30373            fsqlite_btree::reset_btree_copy_profile();
30374            fsqlite_btree::set_btree_copy_profile_enabled(true);
30375            let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30376            let mut matches = 0usize;
30377            let mut payload_capacity = None;
30378            let mut target_capacity = None;
30379            let mut current_capacity = None;
30380
30381            for _ in 0..iterations {
30382                for (boundary, probe, expected, expected_target) in [
30383                    (
30384                        "conflict",
30385                        conflict_key.as_slice(),
30386                        true,
30387                        Some(conflict_target.as_slice()),
30388                    ),
30389                    (
30390                        "miss",
30391                        miss_key.as_slice(),
30392                        false,
30393                        Some(miss_target.as_slice()),
30394                    ),
30395                    ("malformed", malformed_key.as_slice(), false, None),
30396                    (
30397                        "recovery",
30398                        conflict_key.as_slice(),
30399                        true,
30400                        Some(conflict_target.as_slice()),
30401                    ),
30402                ] {
30403                    let actual =
30404                        run_async(storage_cursor_no_conflict_prefix_match(0, cursor, probe))
30405                            .expect("scratch probe should succeed");
30406                    assert_eq!(
30407                        actual, expected,
30408                        "{boundary} probe should preserve semantics"
30409                    );
30410                    if actual {
30411                        matches += 1;
30412                    }
30413
30414                    match expected_target {
30415                        Some(expected_target) => {
30416                            assert_eq!(
30417                                cursor.target_vals_buf.as_slice(),
30418                                expected_target,
30419                                "{boundary} probe should refresh target scratch without stale values"
30420                            );
30421                        }
30422                        None => {
30423                            assert!(
30424                                cursor.target_vals_buf.is_empty(),
30425                                "{boundary} probe should clear malformed target scratch"
30426                            );
30427                        }
30428                    }
30429
30430                    let observed_payload_capacity = cursor.payload_buf.capacity();
30431                    if let Some(expected_capacity) = payload_capacity {
30432                        assert_eq!(
30433                            observed_payload_capacity, expected_capacity,
30434                            "{boundary} probe should retain payload scratch capacity"
30435                        );
30436                    } else if observed_payload_capacity > 0 {
30437                        payload_capacity = Some(observed_payload_capacity);
30438                    }
30439
30440                    let observed_target_capacity = cursor.target_vals_buf.capacity();
30441                    if let Some(expected_capacity) = target_capacity {
30442                        assert_eq!(
30443                            observed_target_capacity, expected_capacity,
30444                            "{boundary} probe should retain target scratch capacity"
30445                        );
30446                    } else if observed_target_capacity > 0 {
30447                        target_capacity = Some(observed_target_capacity);
30448                    }
30449
30450                    let observed_current_capacity = cursor.cur_vals_buf.capacity();
30451                    if let Some(expected_capacity) = current_capacity {
30452                        assert_eq!(
30453                            observed_current_capacity, expected_capacity,
30454                            "{boundary} probe should retain current-entry scratch capacity"
30455                        );
30456                    } else if observed_current_capacity > 0 {
30457                        current_capacity = Some(observed_current_capacity);
30458                    }
30459                }
30460            }
30461
30462            (
30463                matches,
30464                fsqlite_types::record::record_profile_snapshot(),
30465                fsqlite_btree::btree_copy_profile_snapshot(),
30466                payload_capacity.unwrap_or_else(|| cursor.payload_buf.capacity()),
30467                target_capacity.unwrap_or_else(|| cursor.target_vals_buf.capacity()),
30468                current_capacity.unwrap_or_else(|| cursor.cur_vals_buf.capacity()),
30469            )
30470        };
30471
30472        let (legacy_matches, legacy_record_profile, legacy_copy_profile) = {
30473            let cursor = engine
30474                .storage_cursors
30475                .get_mut(&0)
30476                .expect("storage cursor should exist");
30477            run_legacy(cursor)
30478        };
30479        let (
30480            scratch_matches,
30481            scratch_record_profile,
30482            scratch_copy_profile,
30483            payload_capacity,
30484            target_capacity,
30485            current_capacity,
30486        ) = {
30487            let cursor = engine
30488                .storage_cursors
30489                .get_mut(&0)
30490                .expect("storage cursor should exist");
30491            run_scratch(cursor)
30492        };
30493
30494        assert_eq!(legacy_matches, iterations * 2);
30495        assert_eq!(scratch_matches, iterations * 2);
30496        assert_eq!(
30497            scratch_record_profile
30498                .callsite_breakdown
30499                .vdbe_engine
30500                .parse_record_calls,
30501            0,
30502            "mixed-boundary scratch path should avoid parse_record allocations entirely"
30503        );
30504        assert!(
30505            legacy_record_profile
30506                .callsite_breakdown
30507                .vdbe_engine
30508                .parse_record_calls
30509                > scratch_record_profile
30510                    .callsite_breakdown
30511                    .vdbe_engine
30512                    .parse_record_calls,
30513            "legacy path should incur more parse_record churn under mixed boundaries"
30514        );
30515        assert_eq!(
30516            scratch_copy_profile.owned_payload_materialization_calls, 0,
30517            "scratch path should avoid owned payload materialization under mixed boundaries"
30518        );
30519        assert!(
30520            legacy_copy_profile.owned_payload_materialization_calls
30521                >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30522            "legacy path should materialize owned payloads for both found probes per iteration"
30523        );
30524        assert!(
30525            scratch_copy_profile.local_payload_copy_calls
30526                >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30527            "scratch path should reuse caller-owned payload storage on found probes"
30528        );
30529        assert!(payload_capacity > 0);
30530        assert!(target_capacity > 0);
30531        assert!(current_capacity > 0);
30532
30533        eprintln!(
30534            concat!(
30535                "BD_DB300_4_3_3_RESET_CHURN_JSON=",
30536                "{{",
30537                "\"bead_id\":\"bd-db300.4.3.3\",",
30538                "\"hot_path\":\"storage_cursor_no_conflict_mixed_boundaries\",",
30539                "\"iterations\":{},",
30540                "\"boundary_sequence\":\"conflict,miss,malformed,recovery\",",
30541                "\"legacy\":{{",
30542                "\"parse_record_calls\":{},",
30543                "\"owned_payload_materialization_calls\":{}",
30544                "}},",
30545                "\"scratch\":{{",
30546                "\"parse_record_calls\":{},",
30547                "\"owned_payload_materialization_calls\":{},",
30548                "\"local_payload_copy_calls\":{},",
30549                "\"payload_capacity\":{},",
30550                "\"target_capacity\":{},",
30551                "\"current_capacity\":{}",
30552                "}}",
30553                "}}"
30554            ),
30555            iterations,
30556            legacy_record_profile
30557                .callsite_breakdown
30558                .vdbe_engine
30559                .parse_record_calls,
30560            legacy_copy_profile.owned_payload_materialization_calls,
30561            scratch_record_profile
30562                .callsite_breakdown
30563                .vdbe_engine
30564                .parse_record_calls,
30565            scratch_copy_profile.owned_payload_materialization_calls,
30566            scratch_copy_profile.local_payload_copy_calls,
30567            payload_capacity,
30568            target_capacity,
30569            current_capacity,
30570        );
30571
30572        fsqlite_btree::set_btree_copy_profile_enabled(false);
30573    }
30574
30575    #[test]
30576    fn test_no_conflict_opcode_uses_storage_cursor_scratch_probe_path() {
30577        let _guard = VDBE_OBSERVABILITY_LOCK
30578            .lock()
30579            .unwrap_or_else(|e| e.into_inner());
30580        let (mut engine, conflict_key, miss_key) =
30581            build_storage_index_engine_with_duplicate_prefixes();
30582        let program = build_no_conflict_opcode_probe_program(conflict_key, miss_key, vec![0x80]);
30583        let rows = execute_program_with_engine(&mut engine, &program);
30584
30585        assert_eq!(
30586            rows,
30587            vec![
30588                vec![SqliteValue::Integer(0)],
30589                vec![SqliteValue::Integer(1)],
30590                vec![SqliteValue::Integer(1)],
30591            ],
30592            "NoConflict should treat duplicate prefixes as conflicts and malformed probes as insertable"
30593        );
30594    }
30595
30596    #[test]
30597    fn test_no_conflict_opcode_honors_nocase_collation() {
30598        let _guard = VDBE_OBSERVABILITY_LOCK
30599            .lock()
30600            .unwrap_or_else(|e| e.into_inner());
30601        let (mut engine, conflict_key, miss_key) =
30602            build_storage_index_engine_with_nocase_prefixes();
30603        let program = build_no_conflict_opcode_probe_program(conflict_key, miss_key, vec![0x80]);
30604        let rows = execute_program_with_engine(&mut engine, &program);
30605
30606        assert_eq!(
30607            rows,
30608            vec![
30609                vec![SqliteValue::Integer(0)],
30610                vec![SqliteValue::Integer(1)],
30611                vec![SqliteValue::Integer(1)],
30612            ],
30613            "NoConflict must honor per-index NOCASE equality when probing storage indexes"
30614        );
30615    }
30616
30617    #[test]
30618    fn test_found_opcode_honors_nocase_collation() {
30619        let _guard = VDBE_OBSERVABILITY_LOCK
30620            .lock()
30621            .unwrap_or_else(|e| e.into_inner());
30622        let (mut engine, _, _) = build_storage_index_engine_with_nocase_prefixes();
30623        let conflict_key =
30624            encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(1)]);
30625        let miss_key =
30626            encode_record(&[SqliteValue::Text("gamma".into()), SqliteValue::Integer(99)]);
30627        let program = build_found_opcode_probe_program(conflict_key, miss_key);
30628        let rows = execute_program_with_engine(&mut engine, &program);
30629
30630        assert_eq!(
30631            rows,
30632            vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(0)]],
30633            "Found must treat NOCASE-equivalent index keys as exact matches"
30634        );
30635    }
30636
30637    #[test]
30638    fn test_not_found_opcode_honors_nocase_collation() {
30639        let _guard = VDBE_OBSERVABILITY_LOCK
30640            .lock()
30641            .unwrap_or_else(|e| e.into_inner());
30642        let (mut engine, _, _) = build_storage_index_engine_with_nocase_prefixes();
30643        let conflict_key =
30644            encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(1)]);
30645        let miss_key =
30646            encode_record(&[SqliteValue::Text("gamma".into()), SqliteValue::Integer(99)]);
30647        let program = build_not_found_opcode_probe_program(conflict_key, miss_key);
30648        let rows = execute_program_with_engine(&mut engine, &program);
30649
30650        assert_eq!(
30651            rows,
30652            vec![vec![SqliteValue::Integer(0)], vec![SqliteValue::Integer(1)]],
30653            "NotFound must respect NOCASE equality when probing storage indexes"
30654        );
30655    }
30656
30657    #[test]
30658    fn test_seekge_honors_nocase_collation_for_full_index_keys() {
30659        let _guard = VDBE_OBSERVABILITY_LOCK
30660            .lock()
30661            .unwrap_or_else(|e| e.into_inner());
30662        let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30663
30664        let mut b = ProgramBuilder::new();
30665        let end = b.emit_label();
30666        let done = b.emit_label();
30667        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30668
30669        let r_text = b.alloc_reg();
30670        let r_rowid = b.alloc_reg();
30671        let r_seek_key = b.alloc_reg();
30672        let r_found_text = b.alloc_reg();
30673        let r_found_rowid = b.alloc_reg();
30674
30675        b.emit_op(
30676            Opcode::String8,
30677            0,
30678            r_text,
30679            0,
30680            P4::Str("ALPHA".to_owned()),
30681            0,
30682        );
30683        b.emit_op(Opcode::Integer, 1, r_rowid, 0, P4::None, 0);
30684        b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30685        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30686        b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30687        b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30688        b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30689
30690        b.resolve_label(done);
30691        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30692        b.resolve_label(end);
30693
30694        let program = b.finish().expect("program should build");
30695        let rows = execute_program_with_engine(&mut engine, &program);
30696
30697        assert_eq!(
30698            rows,
30699            vec![vec![
30700                SqliteValue::Text("alpha".into()),
30701                SqliteValue::Integer(1),
30702            ]],
30703            "SeekGE must land on the smallest collated key >= the probe, not the first raw-binary successor"
30704        );
30705    }
30706
30707    #[test]
30708    fn test_seekgt_honors_nocase_collation_for_full_index_keys() {
30709        let _guard = VDBE_OBSERVABILITY_LOCK
30710            .lock()
30711            .unwrap_or_else(|e| e.into_inner());
30712        let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30713
30714        let mut b = ProgramBuilder::new();
30715        let end = b.emit_label();
30716        let done = b.emit_label();
30717        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30718
30719        let r_text = b.alloc_reg();
30720        let r_rowid = b.alloc_reg();
30721        let r_seek_key = b.alloc_reg();
30722        let r_found_text = b.alloc_reg();
30723        let r_found_rowid = b.alloc_reg();
30724
30725        b.emit_op(
30726            Opcode::String8,
30727            0,
30728            r_text,
30729            0,
30730            P4::Str("alpha".to_owned()),
30731            0,
30732        );
30733        b.emit_op(Opcode::Integer, 1, r_rowid, 0, P4::None, 0);
30734        b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30735        b.emit_jump_to_label(Opcode::SeekGT, 0, r_seek_key, done, P4::None, 0);
30736        b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30737        b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30738        b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30739
30740        b.resolve_label(done);
30741        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30742        b.resolve_label(end);
30743
30744        let program = b.finish().expect("program should build");
30745        let rows = execute_program_with_engine(&mut engine, &program);
30746
30747        assert_eq!(
30748            rows,
30749            vec![vec![
30750                SqliteValue::Text("ALPHA".into()),
30751                SqliteValue::Integer(2),
30752            ]],
30753            "SeekGT must skip only collated-equal predecessors and land on the next greater key"
30754        );
30755    }
30756
30757    #[test]
30758    fn test_seeklt_honors_nocase_collation_for_full_index_keys() {
30759        let _guard = VDBE_OBSERVABILITY_LOCK
30760            .lock()
30761            .unwrap_or_else(|e| e.into_inner());
30762        let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30763
30764        let mut b = ProgramBuilder::new();
30765        let end = b.emit_label();
30766        let done = b.emit_label();
30767        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30768
30769        let r_text = b.alloc_reg();
30770        let r_rowid = b.alloc_reg();
30771        let r_seek_key = b.alloc_reg();
30772        let r_found_text = b.alloc_reg();
30773        let r_found_rowid = b.alloc_reg();
30774
30775        b.emit_op(
30776            Opcode::String8,
30777            0,
30778            r_text,
30779            0,
30780            P4::Str("ALPHA".to_owned()),
30781            0,
30782        );
30783        b.emit_op(Opcode::Integer, 2, r_rowid, 0, P4::None, 0);
30784        b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30785        b.emit_jump_to_label(Opcode::SeekLT, 0, r_seek_key, done, P4::None, 0);
30786        b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30787        b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30788        b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30789
30790        b.resolve_label(done);
30791        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30792        b.resolve_label(end);
30793
30794        let program = b.finish().expect("program should build");
30795        let rows = execute_program_with_engine(&mut engine, &program);
30796
30797        assert_eq!(
30798            rows,
30799            vec![vec![
30800                SqliteValue::Text("alpha".into()),
30801                SqliteValue::Integer(1),
30802            ]],
30803            "SeekLT must return the greatest collated key strictly below the probe"
30804        );
30805    }
30806
30807    #[test]
30808    fn test_idxinsert_unique_no_conflict_inserts_distinct_prefix_after_cursor_disturbance() {
30809        let _guard = VDBE_OBSERVABILITY_LOCK
30810            .lock()
30811            .unwrap_or_else(|e| e.into_inner());
30812        let (mut engine, _, _) = build_storage_index_engine_with_duplicate_prefixes();
30813
30814        let disturb_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(1)]);
30815        let inserted_key = encode_record(&[SqliteValue::Integer(9), SqliteValue::Integer(500)]);
30816
30817        let mut b = ProgramBuilder::new();
30818        let end = b.emit_label();
30819        let disturbed = b.emit_label();
30820        let found = b.emit_label();
30821        let done = b.emit_label();
30822        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30823
30824        let r_disturb_key = b.alloc_reg();
30825        let r_insert_key = b.alloc_reg();
30826        let r_probe_key = b.alloc_reg();
30827        let r_out = b.alloc_reg();
30828
30829        b.emit_op(Opcode::Blob, 0, r_disturb_key, 0, P4::Blob(disturb_key), 0);
30830        b.emit_jump_to_label(Opcode::Found, 0, r_disturb_key, disturbed, P4::None, 0);
30831        b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
30832        b.resolve_label(disturbed);
30833
30834        b.emit_op(
30835            Opcode::Blob,
30836            0,
30837            r_insert_key,
30838            0,
30839            P4::Blob(inserted_key.clone()),
30840            0,
30841        );
30842        b.emit_op(
30843            Opcode::IdxInsert,
30844            0,
30845            r_insert_key,
30846            1,
30847            P4::Table("idx_col".to_owned()),
30848            1,
30849        );
30850
30851        b.emit_op(Opcode::Blob, 0, r_probe_key, 0, P4::Blob(inserted_key), 0);
30852        b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
30853        b.emit_jump_to_label(Opcode::Found, 0, r_probe_key, found, P4::None, 0);
30854        b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
30855        b.resolve_label(found);
30856        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
30857        b.resolve_label(done);
30858
30859        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
30860        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30861        b.resolve_label(end);
30862
30863        let program = b.finish().expect("program should build");
30864        let rows = execute_program_with_engine(&mut engine, &program);
30865
30866        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
30867    }
30868
30869    #[test]
30870    fn test_count_index_eq_run_consumes_duplicate_run_and_leaves_cursor_on_next_key() {
30871        let mut engine = VdbeEngine::new(8);
30872        install_duplicate_prefix_index_fixture(&mut engine, 0);
30873
30874        let mut b = ProgramBuilder::new();
30875        let end = b.emit_label();
30876        let done = b.emit_label();
30877        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30878
30879        let r_probe = b.alloc_reg();
30880        let r_min_rowid = b.alloc_reg();
30881        let r_seek_key = b.alloc_reg();
30882        let r_count = b.alloc_reg();
30883        let r_next_key = b.alloc_reg();
30884
30885        b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
30886        b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30887        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30888        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30889        b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30890        b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30891        b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
30892        b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
30893        b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
30894
30895        b.resolve_label(done);
30896        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30897        b.resolve_label(end);
30898
30899        let program = b.finish().expect("program should build");
30900        let rows = execute_program_with_engine(&mut engine, &program);
30901
30902        assert_eq!(
30903            rows,
30904            vec![vec![SqliteValue::Integer(128), SqliteValue::Integer(8)]],
30905            "CountIndexEqRun should count the full duplicate run and leave the cursor on the next key"
30906        );
30907    }
30908
30909    #[test]
30910    fn test_count_index_eq_run_uses_cursor_row_decode_scratch() {
30911        let mut engine = VdbeEngine::new(8);
30912        install_duplicate_prefix_index_fixture(&mut engine, 0);
30913
30914        let mut b = ProgramBuilder::new();
30915        let end = b.emit_label();
30916        let done = b.emit_label();
30917        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30918
30919        let r_probe = b.alloc_reg();
30920        let r_min_rowid = b.alloc_reg();
30921        let r_seek_key = b.alloc_reg();
30922        let r_count = b.alloc_reg();
30923        let r_current_key = b.alloc_reg();
30924
30925        b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
30926        b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30927        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30928        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30929        b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30930        b.emit_op(Opcode::Column, 0, 0, r_current_key, P4::None, 0);
30931        b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30932
30933        b.resolve_label(done);
30934        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30935        b.resolve_label(end);
30936
30937        let program = b.finish().expect("program should build");
30938        let _ = execute_program_with_engine(&mut engine, &program);
30939
30940        let cursor = engine
30941            .storage_cursors
30942            .get(&0)
30943            .expect("cursor should still exist");
30944
30945        assert!(
30946            cursor.cur_vals_buf.is_empty(),
30947            "CountIndexEqRun should not populate the legacy full-record decode scratch"
30948        );
30949        assert!(
30950            cursor.row_decode.cached_value_ready(0),
30951            "CountIndexEqRun should leave the first index key cached in row-decode scratch"
30952        );
30953    }
30954
30955    #[test]
30956    fn test_count_index_eq_run_honors_nocase_collation() {
30957        let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30958
30959        let mut b = ProgramBuilder::new();
30960        let end = b.emit_label();
30961        let done = b.emit_label();
30962        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30963
30964        let r_probe = b.alloc_reg();
30965        let r_min_rowid = b.alloc_reg();
30966        let r_seek_key = b.alloc_reg();
30967        let r_count = b.alloc_reg();
30968        let r_next_key = b.alloc_reg();
30969
30970        b.emit_op(
30971            Opcode::String8,
30972            0,
30973            r_probe,
30974            0,
30975            P4::Str("ALPHA".to_owned()),
30976            0,
30977        );
30978        b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30979        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30980        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30981        b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30982        b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30983        b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
30984        b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
30985        b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
30986
30987        b.resolve_label(done);
30988        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30989        b.resolve_label(end);
30990
30991        let program = b.finish().expect("program should build");
30992        let rows = execute_program_with_engine(&mut engine, &program);
30993
30994        assert_eq!(
30995            rows,
30996            vec![vec![
30997                SqliteValue::Integer(2),
30998                SqliteValue::Text("beta".into())
30999            ]],
31000            "CountIndexEqRun must consume a full NOCASE-equivalent run and stop on the next distinct key"
31001        );
31002    }
31003
31004    #[test]
31005    fn test_count_index_eq_run_integer_fast_lane_falls_back_for_float_keys() {
31006        let mut engine = VdbeEngine::new(8);
31007        let mut db = MemDatabase::new();
31008        let index_root = db.allocate_root_page();
31009
31010        engine.enable_storage_cursors(true);
31011        engine.set_database(db);
31012        engine.set_reject_mem_fallback(false);
31013
31014        assert!(
31015            run_async(engine.open_storage_cursor(0, index_root, true)),
31016            "index storage cursor should open"
31017        );
31018
31019        let cursor = engine
31020            .storage_cursors
31021            .get_mut(&0)
31022            .expect("storage cursor should exist");
31023        let key = encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(1)]);
31024        run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31025            .expect("float-key index entry should insert");
31026        assert!(run_async(cursor.cursor.first(&cursor.cx)).expect("cursor first should succeed"));
31027
31028        let matches = run_async(storage_cursor_current_first_index_key_equals(
31029            cursor,
31030            &SqliteValue::Integer(7),
31031            false,
31032            "CountIndexEqRun: malformed index entry record",
31033        ))
31034        .expect("float-key comparison should succeed");
31035
31036        assert!(
31037            matches,
31038            "integer probe should still match a numerically equal float key via generic fallback"
31039        );
31040        assert!(
31041            cursor.row_decode.cached_value_ready(0),
31042            "generic fallback should still decode and cache the float key"
31043        );
31044    }
31045
31046    #[test]
31047    fn test_count_index_eq_run_integer_probe_preserves_float_fallback_through_run_loop() {
31048        let mut engine = VdbeEngine::new(8);
31049        let mut db = MemDatabase::new();
31050        let index_root = db.allocate_root_page();
31051
31052        engine.enable_storage_cursors(true);
31053        engine.set_database(db);
31054        engine.set_reject_mem_fallback(false);
31055
31056        assert!(
31057            run_async(engine.open_storage_cursor(0, index_root, true)),
31058            "index storage cursor should open"
31059        );
31060
31061        let cursor = engine
31062            .storage_cursors
31063            .get_mut(&0)
31064            .expect("storage cursor should exist");
31065        for key in [
31066            encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(1)]),
31067            encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(2)]),
31068            encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(3)]),
31069        ] {
31070            run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31071                .expect("index entry should insert");
31072        }
31073
31074        let mut b = ProgramBuilder::new();
31075        let end = b.emit_label();
31076        let done = b.emit_label();
31077        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31078
31079        let r_probe = b.alloc_reg();
31080        let r_min_rowid = b.alloc_reg();
31081        let r_seek_key = b.alloc_reg();
31082        let r_count = b.alloc_reg();
31083        let r_next_key = b.alloc_reg();
31084
31085        b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
31086        b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
31087        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31088        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31089        b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
31090        b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
31091        b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
31092        b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
31093        b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
31094
31095        b.resolve_label(done);
31096        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31097        b.resolve_label(end);
31098
31099        let program = b.finish().expect("program should build");
31100        let rows = execute_program_with_engine(&mut engine, &program);
31101
31102        assert_eq!(
31103            rows,
31104            vec![vec![SqliteValue::Integer(2), SqliteValue::Integer(8)]],
31105            "integer-probe CountIndexEqRun should still count numerically equal float keys before stopping on the next distinct key"
31106        );
31107    }
31108
31109    #[test]
31110    fn test_count_index_eq_run_local_integer_segment_preserves_float_fallback() {
31111        let mut engine = VdbeEngine::new(8);
31112        let mut db = MemDatabase::new();
31113        let index_root = db.allocate_root_page();
31114
31115        engine.enable_storage_cursors(true);
31116        engine.set_database(db);
31117        engine.set_reject_mem_fallback(false);
31118
31119        assert!(
31120            run_async(engine.open_storage_cursor(0, index_root, true)),
31121            "index storage cursor should open"
31122        );
31123
31124        let cursor = engine
31125            .storage_cursors
31126            .get_mut(&0)
31127            .expect("storage cursor should exist");
31128        for key in [
31129            encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(1)]),
31130            encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(2)]),
31131            encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(3)]),
31132            encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(4)]),
31133        ] {
31134            run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31135                .expect("index entry should insert");
31136        }
31137
31138        let mut b = ProgramBuilder::new();
31139        let end = b.emit_label();
31140        let done = b.emit_label();
31141        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31142
31143        let r_probe = b.alloc_reg();
31144        let r_min_rowid = b.alloc_reg();
31145        let r_seek_key = b.alloc_reg();
31146        let r_count = b.alloc_reg();
31147        let r_next_key = b.alloc_reg();
31148
31149        b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
31150        b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
31151        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31152        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31153        b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
31154        b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
31155        b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
31156        b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
31157        b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
31158
31159        b.resolve_label(done);
31160        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31161        b.resolve_label(end);
31162
31163        let program = b.finish().expect("program should build");
31164        let rows = execute_program_with_engine(&mut engine, &program);
31165
31166        assert_eq!(
31167            rows,
31168            vec![vec![SqliteValue::Integer(3), SqliteValue::Integer(8)]],
31169            "CountIndexEqRun should preserve the float generic fallback after consuming a local integer segment"
31170        );
31171    }
31172
31173    #[test]
31174    fn test_like_const_fast_handles_match_not_and_null() {
31175        let rows = run_program(|b| {
31176            let end = b.emit_label();
31177            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31178
31179            let r_input = b.alloc_reg();
31180            let r_result = b.alloc_reg();
31181
31182            b.emit_op(
31183                Opcode::String8,
31184                0,
31185                r_input,
31186                0,
31187                P4::Str("Document 123".to_owned()),
31188                0,
31189            );
31190            b.emit_op(
31191                Opcode::LikeConstFast,
31192                r_input,
31193                r_result,
31194                SqlLikeFastPathKind::Prefix.opcode_tag(),
31195                P4::Str("Document 1".to_owned()),
31196                0,
31197            );
31198            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31199
31200            b.emit_op(Opcode::String8, 0, r_input, 0, P4::Str("zzz".to_owned()), 0);
31201            b.emit_op(
31202                Opcode::LikeConstFast,
31203                r_input,
31204                r_result,
31205                SqlLikeFastPathKind::Prefix.opcode_tag(),
31206                P4::Str("Document 1".to_owned()),
31207                1,
31208            );
31209            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31210
31211            b.emit_op(Opcode::Null, 0, r_input, 0, P4::None, 0);
31212            b.emit_op(
31213                Opcode::LikeConstFast,
31214                r_input,
31215                r_result,
31216                SqlLikeFastPathKind::Prefix.opcode_tag(),
31217                P4::Str("Document 1".to_owned()),
31218                0,
31219            );
31220            b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31221
31222            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31223            b.resolve_label(end);
31224        });
31225
31226        assert_eq!(
31227            rows,
31228            vec![
31229                vec![SqliteValue::Integer(1)],
31230                vec![SqliteValue::Integer(1)],
31231                vec![SqliteValue::Null],
31232            ],
31233            "LikeConstFast should preserve LIKE match, NOT LIKE inversion, and NULL propagation"
31234        );
31235    }
31236
31237    #[test]
31238    fn test_no_conflict_opcode_reset_for_reuse_reseeds_clean_scratch_state() {
31239        let _guard = VDBE_OBSERVABILITY_LOCK
31240            .lock()
31241            .unwrap_or_else(|e| e.into_inner());
31242        let conflict_key = encode_record(&[SqliteValue::Integer(7)]);
31243        let miss_key = encode_record(&[SqliteValue::Integer(9)]);
31244        let program = build_no_conflict_opcode_probe_program(
31245            conflict_key.clone(),
31246            miss_key.clone(),
31247            vec![0x80],
31248        );
31249        let expected_rows = vec![
31250            vec![SqliteValue::Integer(0)],
31251            vec![SqliteValue::Integer(1)],
31252            vec![SqliteValue::Integer(1)],
31253        ];
31254
31255        let mut engine = VdbeEngine::new(program.register_count());
31256        let (installed_conflict_key, installed_miss_key) =
31257            install_duplicate_prefix_index_fixture(&mut engine, 0);
31258        assert_eq!(installed_conflict_key, conflict_key);
31259        assert_eq!(installed_miss_key, miss_key);
31260
31261        let first_rows = execute_program_with_engine(&mut engine, &program);
31262        assert_eq!(first_rows, expected_rows);
31263
31264        let reset_cx = Cx::new();
31265        engine.reset_for_reuse(program.register_count(), &reset_cx, PageSize::DEFAULT);
31266        assert!(engine.storage_cursors.is_empty());
31267        assert!(engine.results().is_empty());
31268
31269        let (reinstalled_conflict_key, reinstalled_miss_key) =
31270            install_duplicate_prefix_index_fixture(&mut engine, 0);
31271        assert_eq!(reinstalled_conflict_key, conflict_key);
31272        assert_eq!(reinstalled_miss_key, miss_key);
31273        let cursor = engine
31274            .storage_cursors
31275            .get(&0)
31276            .expect("storage cursor should be reinstalled");
31277        assert!(cursor.payload_buf.is_empty());
31278        assert!(cursor.target_vals_buf.is_empty());
31279        assert!(cursor.cur_vals_buf.is_empty());
31280
31281        let second_rows = execute_program_with_engine(&mut engine, &program);
31282        assert_eq!(second_rows, expected_rows);
31283    }
31284
31285    #[test]
31286    fn test_compare_index_prefix_keys_honors_desc_flags() {
31287        let registry = Mutex::new(CollationRegistry::new());
31288        let lhs = vec![SqliteValue::Integer(20), SqliteValue::Integer(2)];
31289        let rhs = vec![SqliteValue::Integer(10), SqliteValue::Integer(1)];
31290        let coll_guard = registry.lock().unwrap();
31291
31292        assert_eq!(
31293            compare_index_prefix_keys(&lhs, &rhs, 1, &[true], &[], &coll_guard),
31294            Ordering::Less,
31295            "descending index comparison should treat the larger key as earlier"
31296        );
31297        assert_eq!(
31298            compare_index_prefix_keys(&lhs, &rhs, 1, &[false], &[], &coll_guard),
31299            Ordering::Greater,
31300            "ascending index comparison should keep natural integer ordering"
31301        );
31302        assert_eq!(
31303            compare_index_prefix_keys(&lhs, &rhs, 0, &[true], &[], &coll_guard),
31304            Ordering::Less,
31305            "zero key_columns should still compare the leading term"
31306        );
31307    }
31308
31309    #[test]
31310    fn test_storage_cursor_first_index_key_compare_honors_desc_flag() {
31311        let mut engine = VdbeEngine::new(8);
31312        let mut db = MemDatabase::new();
31313        let index_root = db.allocate_root_page();
31314
31315        engine.enable_storage_cursors(true);
31316        engine.set_database(db);
31317        engine.set_reject_mem_fallback(false);
31318        engine.set_index_desc_flags_by_root_page(HashMap::from([(index_root, vec![true])]));
31319
31320        assert!(
31321            run_async(engine.open_storage_cursor(0, index_root, true)),
31322            "descending index storage cursor should open"
31323        );
31324
31325        let cursor = engine
31326            .storage_cursors
31327            .get_mut(&0)
31328            .expect("storage cursor should exist");
31329        for key in [
31330            encode_record(&[SqliteValue::Integer(10), SqliteValue::Integer(1)]),
31331            encode_record(&[SqliteValue::Integer(20), SqliteValue::Integer(2)]),
31332        ] {
31333            run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31334                .expect("descending index key should insert");
31335        }
31336        assert!(
31337            run_async(cursor.cursor.first(&cursor.cx)).expect("first should work"),
31338            "descending index should contain entries"
31339        );
31340
31341        let registry = Arc::clone(&engine.collation_registry);
31342        let coll_guard = registry.lock().unwrap_or_else(|err| err.into_inner());
31343        let cmp = run_async(storage_cursor_current_first_index_key_compare(
31344            cursor,
31345            &SqliteValue::Integer(10),
31346            false,
31347            "IdxCmp: malformed index record at cursor position",
31348            true,
31349            None,
31350            &coll_guard,
31351        ))
31352        .expect("first-key compare should succeed");
31353
31354        assert_eq!(
31355            cmp,
31356            Ordering::Less,
31357            "larger integer keys sort before smaller keys in a DESC index"
31358        );
31359    }
31360
31361    #[test]
31362    fn test_idxgt_single_column_prefix_ignores_trailing_rowid() {
31363        let mut engine = VdbeEngine::new(8);
31364        let mut db = MemDatabase::new();
31365        let index_root = db.allocate_root_page();
31366
31367        engine.enable_storage_cursors(true);
31368        engine.set_database(db);
31369        engine.set_reject_mem_fallback(false);
31370
31371        assert!(
31372            run_async(engine.open_storage_cursor(0, index_root, true)),
31373            "index storage cursor should open"
31374        );
31375
31376        let cursor = engine
31377            .storage_cursors
31378            .get_mut(&0)
31379            .expect("storage cursor should exist");
31380        for key in [
31381            encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(1)]),
31382            encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(2)]),
31383            encode_record(&[SqliteValue::Text("beta".into()), SqliteValue::Integer(3)]),
31384        ] {
31385            run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31386                .expect("index key should insert");
31387        }
31388
31389        let mut b = ProgramBuilder::new();
31390        let done = b.emit_label();
31391        let end = b.emit_label();
31392        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31393
31394        let r_probe = b.alloc_reg();
31395        let r_low_rowid = b.alloc_reg();
31396        let r_seek_key = b.alloc_reg();
31397        let r_out = b.alloc_reg();
31398
31399        b.emit_op(
31400            Opcode::String8,
31401            0,
31402            r_probe,
31403            0,
31404            P4::Str("alpha".to_owned()),
31405            0,
31406        );
31407        b.emit_op(Opcode::Int64, 0, r_low_rowid, 0, P4::Int64(i64::MIN), 0);
31408        b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31409        b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31410        b.emit_jump_to_label(Opcode::IdxGT, 0, r_seek_key, done, P4::None, 1);
31411        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
31412        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
31413
31414        b.resolve_label(done);
31415        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31416        b.resolve_label(end);
31417
31418        let program = b.finish().expect("program should build");
31419        let rows = execute_program_with_engine(&mut engine, &program);
31420
31421        assert_eq!(
31422            rows,
31423            vec![vec![SqliteValue::Integer(1)]],
31424            "IdxGT with p5=1 must compare only the equality prefix, not the rowid suffix"
31425        );
31426    }
31427
31428    #[test]
31429    fn test_distinct_key_collated_honors_rtrim() {
31430        let base = distinct_key_collated(&[SqliteValue::Text("abc".into())], Some("RTRIM"));
31431        let padded = distinct_key_collated(&[SqliteValue::Text("abc  ".into())], Some("rtrim"));
31432        let tabbed = distinct_key_collated(&[SqliteValue::Text("abc\t".into())], Some("RTRIM"));
31433
31434        assert_eq!(
31435            base, padded,
31436            "RTRIM DISTINCT keys must ignore trailing ASCII spaces"
31437        );
31438        assert_ne!(
31439            base, tabbed,
31440            "RTRIM DISTINCT keys must not trim non-space suffixes like tabs"
31441        );
31442    }
31443
31444    #[test]
31445    fn test_open_storage_cursor_write_init_failure_does_not_fallback_to_mem() {
31446        use fsqlite_mvcc::{ConcurrentRegistry, InProcessPageLockTable};
31447        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31448
31449        let pager = MockMvccPager;
31450        let cx = Cx::new();
31451        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31452
31453        let mut engine = VdbeEngine::new(8);
31454        // Use a page number whose low byte is 0 so MockTransaction::get_page
31455        // returns a zero type byte, forcing the writable root-page init path.
31456        let root = 256;
31457
31458        // Deliberately install concurrent context with an inactive handle.
31459        // SharedTxnPageIo::write_page will fail before touching pager state.
31460        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
31461        let lock_table = Arc::new(InProcessPageLockTable::new());
31462        let commit_index = Arc::new(CommitIndex::new());
31463        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
31464        let (session_id, handle) = {
31465            let mut guard = registry
31466                .lock()
31467                .unwrap_or_else(std::sync::PoisonError::into_inner);
31468            let session_id = guard
31469                .begin_concurrent(snapshot)
31470                .expect("session should register");
31471            let handle = guard.handle(session_id).expect("handle should exist");
31472            (session_id, handle)
31473        };
31474        handle.lock().mark_aborted();
31475        engine.set_transaction_concurrent(txn, session_id, handle, lock_table, commit_index, 5000);
31476
31477        let opened = run_async(engine.open_storage_cursor(0, root, true));
31478        assert!(
31479            !opened,
31480            "write-init errors must fail cursor open instead of silently falling back to Mem"
31481        );
31482        assert!(
31483            !engine.storage_cursors.contains_key(&0),
31484            "failed open must not leave a cursor installed"
31485        );
31486    }
31487
31488    #[test]
31489    fn test_open_storage_cursor_write_read_failure_does_not_fallback_to_mem() {
31490        use std::path::PathBuf;
31491
31492        use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
31493        use fsqlite_vfs::MemoryVfs;
31494
31495        let vfs = MemoryVfs::new();
31496        let path = PathBuf::from("/vdbe_write_read_failure_no_mem_fallback.db");
31497        let cx = Cx::new();
31498        let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
31499        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31500
31501        let mut db = MemDatabase::new();
31502        let root = 2;
31503        db.create_table_at(root, 1);
31504
31505        let mut engine = VdbeEngine::new(8);
31506        engine.set_database(db);
31507        engine.set_transaction(txn);
31508        engine.set_reject_mem_fallback(false);
31509
31510        let opened = run_async(engine.open_storage_cursor(0, root, true));
31511        assert!(
31512            !opened,
31513            "writable cursor opens must fail when pager reads error instead of falling back to Mem"
31514        );
31515        assert!(
31516            !engine.storage_cursors.contains_key(&0),
31517            "failed open must not leave a cursor installed"
31518        );
31519    }
31520
31521    #[test]
31522    fn test_open_storage_cursor_falls_back_to_mem_without_txn() {
31523        let mut db = MemDatabase::new();
31524        let root = db.create_table(1);
31525        db.get_table_mut(root)
31526            .unwrap()
31527            .insert(1, vec![SqliteValue::Integer(100)]);
31528
31529        let mut engine = VdbeEngine::new(8);
31530        engine.enable_storage_cursors(true);
31531        engine.set_database(db);
31532        engine.set_reject_mem_fallback(false);
31533
31534        // Without a transaction, should fall back to Mem backend.
31535        let opened = run_async(engine.open_storage_cursor(0, root, false));
31536        assert!(opened);
31537        assert!(engine.storage_cursors.contains_key(&0));
31538    }
31539
31540    #[test]
31541    fn test_open_storage_cursor_zero_page_with_txn_does_not_fallback_to_mem() {
31542        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31543
31544        let pager = MockMvccPager;
31545        let cx = Cx::new();
31546        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31547
31548        let mut engine = VdbeEngine::new(8);
31549        engine.set_transaction(txn);
31550
31551        // MockTransaction synthesizes page bytes from the page number; page 256
31552        // yields first byte 0x00, simulating an uninitialized root page.
31553        let opened = run_async(engine.open_storage_cursor(0, 256, false));
31554        assert!(
31555            !opened,
31556            "transaction-backed opens must not silently fall back to MemPageStore"
31557        );
31558        assert!(
31559            !engine.storage_cursors.contains_key(&0),
31560            "failed open must not leave a cursor installed"
31561        );
31562    }
31563
31564    #[test]
31565    fn test_open_storage_cursor_write_invalid_page_does_not_fallback_to_mem() {
31566        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31567
31568        let pager = MockMvccPager;
31569        let cx = Cx::new();
31570        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31571
31572        let mut db = MemDatabase::new();
31573        let root = 256;
31574        db.create_table_at(root, 1);
31575
31576        let mut engine = VdbeEngine::new(8);
31577        engine.set_database(db);
31578        engine.set_transaction(txn);
31579        engine.set_reject_mem_fallback(false);
31580
31581        let opened = run_async(engine.open_storage_cursor(0, root, true));
31582        assert!(
31583            !opened,
31584            "writable cursor opens must fail on invalid pager pages instead of falling back to Mem"
31585        );
31586        assert!(
31587            !engine.storage_cursors.contains_key(&0),
31588            "failed open must not leave a cursor installed"
31589        );
31590    }
31591
31592    #[test]
31593    fn test_txn_cursor_open_close_lifecycle() {
31594        // Verify the TransactionPageIo cursor lifecycle:
31595        // set_transaction → open cursor → close cursor → take_transaction.
31596        // MockTransaction doesn't produce valid B-tree pages, so we don't
31597        // attempt navigation — that's tested via MemPageStore-backed tests.
31598        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31599
31600        let pager = MockMvccPager;
31601        let cx = Cx::new();
31602        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31603
31604        let mut db = MemDatabase::new();
31605        let root = db.create_table(1);
31606
31607        let mut b = ProgramBuilder::new();
31608        let end = b.emit_label();
31609        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31610        // Open a read cursor — this creates a CursorBackend::Txn.
31611        b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31612        // Close the cursor immediately without navigation.
31613        b.emit_op(Opcode::Close, 0, 0, 0, P4::None, 0);
31614        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31615        b.resolve_label(end);
31616        let prog = b.finish().expect("program should build");
31617
31618        let mut engine = VdbeEngine::new(prog.register_count());
31619        engine.set_database(db);
31620        engine.set_transaction(txn);
31621
31622        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
31623        assert_eq!(outcome, ExecOutcome::Done);
31624
31625        // Verify transaction recovery after cursor lifecycle.
31626        engine.storage_cursors.clear();
31627        assert!(
31628            engine
31629                .take_transaction()
31630                .expect("take_transaction should succeed")
31631                .is_some()
31632        );
31633    }
31634
31635    // ── bd-3pti: Seek opcode tests ───────────────────────────────────────
31636
31637    #[test]
31638    fn test_seek_ge_with_storage_cursor() {
31639        // SeekGE(key=5): should position at first row with rowid >= 5.
31640        // Table has rows: 3, 5, 7, 9
31641        let mut db = MemDatabase::new();
31642        let root = db.create_table(1);
31643        let table = db.get_table_mut(root).unwrap();
31644        table.insert(3, vec![SqliteValue::Integer(30)]);
31645        table.insert(5, vec![SqliteValue::Integer(50)]);
31646        table.insert(7, vec![SqliteValue::Integer(70)]);
31647        table.insert(9, vec![SqliteValue::Integer(90)]);
31648
31649        let rows = run_with_storage_cursors(db, |b| {
31650            let end = b.emit_label();
31651            let not_found = b.emit_label();
31652            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31653            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31654
31655            // Seek to rowid >= 5 (should land on rowid 5)
31656            b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31657            b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31658
31659            // Read the column value at current position.
31660            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31661            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31662
31663            b.resolve_label(not_found);
31664            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31665            b.resolve_label(end);
31666        });
31667
31668        assert_eq!(rows.len(), 1);
31669        assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); // rowid 5, value 50
31670    }
31671
31672    #[test]
31673    fn test_seek_ge_not_exact_match() {
31674        // SeekGE(key=4): should position at first row with rowid >= 4.
31675        // Table has rows: 3, 5, 7, 9 → should land on rowid 5
31676        let mut db = MemDatabase::new();
31677        let root = db.create_table(1);
31678        let table = db.get_table_mut(root).unwrap();
31679        table.insert(3, vec![SqliteValue::Integer(30)]);
31680        table.insert(5, vec![SqliteValue::Integer(50)]);
31681        table.insert(7, vec![SqliteValue::Integer(70)]);
31682        table.insert(9, vec![SqliteValue::Integer(90)]);
31683
31684        let rows = run_with_storage_cursors(db, |b| {
31685            let end = b.emit_label();
31686            let not_found = b.emit_label();
31687            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31688            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31689
31690            // Seek to rowid >= 4 (should land on rowid 5, the next larger)
31691            b.emit_op(Opcode::Integer, 4, 1, 0, P4::None, 0);
31692            b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31693
31694            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31695            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31696
31697            b.resolve_label(not_found);
31698            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31699            b.resolve_label(end);
31700        });
31701
31702        assert_eq!(rows.len(), 1);
31703        assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); // rowid 5, value 50
31704    }
31705
31706    #[test]
31707    fn test_seek_gt_with_storage_cursor() {
31708        // SeekGT(key=5): should position at first row with rowid > 5.
31709        // Table has rows: 3, 5, 7, 9 → should land on rowid 7
31710        let mut db = MemDatabase::new();
31711        let root = db.create_table(1);
31712        let table = db.get_table_mut(root).unwrap();
31713        table.insert(3, vec![SqliteValue::Integer(30)]);
31714        table.insert(5, vec![SqliteValue::Integer(50)]);
31715        table.insert(7, vec![SqliteValue::Integer(70)]);
31716        table.insert(9, vec![SqliteValue::Integer(90)]);
31717
31718        let rows = run_with_storage_cursors(db, |b| {
31719            let end = b.emit_label();
31720            let not_found = b.emit_label();
31721            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31722            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31723
31724            // Seek to rowid > 5 (should land on rowid 7)
31725            b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31726            b.emit_jump_to_label(Opcode::SeekGT, 0, 1, not_found, P4::None, 0);
31727
31728            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31729            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31730
31731            b.resolve_label(not_found);
31732            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31733            b.resolve_label(end);
31734        });
31735
31736        assert_eq!(rows.len(), 1);
31737        assert_eq!(rows[0], vec![SqliteValue::Integer(70)]); // rowid 7, value 70
31738    }
31739
31740    #[test]
31741    fn test_seek_le_with_storage_cursor() {
31742        // SeekLE(key=5): should position at last row with rowid <= 5.
31743        // Table has rows: 3, 5, 7, 9 → should land on rowid 5
31744        let mut db = MemDatabase::new();
31745        let root = db.create_table(1);
31746        let table = db.get_table_mut(root).unwrap();
31747        table.insert(3, vec![SqliteValue::Integer(30)]);
31748        table.insert(5, vec![SqliteValue::Integer(50)]);
31749        table.insert(7, vec![SqliteValue::Integer(70)]);
31750        table.insert(9, vec![SqliteValue::Integer(90)]);
31751
31752        let rows = run_with_storage_cursors(db, |b| {
31753            let end = b.emit_label();
31754            let not_found = b.emit_label();
31755            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31756            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31757
31758            // Seek to rowid <= 5 (should land on rowid 5)
31759            b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31760            b.emit_jump_to_label(Opcode::SeekLE, 0, 1, not_found, P4::None, 0);
31761
31762            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31763            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31764
31765            b.resolve_label(not_found);
31766            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31767            b.resolve_label(end);
31768        });
31769
31770        assert_eq!(rows.len(), 1);
31771        assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); // rowid 5, value 50
31772    }
31773
31774    #[test]
31775    fn test_seek_le_not_exact_match() {
31776        // SeekLE(key=6): should position at last row with rowid <= 6.
31777        // Table has rows: 3, 5, 7, 9 → should land on rowid 5
31778        let mut db = MemDatabase::new();
31779        let root = db.create_table(1);
31780        let table = db.get_table_mut(root).unwrap();
31781        table.insert(3, vec![SqliteValue::Integer(30)]);
31782        table.insert(5, vec![SqliteValue::Integer(50)]);
31783        table.insert(7, vec![SqliteValue::Integer(70)]);
31784        table.insert(9, vec![SqliteValue::Integer(90)]);
31785
31786        let rows = run_with_storage_cursors(db, |b| {
31787            let end = b.emit_label();
31788            let not_found = b.emit_label();
31789            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31790            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31791
31792            // Seek to rowid <= 6 (should land on rowid 5)
31793            b.emit_op(Opcode::Integer, 6, 1, 0, P4::None, 0);
31794            b.emit_jump_to_label(Opcode::SeekLE, 0, 1, not_found, P4::None, 0);
31795
31796            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31797            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31798
31799            b.resolve_label(not_found);
31800            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31801            b.resolve_label(end);
31802        });
31803
31804        assert_eq!(rows.len(), 1);
31805        assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); // rowid 5, value 50
31806    }
31807
31808    #[test]
31809    fn test_seek_lt_with_storage_cursor() {
31810        // SeekLT(key=5): should position at last row with rowid < 5.
31811        // Table has rows: 3, 5, 7, 9 → should land on rowid 3
31812        let mut db = MemDatabase::new();
31813        let root = db.create_table(1);
31814        let table = db.get_table_mut(root).unwrap();
31815        table.insert(3, vec![SqliteValue::Integer(30)]);
31816        table.insert(5, vec![SqliteValue::Integer(50)]);
31817        table.insert(7, vec![SqliteValue::Integer(70)]);
31818        table.insert(9, vec![SqliteValue::Integer(90)]);
31819
31820        let rows = run_with_storage_cursors(db, |b| {
31821            let end = b.emit_label();
31822            let not_found = b.emit_label();
31823            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31824            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31825
31826            // Seek to rowid < 5 (should land on rowid 3)
31827            b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31828            b.emit_jump_to_label(Opcode::SeekLT, 0, 1, not_found, P4::None, 0);
31829
31830            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31831            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31832
31833            b.resolve_label(not_found);
31834            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31835            b.resolve_label(end);
31836        });
31837
31838        assert_eq!(rows.len(), 1);
31839        assert_eq!(rows[0], vec![SqliteValue::Integer(30)]); // rowid 3, value 30
31840    }
31841
31842    #[test]
31843    fn test_seek_ge_empty_table_jumps() {
31844        // SeekGE on empty table should jump to p2.
31845        let mut db = MemDatabase::new();
31846        let root = db.create_table(1);
31847        // Table is empty.
31848
31849        let rows = run_with_storage_cursors(db, |b| {
31850            let end = b.emit_label();
31851            let not_found = b.emit_label();
31852            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31853            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31854
31855            b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31856            b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31857
31858            // This should NOT be reached.
31859            b.emit_op(Opcode::Integer, 999, 2, 0, P4::None, 0);
31860            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31861
31862            b.resolve_label(not_found);
31863            // Jump target - we output nothing to indicate the jump was taken.
31864            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31865            b.resolve_label(end);
31866        });
31867
31868        // Empty table → no rows returned, jump to p2.
31869        assert_eq!(rows.len(), 0);
31870    }
31871
31872    #[test]
31873    fn test_seek_lt_no_smaller_row_jumps() {
31874        // SeekLT(key=3) when smallest rowid is 3 should jump to p2.
31875        let mut db = MemDatabase::new();
31876        let root = db.create_table(1);
31877        let table = db.get_table_mut(root).unwrap();
31878        table.insert(3, vec![SqliteValue::Integer(30)]);
31879        table.insert(5, vec![SqliteValue::Integer(50)]);
31880
31881        let rows = run_with_storage_cursors(db, |b| {
31882            let end = b.emit_label();
31883            let not_found = b.emit_label();
31884            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31885            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31886
31887            // Seek to rowid < 3 (no such row → should jump)
31888            b.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
31889            b.emit_jump_to_label(Opcode::SeekLT, 0, 1, not_found, P4::None, 0);
31890
31891            // This should NOT be reached.
31892            b.emit_op(Opcode::Integer, 999, 2, 0, P4::None, 0);
31893            b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31894
31895            b.resolve_label(not_found);
31896            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31897            b.resolve_label(end);
31898        });
31899
31900        // No row < 3 → jump taken, no results.
31901        assert_eq!(rows.len(), 0);
31902    }
31903
31904    // ── Swiss Tables (bd-3ta.7) integration tests ─────────────────────
31905
31906    #[test]
31907    fn test_swiss_index_metrics_emitted_on_cursor_ops() {
31908        // Verify that SwissIndex operations work correctly through the
31909        // MemDatabase interface.  Probe metrics are only recorded when
31910        // TRACE-level tracing is enabled (cold path), so we only assert
31911        // functional correctness here.
31912        let mut db = MemDatabase::new();
31913        let root = db.create_table(2);
31914        assert!(root > 0, "create_table should return a valid root page");
31915
31916        // Insert a row via MemDatabase to exercise SwissIndex lookups.
31917        db.get_table_mut(root).unwrap().insert_row(
31918            1,
31919            vec![SqliteValue::Integer(42), SqliteValue::Text("a".into())],
31920        );
31921        let table = db.get_table(root).unwrap();
31922        assert_eq!(table.rows.len(), 1, "table should contain the inserted row");
31923    }
31924
31925    #[test]
31926    fn test_swiss_index_replaces_hashmap_in_engine() {
31927        // Smoke test: run a simple expression program to exercise the engine's
31928        // SwissIndex-based internal maps (sorters, cursors, aggregates).
31929        use fsqlite_btree::instrumentation::reset_btree_metrics;
31930
31931        reset_btree_metrics();
31932
31933        // Even a simple expression program exercises VdbeEngine construction
31934        // which initializes SwissIndex maps. The metrics counter is global, so
31935        // any cursor open/close in the test suite contributes.
31936        let rows = run_program(|b| {
31937            let end = b.emit_label();
31938            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31939            b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
31940            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
31941            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31942            b.resolve_label(end);
31943        });
31944
31945        assert_eq!(rows.len(), 1);
31946        assert_eq!(rows[0][0], SqliteValue::Integer(42));
31947
31948        // The engine's internal SwissIndex maps (cursors, sorters, aggregates,
31949        // storage_cursors) are all SwissIndex now. If we got here without
31950        // panics, the drop-in replacement works.
31951    }
31952
31953    // ── External Sort Tests (bd-1rw.4) ──────────────────────────────────
31954
31955    /// Mutex to serialize tests that mutate global VDBE observability settings.
31956    ///
31957    /// JIT and metrics configuration are both process-global, so tests that
31958    /// toggle them must not run concurrently.
31959    static VDBE_OBSERVABILITY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
31960
31961    fn run_sorter_metric_program() -> Vec<Vec<SqliteValue>> {
31962        run_program(|b| {
31963            let end = b.emit_label();
31964            let loop_start = b.emit_label();
31965            let empty = b.emit_label();
31966            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31967
31968            let r_value = b.alloc_reg();
31969            let r_record = b.alloc_reg();
31970            let r_sorted = b.alloc_reg();
31971
31972            b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
31973
31974            for value in [50, 30, 10, 40, 20] {
31975                b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
31976                b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
31977                b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
31978            }
31979
31980            b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
31981            b.resolve_label(loop_start);
31982            b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
31983            b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
31984            b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
31985            b.resolve_label(empty);
31986            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31987            b.resolve_label(end);
31988        })
31989    }
31990
31991    #[test]
31992    fn test_sort_metrics_emitted_on_sorter_sort() {
31993        let _guard = VDBE_OBSERVABILITY_LOCK
31994            .lock()
31995            .unwrap_or_else(|e| e.into_inner());
31996        let prev_metrics_enabled = vdbe_metrics_enabled();
31997        set_vdbe_metrics_enabled(true);
31998        // Verify that sort row metrics are updated when a sorter sorts rows.
31999        // Delta-based: snapshot before/after to avoid racing with parallel tests.
32000        let before = vdbe_metrics_snapshot();
32001        let rows = run_sorter_metric_program();
32002
32003        assert_eq!(rows.len(), 5);
32004        let after = vdbe_metrics_snapshot();
32005        let delta_sort_rows = after.sort_rows_total - before.sort_rows_total;
32006        let delta_spill = after.sort_spill_pages_total - before.sort_spill_pages_total;
32007        assert!(
32008            delta_sort_rows >= 5,
32009            "sort_rows_total delta should be >= 5, got {delta_sort_rows}",
32010        );
32011        // No spill expected for 5 tiny rows.
32012        assert_eq!(delta_spill, 0, "no spill expected for small dataset");
32013        set_vdbe_metrics_enabled(prev_metrics_enabled);
32014    }
32015
32016    #[test]
32017    fn test_vdbe_metrics_can_be_disabled_off_hot_path() {
32018        let _guard = VDBE_OBSERVABILITY_LOCK
32019            .lock()
32020            .unwrap_or_else(|e| e.into_inner());
32021        let prev_metrics_enabled = vdbe_metrics_enabled();
32022        reset_vdbe_metrics();
32023        set_vdbe_metrics_enabled(false);
32024
32025        let before = vdbe_metrics_snapshot();
32026        let rows = run_sorter_metric_program();
32027        let after = vdbe_metrics_snapshot();
32028
32029        assert_eq!(rows.len(), 5);
32030        assert_eq!(after.opcodes_executed_total, before.opcodes_executed_total);
32031        assert_eq!(after.statements_total, before.statements_total);
32032        assert_eq!(
32033            after.statement_duration_us_total,
32034            before.statement_duration_us_total
32035        );
32036        assert_eq!(after.sort_rows_total, before.sort_rows_total);
32037        assert_eq!(after.sort_spill_pages_total, before.sort_spill_pages_total);
32038
32039        set_vdbe_metrics_enabled(prev_metrics_enabled);
32040    }
32041
32042    #[test]
32043    fn test_decode_record_with_metrics_flag_bypasses_global_reload() {
32044        let _guard = VDBE_OBSERVABILITY_LOCK
32045            .lock()
32046            .unwrap_or_else(|e| e.into_inner());
32047        let prev_metrics_enabled = vdbe_metrics_enabled();
32048        reset_vdbe_metrics();
32049        set_vdbe_metrics_enabled(false);
32050
32051        let record = SqliteValue::Blob(
32052            encode_record(&[SqliteValue::Integer(7), SqliteValue::Text("hello".into())]).into(),
32053        );
32054
32055        let before = vdbe_metrics_snapshot();
32056        let decoded_without_metrics =
32057            decode_record_with_metrics(&record, false).expect("record should decode");
32058        let after_without_metrics = vdbe_metrics_snapshot();
32059        assert_eq!(
32060            decoded_without_metrics,
32061            vec![SqliteValue::Integer(7), SqliteValue::Text("hello".into())]
32062        );
32063        assert_eq!(
32064            after_without_metrics.record_decode_calls_total,
32065            before.record_decode_calls_total
32066        );
32067        assert_eq!(
32068            after_without_metrics.decoded_values_total,
32069            before.decoded_values_total
32070        );
32071
32072        let decoded_with_metrics =
32073            decode_record_with_metrics(&record, true).expect("record should decode");
32074        let after_with_metrics = vdbe_metrics_snapshot();
32075        assert_eq!(decoded_with_metrics, decoded_without_metrics);
32076        assert_eq!(
32077            after_with_metrics.record_decode_calls_total,
32078            after_without_metrics.record_decode_calls_total + 1
32079        );
32080        assert_eq!(
32081            after_with_metrics.decoded_values_total,
32082            after_without_metrics.decoded_values_total + 2
32083        );
32084
32085        set_vdbe_metrics_enabled(prev_metrics_enabled);
32086    }
32087
32088    #[test]
32089    fn test_storage_cursor_decode_cache_metrics_track_hits_misses_and_position_changes() {
32090        let _guard = VDBE_OBSERVABILITY_LOCK
32091            .lock()
32092            .unwrap_or_else(|e| e.into_inner());
32093        let prev_metrics_enabled = vdbe_metrics_enabled();
32094        reset_vdbe_metrics();
32095        set_vdbe_metrics_enabled(true);
32096
32097        let mut db = MemDatabase::new();
32098        let root = db.create_table(1);
32099        let table = db.get_table_mut(root).expect("table should exist");
32100        table.insert(1, vec![SqliteValue::Integer(10)]);
32101        table.insert(2, vec![SqliteValue::Integer(20)]);
32102
32103        let before = vdbe_metrics_snapshot();
32104        let rows = run_with_storage_cursors(db, |b| {
32105            let end = b.emit_label();
32106            let eof = b.emit_label();
32107            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32108            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
32109            b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32110
32111            let body = b.current_addr();
32112            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
32113            b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
32114            b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32115
32116            let next_target =
32117                i32::try_from(body).expect("program counter should fit into i32 for tests");
32118            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
32119            b.resolve_label(eof);
32120            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32121            b.resolve_label(end);
32122        });
32123        let after = vdbe_metrics_snapshot();
32124
32125        assert_eq!(
32126            rows,
32127            vec![
32128                vec![SqliteValue::Integer(10), SqliteValue::Integer(10)],
32129                vec![SqliteValue::Integer(20), SqliteValue::Integer(20)],
32130            ]
32131        );
32132        assert_eq!(
32133            after.decode_cache_hits_total - before.decode_cache_hits_total,
32134            2
32135        );
32136        assert_eq!(
32137            after.decode_cache_misses_total - before.decode_cache_misses_total,
32138            2
32139        );
32140        assert_eq!(
32141            after.decode_cache_invalidations_position_total
32142                - before.decode_cache_invalidations_position_total,
32143            1
32144        );
32145        assert_eq!(
32146            after.decode_cache_invalidations_write_total
32147                - before.decode_cache_invalidations_write_total,
32148            0
32149        );
32150        assert_eq!(
32151            after.decode_cache_invalidations_pseudo_total
32152                - before.decode_cache_invalidations_pseudo_total,
32153            0
32154        );
32155
32156        set_vdbe_metrics_enabled(prev_metrics_enabled);
32157    }
32158
32159    #[test]
32160    fn test_pseudo_cursor_decode_cache_metrics_track_hits_and_blob_changes() {
32161        let _guard = VDBE_OBSERVABILITY_LOCK
32162            .lock()
32163            .unwrap_or_else(|e| e.into_inner());
32164        let prev_metrics_enabled = vdbe_metrics_enabled();
32165        reset_vdbe_metrics();
32166        set_vdbe_metrics_enabled(true);
32167
32168        let mut engine = VdbeEngine::new(2);
32169        engine.collect_vdbe_metrics = true;
32170        engine.cursors.insert(0, MemCursor::new_pseudo(1));
32171
32172        engine.set_reg_fast(
32173            1,
32174            SqliteValue::Blob(
32175                encode_record(&[SqliteValue::Integer(7), SqliteValue::Text("alpha".into())]).into(),
32176            ),
32177        );
32178
32179        let before = vdbe_metrics_snapshot();
32180        assert_eq!(
32181            run_async(engine.cursor_column(0, 0)).expect("pseudo row should decode"),
32182            SqliteValue::Integer(7)
32183        );
32184        assert_eq!(
32185            run_async(engine.cursor_column(0, 0)).expect("pseudo row cache should hit"),
32186            SqliteValue::Integer(7)
32187        );
32188
32189        engine.set_reg_fast(
32190            1,
32191            SqliteValue::Blob(
32192                encode_record(&[SqliteValue::Integer(9), SqliteValue::Text("beta".into())]).into(),
32193            ),
32194        );
32195        assert_eq!(
32196            run_async(engine.cursor_column(0, 0)).expect("changed pseudo row should decode"),
32197            SqliteValue::Integer(9)
32198        );
32199        let after = vdbe_metrics_snapshot();
32200
32201        assert_eq!(
32202            after.decode_cache_hits_total - before.decode_cache_hits_total,
32203            1
32204        );
32205        assert_eq!(
32206            after.decode_cache_misses_total - before.decode_cache_misses_total,
32207            2
32208        );
32209        assert_eq!(
32210            after.decode_cache_invalidations_pseudo_total
32211                - before.decode_cache_invalidations_pseudo_total,
32212            1
32213        );
32214        assert_eq!(
32215            after.decode_cache_invalidations_position_total
32216                - before.decode_cache_invalidations_position_total,
32217            0
32218        );
32219        assert_eq!(
32220            after.decode_cache_invalidations_write_total
32221                - before.decode_cache_invalidations_write_total,
32222            0
32223        );
32224
32225        set_vdbe_metrics_enabled(prev_metrics_enabled);
32226    }
32227
32228    #[test]
32229    fn test_sorter_decode_cache_metrics_track_hits_misses_and_position_changes() {
32230        let _guard = VDBE_OBSERVABILITY_LOCK
32231            .lock()
32232            .unwrap_or_else(|e| e.into_inner());
32233        let prev_metrics_enabled = vdbe_metrics_enabled();
32234        reset_vdbe_metrics();
32235        set_vdbe_metrics_enabled(true);
32236
32237        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32238        sorter
32239            .insert_row(
32240                vec![SqliteValue::Integer(1)],
32241                encode_record(&[SqliteValue::Integer(1), SqliteValue::Text("alpha".into())]),
32242            )
32243            .expect("sorter insert should succeed");
32244        sorter
32245            .insert_row(
32246                vec![SqliteValue::Integer(2)],
32247                encode_record(&[SqliteValue::Integer(2), SqliteValue::Text("beta".into())]),
32248            )
32249            .expect("sorter insert should succeed");
32250        sorter.sort().expect("sorter sort should succeed");
32251        sorter.position = Some(0);
32252
32253        let mut engine = VdbeEngine::new(1);
32254        engine.collect_vdbe_metrics = true;
32255        engine.sorters.insert(0, sorter);
32256
32257        let before = vdbe_metrics_snapshot();
32258        assert_eq!(
32259            run_async(engine.cursor_column(0, 1)).expect("first sorter decode should succeed"),
32260            SqliteValue::Text("alpha".into())
32261        );
32262        assert_eq!(
32263            run_async(engine.cursor_column(0, 1)).expect("second sorter read should hit cache"),
32264            SqliteValue::Text("alpha".into())
32265        );
32266        engine
32267            .sorters
32268            .get_mut(&0)
32269            .expect("sorter cursor should exist")
32270            .position = Some(1);
32271        assert_eq!(
32272            run_async(engine.cursor_column(0, 1)).expect("next sorter row should decode"),
32273            SqliteValue::Text("beta".into())
32274        );
32275        let after = vdbe_metrics_snapshot();
32276
32277        assert_eq!(
32278            after.decode_cache_hits_total - before.decode_cache_hits_total,
32279            1
32280        );
32281        assert_eq!(
32282            after.decode_cache_misses_total - before.decode_cache_misses_total,
32283            2
32284        );
32285        assert_eq!(
32286            after.decode_cache_invalidations_position_total
32287                - before.decode_cache_invalidations_position_total,
32288            1
32289        );
32290        assert_eq!(
32291            after.decode_cache_invalidations_write_total
32292                - before.decode_cache_invalidations_write_total,
32293            0
32294        );
32295        assert_eq!(
32296            after.decode_cache_invalidations_pseudo_total
32297                - before.decode_cache_invalidations_pseudo_total,
32298            0
32299        );
32300
32301        set_vdbe_metrics_enabled(prev_metrics_enabled);
32302    }
32303
32304    #[test]
32305    fn test_storage_cursor_decode_cache_hits_wide_row_tail_column_after_eager_decode() {
32306        let _guard = VDBE_OBSERVABILITY_LOCK
32307            .lock()
32308            .unwrap_or_else(|e| e.into_inner());
32309        let prev_metrics_enabled = vdbe_metrics_enabled();
32310        reset_vdbe_metrics();
32311        set_vdbe_metrics_enabled(true);
32312
32313        let mut db = MemDatabase::new();
32314        let root = db.create_table(65);
32315        let row = (0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>();
32316        db.get_table_mut(root)
32317            .expect("table should exist")
32318            .insert(1, row);
32319
32320        let before = vdbe_metrics_snapshot();
32321        let rows = run_with_storage_cursors(db, |b| {
32322            let end = b.emit_label();
32323            let eof = b.emit_label();
32324            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32325            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(65), 0);
32326            b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32327            b.emit_op(Opcode::Column, 0, 64, 1, P4::None, 0);
32328            b.emit_op(Opcode::Column, 0, 64, 2, P4::None, 0);
32329            b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32330            b.resolve_label(eof);
32331            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32332            b.resolve_label(end);
32333        });
32334        let after = vdbe_metrics_snapshot();
32335
32336        assert_eq!(
32337            rows,
32338            vec![vec![SqliteValue::Integer(64), SqliteValue::Integer(64)]]
32339        );
32340        assert_eq!(
32341            after.decode_cache_hits_total - before.decode_cache_hits_total,
32342            1
32343        );
32344        assert_eq!(
32345            after.decode_cache_misses_total - before.decode_cache_misses_total,
32346            1
32347        );
32348
32349        set_vdbe_metrics_enabled(prev_metrics_enabled);
32350    }
32351
32352    #[test]
32353    fn test_sorter_decode_cache_hits_wide_row_tail_column_after_eager_decode() {
32354        let _guard = VDBE_OBSERVABILITY_LOCK
32355            .lock()
32356            .unwrap_or_else(|e| e.into_inner());
32357        let prev_metrics_enabled = vdbe_metrics_enabled();
32358        reset_vdbe_metrics();
32359        set_vdbe_metrics_enabled(true);
32360
32361        let record = encode_record(&(0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>());
32362        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32363        sorter
32364            .insert_row(vec![SqliteValue::Integer(0)], record)
32365            .expect("sorter insert should succeed");
32366        sorter.sort().expect("sorter sort should succeed");
32367        sorter.position = Some(0);
32368
32369        let mut engine = VdbeEngine::new(1);
32370        engine.collect_vdbe_metrics = true;
32371        engine.sorters.insert(0, sorter);
32372
32373        let before = vdbe_metrics_snapshot();
32374        assert_eq!(
32375            run_async(engine.cursor_column(0, 64))
32376                .expect("first wide sorter decode should succeed"),
32377            SqliteValue::Integer(64)
32378        );
32379        assert_eq!(
32380            run_async(engine.cursor_column(0, 64))
32381                .expect("second wide sorter read should hit cache"),
32382            SqliteValue::Integer(64)
32383        );
32384        let after = vdbe_metrics_snapshot();
32385
32386        assert_eq!(
32387            after.decode_cache_hits_total - before.decode_cache_hits_total,
32388            1
32389        );
32390        assert_eq!(
32391            after.decode_cache_misses_total - before.decode_cache_misses_total,
32392            1
32393        );
32394
32395        set_vdbe_metrics_enabled(prev_metrics_enabled);
32396    }
32397
32398    #[test]
32399    fn test_storage_cursor_wide_row_reuse_hits_cache_without_second_parse_into() {
32400        let _guard = VDBE_OBSERVABILITY_LOCK
32401            .lock()
32402            .unwrap_or_else(|e| e.into_inner());
32403        let prev_metrics_enabled = vdbe_metrics_enabled();
32404        let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
32405        reset_vdbe_metrics();
32406        set_vdbe_metrics_enabled(true);
32407        fsqlite_types::record::reset_record_profile();
32408
32409        let mut db = MemDatabase::new();
32410        let root = db.create_table(65);
32411        let row = (0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>();
32412        db.get_table_mut(root)
32413            .expect("table should exist")
32414            .insert(1, row);
32415
32416        let before_metrics = vdbe_metrics_snapshot();
32417        let before_record_profile = fsqlite_types::record::record_profile_snapshot();
32418        let rows = run_with_storage_cursors(db, |b| {
32419            let end = b.emit_label();
32420            let eof = b.emit_label();
32421            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32422            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(65), 0);
32423            b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32424            b.emit_op(Opcode::Column, 0, 64, 1, P4::None, 0);
32425            b.emit_op(Opcode::Column, 0, 64, 2, P4::None, 0);
32426            b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32427            b.resolve_label(eof);
32428            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32429            b.resolve_label(end);
32430        });
32431        let after_metrics = vdbe_metrics_snapshot();
32432        let after_record_profile = fsqlite_types::record::record_profile_snapshot();
32433
32434        assert_eq!(
32435            rows,
32436            vec![vec![SqliteValue::Integer(64), SqliteValue::Integer(64)]]
32437        );
32438        assert_eq!(
32439            after_metrics.decode_cache_hits_total - before_metrics.decode_cache_hits_total,
32440            1
32441        );
32442        assert_eq!(
32443            after_metrics.decode_cache_misses_total - before_metrics.decode_cache_misses_total,
32444            1
32445        );
32446        assert_eq!(
32447            after_record_profile
32448                .callsite_breakdown
32449                .vdbe_engine
32450                .parse_record_into_calls
32451                - before_record_profile
32452                    .callsite_breakdown
32453                    .vdbe_engine
32454                    .parse_record_into_calls,
32455            1
32456        );
32457
32458        set_vdbe_metrics_enabled(prev_metrics_enabled);
32459    }
32460
32461    #[test]
32462    #[ignore = "manual perf evidence for bd-db300.2.2"]
32463    fn bench_vdbe_metrics_toggle_execute_hot_path() {
32464        let _guard = VDBE_OBSERVABILITY_LOCK
32465            .lock()
32466            .unwrap_or_else(|e| e.into_inner());
32467        let prev_metrics_enabled = vdbe_metrics_enabled();
32468        let iterations = 20_000;
32469
32470        let mut b = ProgramBuilder::new();
32471        let end = b.emit_label();
32472        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32473        b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32474        b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32475        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32476        b.resolve_label(end);
32477        let prog = b.finish().expect("program should build");
32478
32479        let run = |metrics_enabled| {
32480            set_vdbe_metrics_enabled(metrics_enabled);
32481            reset_vdbe_metrics();
32482            let mut engine = VdbeEngine::new(prog.register_count());
32483            let start = Instant::now();
32484            for _ in 0..iterations {
32485                let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
32486                assert_eq!(outcome, ExecOutcome::Done);
32487                engine.results.clear();
32488            }
32489            start.elapsed()
32490        };
32491
32492        let metrics_disabled = run(false);
32493        let metrics_enabled = run(true);
32494        eprintln!(
32495            "bd-db300.2.2 execute hot-path benchmark: iterations={iterations}, metrics_disabled_us={}, metrics_enabled_us={}",
32496            metrics_disabled.as_micros(),
32497            metrics_enabled.as_micros()
32498        );
32499
32500        set_vdbe_metrics_enabled(prev_metrics_enabled);
32501    }
32502
32503    #[test]
32504    fn test_jit_scaffold_metrics_compile_and_cache_hit() {
32505        let _guard = VDBE_OBSERVABILITY_LOCK
32506            .lock()
32507            .unwrap_or_else(|e| e.into_inner());
32508        let prev_enabled = vdbe_jit_enabled();
32509        let prev_threshold = vdbe_jit_hot_threshold();
32510        let prev_capacity = vdbe_jit_cache_capacity();
32511
32512        // Delta-based: snapshot before/after to avoid racing with parallel tests.
32513        set_vdbe_jit_enabled(true);
32514        let _ = set_vdbe_jit_hot_threshold(2);
32515        let _ = set_vdbe_jit_cache_capacity(8);
32516        let before = vdbe_jit_metrics_snapshot();
32517
32518        for _ in 0..3 {
32519            let rows = run_program(|b| {
32520                let end = b.emit_label();
32521                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32522                b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32523                b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32524                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32525                b.resolve_label(end);
32526            });
32527            assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
32528        }
32529
32530        let after = vdbe_jit_metrics_snapshot();
32531        let delta_compilations = after.jit_compilations_total - before.jit_compilations_total;
32532        let delta_cache_hits = after.jit_cache_hits_total - before.jit_cache_hits_total;
32533        assert!(
32534            delta_compilations >= 1,
32535            "expected at least one JIT compile delta, got {delta_compilations}",
32536        );
32537        assert!(
32538            delta_cache_hits >= 1,
32539            "expected at least one JIT cache hit delta, got {delta_cache_hits}",
32540        );
32541        assert!(
32542            after.cache_entries >= 1,
32543            "expected non-empty JIT cache after hot runs"
32544        );
32545
32546        set_vdbe_jit_enabled(prev_enabled);
32547        let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32548        let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32549    }
32550
32551    #[test]
32552    fn test_jit_disabled_leaves_runtime_state_cold() {
32553        let _guard = VDBE_OBSERVABILITY_LOCK
32554            .lock()
32555            .unwrap_or_else(|e| e.into_inner());
32556        let prev_enabled = vdbe_jit_enabled();
32557        let prev_threshold = vdbe_jit_hot_threshold();
32558        let prev_capacity = vdbe_jit_cache_capacity();
32559
32560        set_vdbe_jit_enabled(false);
32561        let _ = set_vdbe_jit_hot_threshold(1);
32562        let _ = set_vdbe_jit_cache_capacity(8);
32563        reset_vdbe_jit_metrics();
32564        let before = vdbe_jit_metrics_snapshot();
32565
32566        for _ in 0..3 {
32567            let rows = run_program(|b| {
32568                let end = b.emit_label();
32569                b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32570                b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32571                b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32572                b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32573                b.resolve_label(end);
32574            });
32575            assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
32576        }
32577
32578        let after = vdbe_jit_metrics_snapshot();
32579        assert_eq!(after.jit_compilations_total, before.jit_compilations_total);
32580        assert_eq!(
32581            after.jit_compile_failures_total,
32582            before.jit_compile_failures_total
32583        );
32584        assert_eq!(after.jit_triggers_total, before.jit_triggers_total);
32585        assert_eq!(after.jit_cache_hits_total, before.jit_cache_hits_total);
32586        assert_eq!(after.jit_cache_misses_total, before.jit_cache_misses_total);
32587        assert_eq!(after.cache_entries, before.cache_entries);
32588
32589        set_vdbe_jit_enabled(prev_enabled);
32590        let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32591        let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32592    }
32593
32594    #[test]
32595    fn test_jit_scaffold_compile_failure_falls_back_to_interpreter() {
32596        let _guard = VDBE_OBSERVABILITY_LOCK
32597            .lock()
32598            .unwrap_or_else(|e| e.into_inner());
32599        let prev_enabled = vdbe_jit_enabled();
32600        let prev_threshold = vdbe_jit_hot_threshold();
32601        let prev_capacity = vdbe_jit_cache_capacity();
32602
32603        // Delta-based: snapshot before/after to avoid racing with parallel tests.
32604        set_vdbe_jit_enabled(true);
32605        let _ = set_vdbe_jit_hot_threshold(1);
32606        let _ = set_vdbe_jit_cache_capacity(8);
32607        let before = vdbe_jit_metrics_snapshot();
32608
32609        let mut db = MemDatabase::new();
32610        let root = db.create_table(1);
32611        db.get_table_mut(root)
32612            .expect("table should exist")
32613            .insert_row(7, vec![SqliteValue::Integer(700)]);
32614
32615        let rows = run_with_storage_cursors(db, |b| {
32616            let start = b.emit_label();
32617            b.emit_jump_to_label(Opcode::Init, 0, 0, start, P4::None, 0);
32618            b.resolve_label(start);
32619            b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
32620            let empty = b.emit_label();
32621            b.emit_jump_to_label(Opcode::Rewind, 0, 0, empty, P4::None, 0);
32622            b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
32623            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32624            b.resolve_label(empty);
32625            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32626        });
32627
32628        assert_eq!(rows, vec![vec![SqliteValue::Integer(700)]]);
32629        let after = vdbe_jit_metrics_snapshot();
32630        let delta_failures = after.jit_compile_failures_total - before.jit_compile_failures_total;
32631        assert!(
32632            delta_failures >= 1,
32633            "expected at least one JIT compile failure delta, got {delta_failures}",
32634        );
32635
32636        set_vdbe_jit_enabled(prev_enabled);
32637        let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32638        let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32639    }
32640
32641    #[test]
32642    fn test_jit_scaffold_distinguishes_programs_that_only_differ_in_p4() {
32643        let _guard = VDBE_OBSERVABILITY_LOCK
32644            .lock()
32645            .unwrap_or_else(|e| e.into_inner());
32646        let prev_enabled = vdbe_jit_enabled();
32647        let prev_threshold = vdbe_jit_hot_threshold();
32648        let prev_capacity = vdbe_jit_cache_capacity();
32649
32650        set_vdbe_jit_enabled(true);
32651        let _ = set_vdbe_jit_hot_threshold(1);
32652        let _ = set_vdbe_jit_cache_capacity(8);
32653        reset_vdbe_jit_metrics();
32654        let before = vdbe_jit_metrics_snapshot();
32655
32656        let alpha_rows = run_program(|b| {
32657            let end = b.emit_label();
32658            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32659            b.emit_op(Opcode::String8, 0, 1, 0, P4::Str("alpha".to_owned()), 0);
32660            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32661            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32662            b.resolve_label(end);
32663        });
32664        assert_eq!(alpha_rows, vec![vec![SqliteValue::Text("alpha".into())]]);
32665
32666        let beta_rows = run_program(|b| {
32667            let end = b.emit_label();
32668            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32669            b.emit_op(Opcode::String8, 0, 1, 0, P4::Str("beta".to_owned()), 0);
32670            b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32671            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32672            b.resolve_label(end);
32673        });
32674        assert_eq!(beta_rows, vec![vec![SqliteValue::Text("beta".into())]]);
32675
32676        let after = vdbe_jit_metrics_snapshot();
32677        let delta_compilations = after.jit_compilations_total - before.jit_compilations_total;
32678        let delta_cache_hits = after.jit_cache_hits_total - before.jit_cache_hits_total;
32679        assert!(
32680            delta_compilations >= 2,
32681            "expected both distinct P4 programs to compile separately, got {delta_compilations}",
32682        );
32683        assert_eq!(
32684            delta_cache_hits, 0,
32685            "programs that only differ in P4 must not alias in the JIT cache",
32686        );
32687
32688        reset_vdbe_jit_metrics();
32689        set_vdbe_jit_enabled(prev_enabled);
32690        let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32691        let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32692    }
32693
32694    #[test]
32695    fn test_jit_cache_hit_arc_clone_microbench() {
32696        let value_sources = (0..24).map(InsertValueSource::Binding).collect::<Vec<_>>();
32697        let compiled = CompiledProgram::SimpleInsert(SimpleInsertTemplate {
32698            cursor_id: 0,
32699            root_page: 2,
32700            num_cols: i32::try_from(value_sources.len()).expect("test value count fits i32"),
32701            value_sources,
32702            affinity: Some("BBBBBBBBBBBBBBBBBBBBBBBB".to_owned()),
32703            record_p4: P4::None,
32704            record_builder: CompiledRecordBuilder::Generic,
32705            insert_flags: 2,
32706        });
32707        let shared = Arc::new(compiled.clone());
32708        let iterations = 50_000_u64;
32709
32710        let direct_started = Instant::now();
32711        let mut direct_acc = 0_u64;
32712        for _ in 0..iterations {
32713            let cloned = std::hint::black_box(compiled.clone());
32714            direct_acc = direct_acc.wrapping_add(estimate_compiled_program_size(&cloned));
32715        }
32716        let direct_clone_ns = direct_started.elapsed().as_nanos();
32717
32718        let arc_started = Instant::now();
32719        let mut arc_acc = 0_usize;
32720        for _ in 0..iterations {
32721            let cloned = std::hint::black_box(Arc::clone(&shared));
32722            arc_acc = arc_acc.wrapping_add(Arc::strong_count(&cloned));
32723        }
32724        let arc_clone_ns = arc_started.elapsed().as_nanos();
32725
32726        std::hint::black_box((direct_acc, arc_acc));
32727        eprintln!(
32728            "bd-db300.2 jit cache-hit clone microbench: iterations={iterations}, deep_clone_ns={direct_clone_ns}, arc_clone_ns={arc_clone_ns}, template_size_bytes={}",
32729            estimate_compiled_program_size(shared.as_ref())
32730        );
32731        assert!(direct_acc > 0);
32732        assert!(arc_acc > 0);
32733        assert_eq!(Arc::strong_count(&shared), 1);
32734    }
32735
32736    #[test]
32737    fn test_sorter_spill_to_disk_under_low_threshold() {
32738        // Set an artificially low spill threshold to trigger disk spill
32739        // with a small dataset, then verify the external merge produces
32740        // correct sorted output.
32741
32742        // Build the sorter cursor directly to test spill logic.
32743        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32744        // Set threshold to 1 byte to force immediate spill on first insert.
32745        sorter.spill_threshold = 1;
32746
32747        // Insert several rows — each should trigger a spill.
32748        // The blob must be a valid serialized record so that the spill
32749        // read-back path (`parse_record_prefix`) can decode key columns.
32750        for value in [50i64, 30, 10, 40, 20] {
32751            let vals = vec![SqliteValue::Integer(value)];
32752            let blob = serialize_record(&vals);
32753            sorter
32754                .insert_row(vals, blob)
32755                .expect("insert should succeed");
32756        }
32757
32758        // We should have spilled runs.
32759        assert!(
32760            !sorter.spill_runs.is_empty(),
32761            "low threshold should cause spills"
32762        );
32763        let spill_count = sorter.spill_runs.len();
32764        assert!(
32765            sorter.spill_pages_total > 0,
32766            "spill_pages_total should be > 0"
32767        );
32768
32769        // Sort (triggers merge).
32770        sorter.sort().expect("sort should succeed");
32771
32772        // After merge, all runs should be cleaned up.
32773        assert!(sorter.spill_runs.is_empty(), "runs should be drained");
32774
32775        // Verify sorted order.
32776        let values: Vec<i64> = sorter
32777            .rows
32778            .iter()
32779            .map(|r| r.values[0].to_integer())
32780            .collect();
32781        assert_eq!(values, vec![10, 20, 30, 40, 50]);
32782
32783        // All 5 rows should have been counted (spill batches + in-memory remainder).
32784        assert!(
32785            sorter.rows_sorted_total >= 5,
32786            "rows_sorted_total should be >= 5, got {}",
32787            sorter.rows_sorted_total
32788        );
32789        // At least one spill run was created.
32790        assert!(spill_count >= 1, "at least one spill run expected");
32791    }
32792
32793    #[test]
32794    fn test_sorter_reset_cleans_spill_state() {
32795        // Verify that reset() clears in-memory rows, position, and spill runs.
32796        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32797        sorter.spill_threshold = 1;
32798
32799        for value in [3i64, 1, 2] {
32800            sorter
32801                .insert_row(vec![SqliteValue::Integer(value)], vec![])
32802                .expect("insert should succeed");
32803        }
32804        assert!(!sorter.spill_runs.is_empty());
32805
32806        sorter.reset();
32807
32808        assert!(sorter.rows.is_empty(), "rows should be cleared");
32809        assert!(sorter.position.is_none(), "position should be None");
32810        assert!(sorter.spill_runs.is_empty(), "spill_runs should be cleared");
32811        assert_eq!(sorter.memory_used, 0, "memory_used should be 0");
32812    }
32813
32814    #[test]
32815    fn test_sorter_desc_key_order_with_external_merge() {
32816        // Verify that DESC sort order works correctly through the external
32817        // merge path.
32818        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Desc], Vec::new());
32819        sorter.spill_threshold = 1;
32820
32821        for value in [10i64, 50, 30, 20, 40] {
32822            let vals = vec![SqliteValue::Integer(value)];
32823            let blob = serialize_record(&vals);
32824            sorter
32825                .insert_row(vals, blob)
32826                .expect("insert should succeed");
32827        }
32828
32829        sorter.sort().expect("sort should succeed");
32830
32831        let values: Vec<i64> = sorter
32832            .rows
32833            .iter()
32834            .map(|r| r.values[0].to_integer())
32835            .collect();
32836        assert_eq!(values, vec![50, 40, 30, 20, 10]);
32837    }
32838
32839    #[test]
32840    fn test_sorter_multi_column_key_with_mixed_order() {
32841        // Test sorting with 2 key columns: first ASC, second DESC.
32842        let mut sorter =
32843            SorterCursor::new(2, vec![SortKeyOrder::Asc, SortKeyOrder::Desc], Vec::new());
32844
32845        // Insert rows: (group, value)
32846        for (group, value) in [(1i64, 30i64), (2, 10), (1, 20), (2, 40), (1, 10)] {
32847            sorter
32848                .insert_row(
32849                    vec![SqliteValue::Integer(group), SqliteValue::Integer(value)],
32850                    vec![],
32851                )
32852                .expect("insert should succeed");
32853        }
32854
32855        sorter.sort().expect("sort should succeed");
32856
32857        let values: Vec<(i64, i64)> = sorter
32858            .rows
32859            .iter()
32860            .map(|r| (r.values[0].to_integer(), r.values[1].to_integer()))
32861            .collect();
32862        // Group 1 first (ASC), within group value DESC: 30, 20, 10.
32863        // Group 2 second, within group value DESC: 40, 10.
32864        assert_eq!(values, vec![(1, 30), (1, 20), (1, 10), (2, 40), (2, 10)]);
32865    }
32866
32867    #[test]
32868    fn test_sorter_memory_estimation() {
32869        // Verify memory tracking increases with row insertions.
32870        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32871        assert_eq!(sorter.memory_used, 0);
32872
32873        sorter
32874            .insert_row(vec![SqliteValue::Integer(42)], vec![])
32875            .expect("insert should succeed");
32876        let after_one = sorter.memory_used;
32877        assert!(after_one > 0, "memory should increase after insert");
32878
32879        sorter
32880            .insert_row(vec![SqliteValue::Text("hello world".into())], vec![])
32881            .expect("insert should succeed");
32882        let after_two = sorter.memory_used;
32883        assert!(
32884            after_two > after_one,
32885            "memory should increase with text value"
32886        );
32887    }
32888
32889    #[test]
32890    fn test_sorter_empty_sort() {
32891        // Sorting an empty sorter should succeed and leave rows empty.
32892        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32893        sorter.sort().expect("empty sort should succeed");
32894        assert!(sorter.rows.is_empty());
32895    }
32896
32897    #[test]
32898    fn test_sorter_pure_inmemory_sort_path() {
32899        // Verify the fast in-memory path works when no spill occurs.
32900        let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32901        // Default threshold is 100 MiB — won't spill.
32902
32903        for value in [5i64, 3, 1, 4, 2] {
32904            sorter
32905                .insert_row(vec![SqliteValue::Integer(value)], vec![])
32906                .expect("insert should succeed");
32907        }
32908
32909        assert!(sorter.spill_runs.is_empty(), "no spill expected");
32910        sorter.sort().expect("sort should succeed");
32911
32912        let values: Vec<i64> = sorter
32913            .rows
32914            .iter()
32915            .map(|r| r.values[0].to_integer())
32916            .collect();
32917        assert_eq!(values, vec![1, 2, 3, 4, 5]);
32918        assert_eq!(sorter.rows_sorted_total, 5);
32919    }
32920
32921    #[test]
32922    fn test_sorter_top_n_limit_keeps_only_best_rows() {
32923        let mut sorter = SorterCursor::with_collation_registry(
32924            1,
32925            vec![SortKeyOrder::Asc],
32926            Vec::new(),
32927            Arc::new(Mutex::new(CollationRegistry::new())),
32928            Some(3),
32929        );
32930
32931        for value in [9i64, 1, 7, 3, 2] {
32932            sorter
32933                .insert_row(vec![SqliteValue::Integer(value)], vec![])
32934                .expect("insert should succeed");
32935        }
32936
32937        assert_eq!(
32938            sorter.rows.len(),
32939            3,
32940            "top-N sorter should prune rows during insertion"
32941        );
32942        sorter.sort().expect("sort should succeed");
32943
32944        let values: Vec<i64> = sorter
32945            .rows
32946            .iter()
32947            .map(|r| r.values[0].to_integer())
32948            .collect();
32949        assert_eq!(values, vec![1, 2, 3]);
32950    }
32951
32952    #[test]
32953    fn test_sorter_top_n_limit_desc_uses_heap_then_final_sort() {
32954        let mut sorter = SorterCursor::with_collation_registry(
32955            1,
32956            vec![SortKeyOrder::Desc],
32957            Vec::new(),
32958            Arc::new(Mutex::new(CollationRegistry::new())),
32959            Some(3),
32960        );
32961
32962        for value in [1i64, 2, 3, 4, 5] {
32963            sorter
32964                .insert_row(vec![SqliteValue::Integer(value)], vec![])
32965                .expect("insert should succeed");
32966        }
32967
32968        let mut retained: Vec<i64> = sorter
32969            .rows
32970            .iter()
32971            .map(|r| r.values[0].to_integer())
32972            .collect();
32973        retained.sort_unstable();
32974        assert_eq!(retained, vec![3, 4, 5]);
32975
32976        sorter.sort().expect("sort should succeed");
32977        let sorted: Vec<i64> = sorter
32978            .rows
32979            .iter()
32980            .map(|r| r.values[0].to_integer())
32981            .collect();
32982        assert_eq!(sorted, vec![5, 4, 3]);
32983    }
32984
32985    #[test]
32986    fn test_sorter_top_n_equal_keys_keep_earliest_source_rows() {
32987        let mut sorter = SorterCursor::with_collation_registry(
32988            1,
32989            vec![SortKeyOrder::Asc],
32990            Vec::new(),
32991            Arc::new(Mutex::new(CollationRegistry::new())),
32992            Some(2),
32993        );
32994
32995        for payload in [10i64, 20, 30] {
32996            let values = [SqliteValue::Integer(1), SqliteValue::Integer(payload)];
32997            sorter
32998                .insert_row(
32999                    vec![values[0].clone()],
33000                    fsqlite_types::record::serialize_record(&values),
33001                )
33002                .expect("insert should succeed");
33003        }
33004
33005        sorter.sort().expect("sort should succeed");
33006        let payloads: Vec<i64> = sorter
33007            .rows
33008            .iter()
33009            .map(|row| {
33010                fsqlite_types::record::parse_record(&row.blob)
33011                    .expect("retained record should decode")[1]
33012                    .to_integer()
33013            })
33014            .collect();
33015        assert_eq!(payloads, vec![10, 20]);
33016    }
33017
33018    #[cfg(not(target_arch = "wasm32"))]
33019    #[test]
33020    fn test_sorter_top_n_downgrades_before_discard_to_preserve_stable_spill_input() {
33021        let mut sorter = SorterCursor::with_collation_registry(
33022            1,
33023            vec![SortKeyOrder::Asc],
33024            Vec::new(),
33025            Arc::new(Mutex::new(CollationRegistry::new())),
33026            Some(10),
33027        );
33028        sorter.spill_threshold = usize::MAX;
33029
33030        let make_row = |payload| {
33031            let record = [SqliteValue::Integer(7), SqliteValue::Integer(payload)];
33032            (
33033                vec![record[0].clone()],
33034                fsqlite_types::record::serialize_record(&record),
33035            )
33036        };
33037
33038        for payload in [10i64, 20] {
33039            let (key, blob) = make_row(payload);
33040            sorter
33041                .insert_row(key, blob)
33042                .expect("heap-building insert should succeed");
33043        }
33044        let heap_payloads: Vec<i64> = sorter
33045            .rows
33046            .iter()
33047            .map(|row| {
33048                fsqlite_types::record::parse_record(&row.blob).expect("heap row should decode")[1]
33049                    .to_integer()
33050            })
33051            .collect();
33052        assert_eq!(
33053            heap_payloads,
33054            vec![20, 10],
33055            "setup should prove the bounded heap reordered equal-key rows"
33056        );
33057
33058        let (third_key, third_blob) = make_row(30);
33059        let third_size = SorterCursor::estimate_row_size(&third_key, &third_blob);
33060        sorter.spill_threshold = sorter.memory_used.saturating_add(third_size);
33061        sorter
33062            .insert_row(third_key, third_blob)
33063            .expect("downgrading insert should spill successfully");
33064
33065        assert!(
33066            sorter.top_n_limit.is_none(),
33067            "complete input prefix should downgrade to the spillable sorter"
33068        );
33069        assert!(
33070            !sorter.spill_runs.is_empty(),
33071            "downgrading insert should activate the ordinary spill path"
33072        );
33073
33074        let (fourth_key, fourth_blob) = make_row(40);
33075        sorter
33076            .insert_row(fourth_key, fourth_blob)
33077            .expect("post-downgrade insert should succeed");
33078        sorter.sort().expect("external merge should succeed");
33079
33080        let payloads: Vec<i64> = sorter
33081            .rows
33082            .iter()
33083            .map(|row| {
33084                fsqlite_types::record::parse_record(&row.blob).expect("merged row should decode")[1]
33085                    .to_integer()
33086            })
33087            .collect();
33088        assert_eq!(
33089            payloads,
33090            vec![10, 20, 30, 40],
33091            "downgrade must preserve every row and stable equal-key source order"
33092        );
33093    }
33094
33095    #[cfg(target_arch = "wasm32")]
33096    #[test]
33097    fn test_sorter_top_n_stays_bounded_without_native_spill_support() {
33098        let mut sorter = SorterCursor::with_collation_registry(
33099            1,
33100            vec![SortKeyOrder::Asc],
33101            Vec::new(),
33102            Arc::new(Mutex::new(CollationRegistry::new())),
33103            Some(2),
33104        );
33105        sorter.spill_threshold = 1;
33106
33107        for value in [3i64, 2, 1] {
33108            sorter
33109                .insert_row(vec![SqliteValue::Integer(value)], Vec::new())
33110                .expect("bounded insert should succeed");
33111        }
33112
33113        assert_eq!(
33114            sorter.top_n_limit,
33115            Some(2),
33116            "wasm must keep its effective top-N memory bound"
33117        );
33118        sorter
33119            .sort()
33120            .expect("bounded in-memory sort should succeed");
33121        let values: Vec<i64> = sorter
33122            .rows
33123            .iter()
33124            .map(|row| row.values[0].to_integer())
33125            .collect();
33126        assert_eq!(values, vec![1, 2]);
33127    }
33128
33129    #[test]
33130    fn test_sorter_top_n_preflight_respects_multikey_null_and_nocase_order() {
33131        let registry = Arc::new(Mutex::new(CollationRegistry::new()));
33132        let mut sorter = SorterCursor::with_collation_registry(
33133            2,
33134            vec![SortKeyOrder::AscNullsLast, SortKeyOrder::Desc],
33135            vec![None, Some("NOCASE".to_owned())],
33136            Arc::clone(&registry),
33137            Some(1),
33138        );
33139        sorter
33140            .insert_row(
33141                vec![SqliteValue::Integer(2), SqliteValue::Text("beta".into())],
33142                Vec::new(),
33143            )
33144            .expect("insert should succeed");
33145
33146        let collations = registry.lock().unwrap_or_else(|error| error.into_inner());
33147        assert!(sorter.would_retain_top_n(
33148            &[SqliteValue::Integer(1), SqliteValue::Text("alpha".into()),],
33149            &collations,
33150        ));
33151        assert!(!sorter.would_retain_top_n(
33152            &[SqliteValue::Integer(2), SqliteValue::Text("BETA".into()),],
33153            &collations,
33154        ));
33155        assert!(!sorter.would_retain_top_n(
33156            &[SqliteValue::Null, SqliteValue::Text("zeta".into())],
33157            &collations,
33158        ));
33159    }
33160
33161    // ── bd-2ttd8.1: Pager routing and parity-cert tests ──────────────
33162
33163    #[test]
33164    fn test_reject_mem_fallback_default_on() {
33165        // bd-zjisk.1: Parity-cert mode is enabled by default.
33166        let engine = VdbeEngine::new(8);
33167        assert!(engine.reject_mem_fallback);
33168    }
33169
33170    #[test]
33171    fn test_set_reject_mem_fallback() {
33172        let mut engine = VdbeEngine::new(8);
33173        engine.set_reject_mem_fallback(true);
33174        assert!(engine.reject_mem_fallback);
33175        engine.set_reject_mem_fallback(false);
33176        assert!(!engine.reject_mem_fallback);
33177    }
33178
33179    #[test]
33180    fn test_open_storage_cursor_mem_fallback_without_parity_cert() {
33181        // Without parity-cert mode, OpenRead should succeed via MemPageStore
33182        // fallback when no pager transaction is set.
33183        let mut db = MemDatabase::new();
33184        let root = db.create_table(1);
33185        let table = db.get_table_mut(root).expect("table should exist");
33186        table.insert(1, vec![SqliteValue::Integer(42)]);
33187
33188        let mut engine = VdbeEngine::new(8);
33189        engine.set_database(db);
33190        // Explicitly opt out of parity-cert mode to test fallback path.
33191        engine.set_reject_mem_fallback(false);
33192
33193        // No txn_page_io set — should fall back to MemPageStore.
33194        assert!(run_async(engine.open_storage_cursor(0, root, false)));
33195        assert!(engine.storage_cursors.get(&0).is_some());
33196    }
33197
33198    #[test]
33199    fn test_open_storage_cursor_rejected_in_parity_cert_mode() {
33200        // In parity-cert mode (now the default), OpenRead should FAIL when
33201        // no pager transaction is available and MemPageStore fallback would
33202        // be used.
33203        let mut db = MemDatabase::new();
33204        let root = db.create_table(1);
33205        let table = db.get_table_mut(root).expect("table should exist");
33206        table.insert(1, vec![SqliteValue::Integer(42)]);
33207
33208        let mut engine = VdbeEngine::new(8);
33209        engine.set_database(db);
33210        // Default is already true (parity-cert mode), but be explicit.
33211        engine.set_reject_mem_fallback(true);
33212
33213        // No txn_page_io set — parity-cert should reject the fallback.
33214        assert!(!run_async(engine.open_storage_cursor(0, root, false)));
33215        assert!(engine.storage_cursors.get(&0).is_none());
33216    }
33217
33218    #[test]
33219    fn test_open_storage_cursor_invalid_page_number() {
33220        // Root page 0 is invalid (PageNumber requires nonzero).
33221        let mut engine = VdbeEngine::new(8);
33222        assert!(!run_async(engine.open_storage_cursor(0, 0, false)));
33223    }
33224
33225    #[test]
33226    fn test_bd_2ttd8_set_transaction_enables_storage_cursors() {
33227        // set_transaction should auto-enable storage cursors and set txn_page_io.
33228        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33229
33230        let pager = MockMvccPager;
33231        let cx = Cx::new();
33232        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33233
33234        let mut engine = VdbeEngine::new(8);
33235        engine.set_transaction(txn);
33236        assert!(engine.storage_cursors_enabled);
33237        assert!(engine.txn_page_io.is_some());
33238    }
33239
33240    #[test]
33241    fn test_storage_only_table_program_executes_without_attached_memdb() {
33242        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
33243
33244        let pager = MemoryMockMvccPager;
33245        let cx = Cx::new();
33246        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33247        let root = 256;
33248
33249        let mut b = ProgramBuilder::new();
33250        let end = b.emit_label();
33251        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33252        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33253        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33254        b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
33255        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
33256        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
33257        b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
33258        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
33259        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33260        b.resolve_label(end);
33261
33262        let prog = b.finish().expect("program should build");
33263        assert!(
33264            !prog.requires_attached_memdb(),
33265            "storage-only table bytecode should not require an attached MemDatabase"
33266        );
33267
33268        let mut engine = VdbeEngine::new(prog.register_count());
33269        engine.set_transaction(txn);
33270        engine.set_reject_mem_fallback(true);
33271
33272        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33273        assert_eq!(outcome, ExecOutcome::Done);
33274        assert!(
33275            engine.all_cursors_are_txn_backed(),
33276            "storage-only hot path must stay on txn-backed cursors"
33277        );
33278        let rows: Vec<_> = engine
33279            .take_results()
33280            .into_iter()
33281            .map(|row| row.into_vec())
33282            .collect();
33283        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33284        assert!(
33285            engine.take_database().is_none(),
33286            "the engine should not need an attached MemDatabase for this hot path"
33287        );
33288    }
33289
33290    #[test]
33291    fn test_storage_only_nested_loop_join_executes_without_attached_memdb() {
33292        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
33293
33294        let pager = MemoryMockMvccPager;
33295        let cx = Cx::new();
33296        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33297        let orders_root = 256;
33298        let customers_root = 257;
33299
33300        let mut b = ProgramBuilder::new();
33301        let end = b.emit_label();
33302        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33303        b.emit_op(Opcode::OpenWrite, 0, orders_root, 0, P4::Int(2), 0);
33304        b.emit_op(Opcode::OpenWrite, 1, customers_root, 0, P4::Int(2), 0);
33305
33306        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33307        b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33308        b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("ann".to_owned()), 0);
33309        b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33310        b.emit_op(Opcode::Insert, 1, 4, 1, P4::None, 0);
33311
33312        b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
33313        b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
33314        b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("bob".to_owned()), 0);
33315        b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33316        b.emit_op(Opcode::Insert, 1, 4, 1, P4::None, 0);
33317
33318        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33319        b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33320        b.emit_op(Opcode::Integer, 10, 3, 0, P4::None, 0);
33321        b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33322        b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33323
33324        b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
33325        b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
33326        b.emit_op(Opcode::Integer, 20, 3, 0, P4::None, 0);
33327        b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33328        b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33329
33330        b.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
33331        b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33332        b.emit_op(Opcode::Integer, 30, 3, 0, P4::None, 0);
33333        b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33334        b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33335
33336        let done = b.emit_label();
33337        let next_order = b.emit_label();
33338        b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
33339        let order_body = b.current_addr();
33340        b.emit_op(Opcode::Column, 0, 0, 10, P4::None, 0);
33341        b.emit_op(Opcode::Column, 0, 1, 11, P4::None, 0);
33342
33343        b.emit_jump_to_label(Opcode::Rewind, 1, 0, next_order, P4::None, 0);
33344        let customer_body = b.current_addr();
33345        b.emit_op(Opcode::Column, 1, 0, 12, P4::None, 0);
33346        let no_match = b.emit_label();
33347        b.emit_jump_to_label(Opcode::Ne, 10, 12, no_match, P4::None, 0);
33348        b.emit_op(Opcode::Column, 1, 1, 13, P4::None, 0);
33349        b.emit_op(Opcode::SCopy, 11, 14, 0, P4::None, 0);
33350        b.emit_op(Opcode::ResultRow, 13, 2, 0, P4::None, 0);
33351        b.resolve_label(no_match);
33352        b.emit_op(
33353            Opcode::Next,
33354            1,
33355            i32::try_from(customer_body).expect("test program address should fit in i32"),
33356            0,
33357            P4::None,
33358            0,
33359        );
33360
33361        b.resolve_label(next_order);
33362        b.emit_op(
33363            Opcode::Next,
33364            0,
33365            i32::try_from(order_body).expect("test program address should fit in i32"),
33366            0,
33367            P4::None,
33368            0,
33369        );
33370        b.resolve_label(done);
33371        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33372        b.resolve_label(end);
33373
33374        let prog = b.finish().expect("program should build");
33375        assert!(
33376            !prog.requires_attached_memdb(),
33377            "pager-backed nested-loop join bytecode must not require an attached MemDatabase"
33378        );
33379
33380        let mut engine = VdbeEngine::new(prog.register_count());
33381        engine.set_transaction(txn);
33382        engine.set_reject_mem_fallback(true);
33383
33384        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33385        assert_eq!(outcome, ExecOutcome::Done);
33386        assert!(
33387            engine.all_cursors_are_txn_backed(),
33388            "join scan cursors must stay on txn-backed storage cursors"
33389        );
33390        let rows: Vec<_> = engine
33391            .take_results()
33392            .into_iter()
33393            .map(|row| row.into_vec())
33394            .collect();
33395        assert_eq!(
33396            rows,
33397            vec![
33398                vec![SqliteValue::Text("ann".into()), SqliteValue::Integer(10)],
33399                vec![SqliteValue::Text("bob".into()), SqliteValue::Integer(20)],
33400                vec![SqliteValue::Text("ann".into()), SqliteValue::Integer(30)],
33401            ]
33402        );
33403        assert!(
33404            engine.take_database().is_none(),
33405            "the engine should not need an attached MemDatabase for join scans"
33406        );
33407    }
33408
33409    #[test]
33410    fn test_open_read_opcode_with_mem_fallback() {
33411        // OpenRead via VDBE execution should succeed when MemDatabase has the
33412        // table, verifying the full cursor lifecycle.
33413        let mut db = MemDatabase::new();
33414        let root = db.create_table(1);
33415        let table = db.get_table_mut(root).expect("table should exist");
33416        table.insert(1, vec![SqliteValue::Integer(100)]);
33417
33418        let mut b = ProgramBuilder::new();
33419        let end = b.emit_label();
33420        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33421        b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
33422        // Rewind to first row.
33423        let halt_label = b.emit_label();
33424        b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt_label, P4::None, 0);
33425        // Read column 0 into register 1.
33426        b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
33427        b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
33428        b.resolve_label(halt_label);
33429        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33430        b.resolve_label(end);
33431
33432        let prog = b.finish().expect("program should build");
33433        let mut engine = VdbeEngine::new(prog.register_count());
33434        engine.set_database(db);
33435        // Explicitly opt out of parity-cert to test the MemPageStore fallback.
33436        engine.set_reject_mem_fallback(false);
33437
33438        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33439        assert_eq!(outcome, ExecOutcome::Done);
33440
33441        let results: Vec<_> = engine
33442            .take_results()
33443            .into_iter()
33444            .map(|v| v.into_vec())
33445            .collect();
33446        assert_eq!(results.len(), 1);
33447        assert_eq!(results[0], vec![SqliteValue::Integer(100)]);
33448    }
33449
33450    #[test]
33451    fn test_open_write_insert_delete_cursor_lifecycle() {
33452        // Verify full cursor lifecycle: OpenWrite → Insert → Rewind →
33453        // Column → Delete → verify empty.
33454        let mut db = MemDatabase::new();
33455        let root = db.create_table(1);
33456
33457        let mut b = ProgramBuilder::new();
33458        let end = b.emit_label();
33459        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33460        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33461
33462        // Insert rowid=1 with value 42.
33463        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0); // rowid in r1
33464        b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0); // value in r2
33465        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0); // record in r3
33466        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
33467
33468        // Rewind and read back.
33469        let eof_label = b.emit_label();
33470        b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof_label, P4::None, 0);
33471        b.emit_op(Opcode::Column, 0, 0, 4, P4::None, 0);
33472        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
33473        // Delete the row.
33474        b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
33475        b.resolve_label(eof_label);
33476
33477        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33478        b.resolve_label(end);
33479
33480        let prog = b.finish().expect("program should build");
33481        let mut engine = VdbeEngine::new(prog.register_count());
33482        engine.set_database(db);
33483        // Explicitly opt out of parity-cert to test the MemPageStore fallback.
33484        engine.set_reject_mem_fallback(false);
33485
33486        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33487        assert_eq!(outcome, ExecOutcome::Done);
33488
33489        let results: Vec<_> = engine
33490            .take_results()
33491            .into_iter()
33492            .map(|v| v.into_vec())
33493            .collect();
33494        assert_eq!(results.len(), 1);
33495        assert_eq!(results[0], vec![SqliteValue::Integer(42)]);
33496    }
33497
33498    #[test]
33499    fn test_parity_cert_rejects_open_read_without_txn() {
33500        // In parity-cert mode, OpenRead should fail execution when no pager
33501        // transaction is available.
33502        let mut db = MemDatabase::new();
33503        let root = db.create_table(1);
33504        let table = db.get_table_mut(root).expect("table should exist");
33505        table.insert(1, vec![SqliteValue::Integer(1)]);
33506
33507        let mut b = ProgramBuilder::new();
33508        let end = b.emit_label();
33509        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33510        b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
33511        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33512        b.resolve_label(end);
33513
33514        let prog = b.finish().expect("program should build");
33515        let mut engine = VdbeEngine::new(prog.register_count());
33516        engine.set_database(db);
33517        engine.set_reject_mem_fallback(true);
33518
33519        let result = run_async(engine.execute(&prog));
33520        assert!(
33521            result.is_err(),
33522            "OpenRead should fail in parity-cert mode without txn"
33523        );
33524    }
33525
33526    #[test]
33527    fn test_parity_cert_rejects_open_write_without_txn() {
33528        // In parity-cert mode, OpenWrite should also fail without a txn.
33529        let mut db = MemDatabase::new();
33530        let root = db.create_table(1);
33531
33532        let mut b = ProgramBuilder::new();
33533        let end = b.emit_label();
33534        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33535        b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33536        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33537        b.resolve_label(end);
33538
33539        let prog = b.finish().expect("program should build");
33540        let mut engine = VdbeEngine::new(prog.register_count());
33541        engine.set_database(db);
33542        engine.set_reject_mem_fallback(true);
33543
33544        let result = run_async(engine.execute(&prog));
33545        assert!(
33546            result.is_err(),
33547            "OpenWrite should fail in parity-cert mode without txn"
33548        );
33549    }
33550
33551    // ── bd-2ttd8.4: Backend-identity ratchet tests ──────────────────
33552
33553    #[test]
33554    fn test_cursor_backend_kind_mem() {
33555        let mut db = MemDatabase::new();
33556        let root = db.create_table(1);
33557
33558        let mut engine = VdbeEngine::new(8);
33559        engine.set_database(db);
33560        engine.set_reject_mem_fallback(false);
33561
33562        assert!(run_async(engine.open_storage_cursor(0, root, false)));
33563        assert!(
33564            engine.has_mem_cursor(),
33565            "cursor should be mem-backed without txn"
33566        );
33567        assert!(!engine.all_cursors_are_txn_backed());
33568    }
33569
33570    #[test]
33571    fn test_cursor_backend_kind_txn() {
33572        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33573        let pager = MockMvccPager;
33574        let cx = Cx::new();
33575        let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33576
33577        let mut engine = VdbeEngine::new(8);
33578        engine.set_database(MemDatabase::new());
33579        engine.set_transaction(txn);
33580
33581        // Open cursor on page 1 (valid with pager txn).
33582        assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33583        assert!(
33584            engine.all_cursors_are_txn_backed(),
33585            "cursor should be txn-backed with pager transaction"
33586        );
33587        assert!(!engine.has_mem_cursor());
33588    }
33589
33590    #[test]
33591    fn test_validate_parity_cert_invariant_no_cursors() {
33592        let mut engine = VdbeEngine::new(8);
33593        engine.set_reject_mem_fallback(true);
33594        assert!(
33595            engine.validate_parity_cert_invariant().is_ok(),
33596            "vacuously valid with no cursors"
33597        );
33598    }
33599
33600    #[test]
33601    fn test_validate_parity_cert_invariant_with_txn_cursor() {
33602        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33603        let pager = MockMvccPager;
33604        let cx = Cx::new();
33605        let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33606
33607        let mut engine = VdbeEngine::new(8);
33608        engine.set_database(MemDatabase::new());
33609        engine.set_transaction(txn);
33610        engine.set_reject_mem_fallback(true);
33611
33612        assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33613        assert!(
33614            engine.validate_parity_cert_invariant().is_ok(),
33615            "txn-backed cursor satisfies parity-cert invariant"
33616        );
33617    }
33618
33619    #[test]
33620    fn test_validate_parity_cert_invariant_disabled_allows_mem() {
33621        let mut db = MemDatabase::new();
33622        let root = db.create_table(1);
33623
33624        let mut engine = VdbeEngine::new(8);
33625        engine.set_database(db);
33626        // Explicitly disable parity-cert — mem cursors allowed.
33627        engine.set_reject_mem_fallback(false);
33628        assert!(run_async(engine.open_storage_cursor(0, root, false)));
33629        assert!(
33630            engine.validate_parity_cert_invariant().is_ok(),
33631            "parity-cert disabled should always pass"
33632        );
33633    }
33634
33635    #[test]
33636    fn test_all_cursors_txn_backed_vacuous() {
33637        let engine = VdbeEngine::new(8);
33638        assert!(
33639            engine.all_cursors_are_txn_backed(),
33640            "no cursors → vacuously true"
33641        );
33642        assert!(!engine.has_mem_cursor(), "no cursors → no mem cursor");
33643    }
33644
33645    #[test]
33646    fn test_cursor_kind_str_values() {
33647        let mut db = MemDatabase::new();
33648        let root = db.create_table(1);
33649
33650        let mut engine = VdbeEngine::new(8);
33651        engine.set_database(db);
33652        engine.set_reject_mem_fallback(false);
33653        run_async(engine.open_storage_cursor(0, root, false));
33654
33655        let sc = engine.storage_cursors.get(&0).unwrap();
33656        assert_eq!(sc.cursor.kind_str(), "mem");
33657    }
33658
33659    #[test]
33660    fn test_ratchet_prevents_mem_cursor_creation_in_parity_mode() {
33661        // This is the core anti-regression ratchet: when parity-cert is
33662        // enabled and no txn is set, cursor creation MUST fail — it cannot
33663        // silently fall through to MemPageStore.
33664        let mut db = MemDatabase::new();
33665        let root = db.create_table(1);
33666        let table = db.get_table_mut(root).unwrap();
33667        table.insert(1, vec![SqliteValue::Integer(99)]);
33668
33669        let mut engine = VdbeEngine::new(8);
33670        engine.set_database(db);
33671        engine.set_reject_mem_fallback(true);
33672
33673        // Attempt to open cursor — should fail.
33674        let opened = run_async(engine.open_storage_cursor(0, root, false));
33675        assert!(
33676            !opened,
33677            "ratchet must prevent cursor creation in parity-cert mode"
33678        );
33679
33680        // Validate invariant still holds.
33681        assert!(engine.validate_parity_cert_invariant().is_ok());
33682        assert!(!engine.has_mem_cursor());
33683    }
33684
33685    #[test]
33686    fn test_ratchet_allows_txn_cursor_in_parity_mode() {
33687        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33688        let pager = MockMvccPager;
33689        let cx = Cx::new();
33690        let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33691
33692        let mut engine = VdbeEngine::new(8);
33693        engine.set_database(MemDatabase::new());
33694        engine.set_transaction(txn);
33695        engine.set_reject_mem_fallback(true);
33696
33697        // With txn set, cursor creation should succeed via pager path.
33698        let opened = run_async(engine.open_storage_cursor(0, 1, false));
33699        assert!(opened, "txn-backed cursor should work in parity-cert mode");
33700        assert!(engine.all_cursors_are_txn_backed());
33701        assert!(engine.validate_parity_cert_invariant().is_ok());
33702    }
33703
33704    #[test]
33705    fn test_ratchet_multiple_cursors_mixed_rejection() {
33706        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33707        let pager = MockMvccPager;
33708        let cx = Cx::new();
33709        let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33710
33711        let mut engine = VdbeEngine::new(8);
33712        engine.set_database(MemDatabase::new());
33713        engine.set_transaction(txn);
33714        engine.set_reject_mem_fallback(true);
33715
33716        // Open cursor 0 on page 1 — should succeed (txn path).
33717        assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33718        assert!(engine.all_cursors_are_txn_backed());
33719
33720        // Attempt cursor 1 on non-existent high page — should still
33721        // succeed via txn path (MockMvccPager returns zero-filled pages).
33722        assert!(run_async(engine.open_storage_cursor(1, 1, false)));
33723        assert!(engine.all_cursors_are_txn_backed());
33724        assert!(engine.validate_parity_cert_invariant().is_ok());
33725    }
33726
33727    // ── RowSet opcode tests ──────────────────────────────────────
33728
33729    #[test]
33730    fn test_rowset_add_and_read_returns_sorted() {
33731        // Add rowids 30, 10, 20 then read them back — should come out sorted.
33732        let rows = run_program(|b| {
33733            let end = b.emit_label();
33734            let exhausted = b.emit_label();
33735            let loop_start = b.emit_label();
33736            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33737
33738            let r_val = b.alloc_reg();
33739            let r_out = b.alloc_reg();
33740            let rowset_reg = b.alloc_reg();
33741
33742            // Add 30, 10, 20 to rowset
33743            for v in [30, 10, 20] {
33744                b.emit_op(Opcode::Integer, v, r_val, 0, P4::None, 0);
33745                b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33746            }
33747
33748            // Read loop: RowSetRead P1=rowset, P2=jump_when_exhausted, P3=output
33749            b.resolve_label(loop_start);
33750            b.emit_jump_to_label(
33751                Opcode::RowSetRead,
33752                rowset_reg,
33753                r_out,
33754                exhausted,
33755                P4::None,
33756                0,
33757            );
33758            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33759            b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33760
33761            b.resolve_label(exhausted);
33762            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33763            b.resolve_label(end);
33764        });
33765
33766        let vals: Vec<i64> = rows.into_iter().map(|row| row[0].to_integer()).collect();
33767        assert_eq!(vals, vec![10, 20, 30]);
33768    }
33769
33770    #[test]
33771    fn test_rowset_deduplicates() {
33772        // Add the same value twice; read should return it once.
33773        let rows = run_program(|b| {
33774            let end = b.emit_label();
33775            let exhausted = b.emit_label();
33776            let loop_start = b.emit_label();
33777            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33778
33779            let r_val = b.alloc_reg();
33780            let r_out = b.alloc_reg();
33781            let rowset_reg = b.alloc_reg();
33782
33783            for v in [5, 5, 5] {
33784                b.emit_op(Opcode::Integer, v, r_val, 0, P4::None, 0);
33785                b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33786            }
33787
33788            b.resolve_label(loop_start);
33789            b.emit_jump_to_label(
33790                Opcode::RowSetRead,
33791                rowset_reg,
33792                r_out,
33793                exhausted,
33794                P4::None,
33795                0,
33796            );
33797            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33798            b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33799
33800            b.resolve_label(exhausted);
33801            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33802            b.resolve_label(end);
33803        });
33804
33805        assert_eq!(rows.len(), 1);
33806        assert_eq!(rows[0][0].to_integer(), 5);
33807    }
33808
33809    #[test]
33810    fn test_rowset_test_jumps_if_found() {
33811        // Add 42 to rowset, then test for 42 — should jump.
33812        let rows = run_program(|b| {
33813            let end = b.emit_label();
33814            let found = b.emit_label();
33815            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33816
33817            let r_val = b.alloc_reg();
33818            let r_out = b.alloc_reg();
33819            let rowset_reg = b.alloc_reg();
33820
33821            // Add 42
33822            b.emit_op(Opcode::Integer, 42, r_val, 0, P4::None, 0);
33823            b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33824
33825            // Test for 42 — should jump to `found`
33826            // RowSetTest: P1=rowset, P2=jump_if_found, P3=value register
33827            b.emit_jump_to_label(Opcode::RowSetTest, rowset_reg, r_val, found, P4::None, 0);
33828            // Not found path
33829            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
33830            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33831            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33832
33833            // Found path
33834            b.resolve_label(found);
33835            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
33836            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33837            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33838            b.resolve_label(end);
33839        });
33840
33841        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33842    }
33843
33844    #[test]
33845    fn test_rowset_test_falls_through_and_adds_if_not_found() {
33846        // RowSetTest on empty set: should fall through and add the value.
33847        let rows = run_program(|b| {
33848            let end = b.emit_label();
33849            let found = b.emit_label();
33850            let exhausted = b.emit_label();
33851            let loop_start = b.emit_label();
33852            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33853
33854            let r_val = b.alloc_reg();
33855            let r_out = b.alloc_reg();
33856            let rowset_reg = b.alloc_reg();
33857
33858            // Test for 99 on empty rowset — should fall through and add 99
33859            b.emit_op(Opcode::Integer, 99, r_val, 0, P4::None, 0);
33860            b.emit_jump_to_label(Opcode::RowSetTest, rowset_reg, r_val, found, P4::None, 0);
33861
33862            // Fall-through: 99 was added, now read it back
33863            b.resolve_label(loop_start);
33864            b.emit_jump_to_label(
33865                Opcode::RowSetRead,
33866                rowset_reg,
33867                r_out,
33868                exhausted,
33869                P4::None,
33870                0,
33871            );
33872            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33873            b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33874
33875            b.resolve_label(found);
33876            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33877            b.resolve_label(exhausted);
33878            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33879            b.resolve_label(end);
33880        });
33881
33882        assert_eq!(rows.len(), 1);
33883        assert_eq!(rows[0][0].to_integer(), 99);
33884    }
33885
33886    // ── FK counter opcode tests ──────────────────────────────────
33887
33888    #[test]
33889    fn test_fk_counter_and_fk_if_zero() {
33890        // FkCounter increments, FkIfZero tests for zero.
33891        let rows = run_program(|b| {
33892            let end = b.emit_label();
33893            let is_zero = b.emit_label();
33894            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33895
33896            let r_out = b.alloc_reg();
33897
33898            // Increment FK counter by 3
33899            b.emit_op(Opcode::FkCounter, 0, 3, 0, P4::None, 0);
33900            // Test if zero — should NOT jump (counter is 3)
33901            b.emit_jump_to_label(Opcode::FkIfZero, 0, 0, is_zero, P4::None, 0);
33902            // Decrement by 3
33903            b.emit_op(Opcode::FkCounter, 0, -3, 0, P4::None, 0);
33904            // Test if zero — SHOULD jump now
33905            b.emit_jump_to_label(Opcode::FkIfZero, 0, 0, is_zero, P4::None, 0);
33906            // Should not reach here
33907            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
33908            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33909            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33910
33911            b.resolve_label(is_zero);
33912            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
33913            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33914            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33915            b.resolve_label(end);
33916        });
33917
33918        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33919    }
33920
33921    // ── MemMax opcode test ───────────────────────────────────────
33922
33923    #[test]
33924    fn test_memmax_stores_larger_value() {
33925        let rows = run_program(|b| {
33926            let end = b.emit_label();
33927            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33928
33929            let r1 = b.alloc_reg();
33930            let r2 = b.alloc_reg();
33931
33932            // r1=50, r2=30 → MemMax(r1, r2) → r2=50
33933            b.emit_op(Opcode::Integer, 50, r1, 0, P4::None, 0);
33934            b.emit_op(Opcode::Integer, 30, r2, 0, P4::None, 0);
33935            b.emit_op(Opcode::MemMax, r1, r2, 0, P4::None, 0);
33936            b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
33937
33938            // r1=10, r2=50 → MemMax(r1, r2) → r2 stays 50
33939            // Note: ResultRow clears registers, so r2 must be re-initialized.
33940            b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
33941            b.emit_op(Opcode::Integer, 50, r2, 0, P4::None, 0);
33942            b.emit_op(Opcode::MemMax, r1, r2, 0, P4::None, 0);
33943            b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
33944
33945            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33946            b.resolve_label(end);
33947        });
33948
33949        assert_eq!(
33950            rows,
33951            vec![
33952                vec![SqliteValue::Integer(50)],
33953                vec![SqliteValue::Integer(50)],
33954            ]
33955        );
33956    }
33957
33958    // ── OffsetLimit opcode test ──────────────────────────────────
33959
33960    #[test]
33961    fn test_offset_limit_combines_values() {
33962        let rows = run_program(|b| {
33963            let end = b.emit_label();
33964            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33965
33966            let r_limit = b.alloc_reg();
33967            let r_offset = b.alloc_reg();
33968            let r_combined = b.alloc_reg();
33969
33970            // LIMIT=10, OFFSET=5 → combined=15
33971            b.emit_op(Opcode::Integer, 10, r_limit, 0, P4::None, 0);
33972            b.emit_op(Opcode::Integer, 5, r_offset, 0, P4::None, 0);
33973            b.emit_op(
33974                Opcode::OffsetLimit,
33975                r_limit,
33976                r_offset,
33977                r_combined,
33978                P4::None,
33979                0,
33980            );
33981            b.emit_op(Opcode::ResultRow, r_combined, 1, 0, P4::None, 0);
33982
33983            // LIMIT=-1 (no limit), OFFSET=5 → combined=-1
33984            b.emit_op(Opcode::Integer, -1, r_limit, 0, P4::None, 0);
33985            b.emit_op(
33986                Opcode::OffsetLimit,
33987                r_limit,
33988                r_offset,
33989                r_combined,
33990                P4::None,
33991                0,
33992            );
33993            b.emit_op(Opcode::ResultRow, r_combined, 1, 0, P4::None, 0);
33994
33995            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33996            b.resolve_label(end);
33997        });
33998
33999        assert_eq!(
34000            rows,
34001            vec![
34002                vec![SqliteValue::Integer(15)],
34003                vec![SqliteValue::Integer(-1)],
34004            ]
34005        );
34006    }
34007
34008    // ── DecrJumpZero opcode test ─────────────────────────────────
34009
34010    #[test]
34011    fn test_decr_jump_zero_via_hot_path() {
34012        // Pins the hot-path arm's behaviour to the main-match arm:
34013        // - counter > 1: decrement, fall through (pc += 1)
34014        // - counter == 1: decrement to 0, jump to p2
34015        // - counter == 0: fall through (no write, no jump)
34016        //
34017        // `ResultRow` drains its source registers via `take_reg_range`, so
34018        // the asserts below reflect that move-out.
34019        //
34020        // Trace (r_counter seeded to 2, r_hit to 0):
34021        //   DecrJumpZero   val=2→1, fall through          (r_hit=0)
34022        //   AddImm r_hit,1 r_hit=1
34023        //   ResultRow r_hit                               emits [1], r_hit=NULL
34024        //   DecrJumpZero   val=1→0, jump over AddImm      (r_hit=NULL)
34025        //   ResultRow r_hit                               emits [NULL]
34026        //   DecrJumpZero   val=0 not>0, fall through      (r_counter still 0)
34027        //   AddImm r_hit,1 r_hit = NULL.to_integer() + 1 = 1
34028        //   ResultRow r_counter                           emits [0]
34029        let rows = run_program(|b| {
34030            let end = b.emit_label();
34031            let skip1 = b.emit_label();
34032            let skip2 = b.emit_label();
34033            let skip3 = b.emit_label();
34034            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34035
34036            let r_counter = b.alloc_reg();
34037            let r_hit = b.alloc_reg();
34038            b.emit_op(Opcode::Integer, 2, r_counter, 0, P4::None, 0);
34039            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34040
34041            b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip1, P4::None, 0);
34042            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34043            b.resolve_label(skip1);
34044            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34045
34046            b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip2, P4::None, 0);
34047            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34048            b.resolve_label(skip2);
34049            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34050
34051            b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip3, P4::None, 0);
34052            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34053            b.resolve_label(skip3);
34054            b.emit_op(Opcode::ResultRow, r_counter, 1, 0, P4::None, 0);
34055
34056            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34057            b.resolve_label(end);
34058        });
34059
34060        assert_eq!(
34061            rows,
34062            vec![
34063                vec![SqliteValue::Integer(1)],
34064                vec![SqliteValue::Null],
34065                vec![SqliteValue::Integer(0)],
34066            ]
34067        );
34068    }
34069
34070    // ── IfPos opcode test ────────────────────────────────────────
34071
34072    #[test]
34073    fn test_if_pos_via_hot_path() {
34074        // Pins the hot-path arm's behaviour to the main-match arm:
34075        // - val > 0: subtract p3, jump to p2 (taken branch)
34076        // - val == 0: fall through (no write, no jump)
34077        // - val < 0: fall through (no write, no jump)
34078        //
34079        // `ResultRow` drains its source register via `take_reg_range`, so
34080        // the asserts below reflect that move-out.
34081        //
34082        // Trace (r_offset seeded to 2, r_hit to 0; each IfPos has p3=1):
34083        //   IfPos r_offset → val=2>0: val-1=1, jump skip1 (skip AddImm)  r_offset=1, r_hit=0
34084        //   skip1: ResultRow r_hit                                       emits [0], r_hit=NULL
34085        //   IfPos r_offset → val=1>0: val-1=0, jump skip2 (skip AddImm)  r_offset=0, r_hit=NULL
34086        //   skip2: ResultRow r_hit                                       emits [NULL]
34087        //   IfPos r_offset → val=0 not>0, fall through                   r_offset=0
34088        //   AddImm r_hit,1                                               r_hit = NULL.to_integer()+1 = 1
34089        //   skip3: ResultRow r_offset                                    emits [0]
34090        let rows = run_program(|b| {
34091            let end = b.emit_label();
34092            let skip1 = b.emit_label();
34093            let skip2 = b.emit_label();
34094            let skip3 = b.emit_label();
34095            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34096
34097            let r_offset = b.alloc_reg();
34098            let r_hit = b.alloc_reg();
34099            b.emit_op(Opcode::Integer, 2, r_offset, 0, P4::None, 0);
34100            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34101
34102            // p3=1 (decrement-by-one OFFSET shape).
34103            b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip1, P4::None, 0);
34104            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34105            b.resolve_label(skip1);
34106            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34107
34108            b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip2, P4::None, 0);
34109            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34110            b.resolve_label(skip2);
34111            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34112
34113            b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip3, P4::None, 0);
34114            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34115            b.resolve_label(skip3);
34116            b.emit_op(Opcode::ResultRow, r_offset, 1, 0, P4::None, 0);
34117
34118            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34119            b.resolve_label(end);
34120        });
34121
34122        assert_eq!(
34123            rows,
34124            vec![
34125                vec![SqliteValue::Integer(0)],
34126                vec![SqliteValue::Null],
34127                vec![SqliteValue::Integer(0)],
34128            ]
34129        );
34130    }
34131
34132    // ── IsNull opcode test ───────────────────────────────────────
34133
34134    #[test]
34135    fn test_is_null_via_hot_path() {
34136        // Pins the hot-path arm's behaviour to the main-match arm:
34137        // - val IS NULL: jump to p2 (taken branch)
34138        // - val IS NOT NULL: fall through (no jump, no register write)
34139        //
34140        // The hot-path arm only covers control flow — neither branch
34141        // mutates the source register, so the seeded value stays put
34142        // across each IsNull dispatch.
34143        //
34144        // `ResultRow` drains its source register via `take_reg_range`,
34145        // so the asserts below reflect that move-out.
34146        //
34147        // Trace (r_null seeded to NULL, r_int seeded to 7, r_hit to 0):
34148        //   IsNull r_null  → NULL: jump skip1 (skip AddImm)            r_hit=0
34149        //   skip1: ResultRow r_hit                                     emits [0], r_hit=NULL
34150        //   IsNull r_int   → not NULL: fall through                    r_hit=NULL
34151        //   AddImm r_hit,1 r_hit = NULL.to_integer()+1 = 1
34152        //   skip2: ResultRow r_hit                                     emits [1], r_hit=NULL
34153        //   IsNull r_null  → NULL: jump skip3 (skip AddImm)            r_hit=NULL
34154        //   skip3: ResultRow r_int                                     emits [7]
34155        let rows = run_program(|b| {
34156            let end = b.emit_label();
34157            let skip1 = b.emit_label();
34158            let skip2 = b.emit_label();
34159            let skip3 = b.emit_label();
34160            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34161
34162            let r_null = b.alloc_reg();
34163            let r_int = b.alloc_reg();
34164            let r_hit = b.alloc_reg();
34165            // r_null left as the default Null seed; explicitly set the
34166            // others so the test reads as a complete state machine
34167            // even if `alloc_reg`'s initial value ever changes.
34168            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34169            b.emit_op(Opcode::Integer, 7, r_int, 0, P4::None, 0);
34170            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34171
34172            b.emit_jump_to_label(Opcode::IsNull, r_null, 0, skip1, P4::None, 0);
34173            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34174            b.resolve_label(skip1);
34175            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34176
34177            b.emit_jump_to_label(Opcode::IsNull, r_int, 0, skip2, P4::None, 0);
34178            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34179            b.resolve_label(skip2);
34180            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34181
34182            b.emit_jump_to_label(Opcode::IsNull, r_null, 0, skip3, P4::None, 0);
34183            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34184            b.resolve_label(skip3);
34185            b.emit_op(Opcode::ResultRow, r_int, 1, 0, P4::None, 0);
34186
34187            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34188            b.resolve_label(end);
34189        });
34190
34191        assert_eq!(
34192            rows,
34193            vec![
34194                vec![SqliteValue::Integer(0)],
34195                vec![SqliteValue::Integer(1)],
34196                vec![SqliteValue::Integer(7)],
34197            ]
34198        );
34199    }
34200
34201    #[test]
34202    fn test_not_null_main_dispatch() {
34203        // Pins the main interpreter arm's behaviour after the hot-dispatch
34204        // arm was removed:
34205        // - val IS NULL: fall through
34206        // - val IS NOT NULL: jump to p2
34207        let rows = run_program(|b| {
34208            let end = b.emit_label();
34209            let skip1 = b.emit_label();
34210            let skip2 = b.emit_label();
34211            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34212
34213            let r_null = b.alloc_reg();
34214            let r_int = b.alloc_reg();
34215            let r_hit = b.alloc_reg();
34216            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34217            b.emit_op(Opcode::Integer, 7, r_int, 0, P4::None, 0);
34218            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34219
34220            b.emit_jump_to_label(Opcode::NotNull, r_null, 0, skip1, P4::None, 0);
34221            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34222            b.resolve_label(skip1);
34223
34224            b.emit_jump_to_label(Opcode::NotNull, r_int, 0, skip2, P4::None, 0);
34225            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34226            b.resolve_label(skip2);
34227
34228            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34229            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34230            b.resolve_label(end);
34231        });
34232
34233        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34234    }
34235
34236    #[test]
34237    fn test_if_not_via_hot_path() {
34238        // Pins the hot-path arm's behaviour to the main-match arm
34239        // across all three IfNot input classes:
34240        //   - truthy non-zero integer:    fall through (no jump)
34241        //   - falsy zero integer:         jump (taken)
34242        //   - NULL with p3=0:             fall through (don't jump)
34243        //   - NULL with p3=1:             jump (taken)
34244        //
34245        // The hot-path arm only covers control flow — neither branch
34246        // mutates the source register, so the seeded values stay put
34247        // across each IfNot dispatch.  Each block uses an AddImm
34248        // sentinel after the IfNot to prove which side ran.
34249        //
34250        // Trace (r_truthy=1, r_falsy=0, r_null=NULL, r_hit=0):
34251        //   IfNot r_truthy → 1 truthy: fall through; AddImm r_hit,1 → r_hit=1
34252        //   skip1: ResultRow r_hit                              emits [1], r_hit=NULL
34253        //   r_hit=0
34254        //   IfNot r_falsy  → 0 falsy: jump skip2 (skip AddImm); r_hit=0
34255        //   skip2: ResultRow r_hit                              emits [0], r_hit=NULL
34256        //   r_hit=0
34257        //   IfNot r_null,p3=0 → NULL && p3==0: fall through; AddImm r_hit,1 → r_hit=1
34258        //   skip3: ResultRow r_hit                              emits [1], r_hit=NULL
34259        //   r_hit=0
34260        //   IfNot r_null,p3=1 → NULL && p3!=0: jump skip4 (skip AddImm); r_hit=0
34261        //   skip4: ResultRow r_hit                              emits [0]
34262        let rows = run_program(|b| {
34263            let end = b.emit_label();
34264            let skip1 = b.emit_label();
34265            let skip2 = b.emit_label();
34266            let skip3 = b.emit_label();
34267            let skip4 = b.emit_label();
34268            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34269
34270            let r_truthy = b.alloc_reg();
34271            let r_falsy = b.alloc_reg();
34272            let r_null = b.alloc_reg();
34273            let r_hit = b.alloc_reg();
34274            b.emit_op(Opcode::Integer, 1, r_truthy, 0, P4::None, 0);
34275            b.emit_op(Opcode::Integer, 0, r_falsy, 0, P4::None, 0);
34276            b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34277
34278            // Block 1: truthy → fall through
34279            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34280            b.emit_jump_to_label(Opcode::IfNot, r_truthy, 0, skip1, P4::None, 0);
34281            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34282            b.resolve_label(skip1);
34283            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34284
34285            // Block 2: falsy → jump
34286            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34287            b.emit_jump_to_label(Opcode::IfNot, r_falsy, 0, skip2, P4::None, 0);
34288            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34289            b.resolve_label(skip2);
34290            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34291
34292            // Block 3: NULL with p3=0 → fall through
34293            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34294            b.emit_jump_to_label(Opcode::IfNot, r_null, 0, skip3, P4::None, 0);
34295            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34296            b.resolve_label(skip3);
34297            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34298
34299            // Block 4: NULL with p3=1 → jump
34300            b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34301            b.emit_jump_to_label(Opcode::IfNot, r_null, 1, skip4, P4::None, 0);
34302            b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34303            b.resolve_label(skip4);
34304            b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34305
34306            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34307            b.resolve_label(end);
34308        });
34309
34310        assert_eq!(
34311            rows,
34312            vec![
34313                vec![SqliteValue::Integer(1)],
34314                vec![SqliteValue::Integer(0)],
34315                vec![SqliteValue::Integer(1)],
34316                vec![SqliteValue::Integer(0)],
34317            ]
34318        );
34319    }
34320
34321    // ── Rowid opcode test ────────────────────────────────────────
34322
34323    #[test]
34324    fn test_rowid_via_hot_path() {
34325        // Pins the hot-path arm's behaviour to the main-match arm
34326        // across the two Rowid input classes that share a single
34327        // `cursor_rowid` call:
34328        //   - storage cursor positioned on a row → emit Integer(rowid)
34329        //   - cursor id never opened            → emit Null
34330        //
34331        // The hot-path arm is a verbatim copy of the main-match arm
34332        // (`val = self.cursor_rowid(cursor_id)?; set_reg_fast(p2, val)`),
34333        // so any divergence between the two would surface here.
34334        //
34335        // Layout note: the bytecode verifier requires Rewind p2 to be
34336        // strictly `< op_count`, so the Rewind EOF target must point at
34337        // a real instruction (we use the Halt at the program end via a
34338        // label resolved *at* that Halt).  Init is omitted entirely:
34339        // the engine starts execution at pc=0 unconditionally and no
34340        // verifier path requires Init's presence.
34341        let mut db = MemDatabase::new();
34342        let root = db.create_table(1);
34343        let table = db.get_table_mut(root).unwrap();
34344        table.insert(7, vec![SqliteValue::Integer(70)]);
34345        table.insert(11, vec![SqliteValue::Integer(110)]);
34346
34347        let (rows, _) = run_write_with_storage_cursors(db, |b| {
34348            let halt = b.emit_label();
34349
34350            // Probe a never-opened cursor id first → expects Null.  This
34351            // exercises the `cursor_rowid` fallthrough where neither
34352            // `storage_cursors` nor `vtab_cursors` nor `cursors` has a
34353            // matching id.
34354            let r_unopened = b.alloc_reg();
34355            b.emit_op(Opcode::Rowid, 99, r_unopened, 0, P4::None, 0);
34356            b.emit_op(Opcode::ResultRow, r_unopened, 1, 0, P4::None, 0);
34357
34358            // Open writable cursor 0 and Rewind onto rowid=7.  EOF
34359            // jumps to Halt directly.
34360            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
34361            b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt, P4::None, 0);
34362
34363            // Body: Rowid → ResultRow → Next loops until EOF, then
34364            // falls through to Halt.
34365            let body = b.current_addr();
34366            let r_out = b.alloc_reg();
34367            b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
34368            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34369            let next_target =
34370                i32::try_from(body).expect("program counter should fit into i32 for tests");
34371            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
34372
34373            b.resolve_label(halt);
34374            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34375        });
34376
34377        assert_eq!(
34378            rows,
34379            vec![
34380                vec![SqliteValue::Null],
34381                vec![SqliteValue::Integer(7)],
34382                vec![SqliteValue::Integer(11)],
34383            ]
34384        );
34385    }
34386
34387    // ── IdxRowid opcode test ─────────────────────────────────────
34388
34389    #[test]
34390    fn test_idx_rowid_via_hot_path() {
34391        // Pins the hot-path arm's behaviour to the main-match arm.
34392        // IdxRowid shares the `cursor_rowid` helper with Rowid, so the
34393        // hot-path arm is byte-equivalent to the main-match arm; this
34394        // test exercises both input classes that helper handles:
34395        //   - storage cursor positioned on a row → emit Integer(rowid)
34396        //   - cursor id never opened            → emit Null
34397        let mut db = MemDatabase::new();
34398        let root = db.create_table(1);
34399        let table = db.get_table_mut(root).unwrap();
34400        table.insert(13, vec![SqliteValue::Integer(130)]);
34401        table.insert(17, vec![SqliteValue::Integer(170)]);
34402
34403        let (rows, _) = run_write_with_storage_cursors(db, |b| {
34404            let halt = b.emit_label();
34405
34406            // Probe a never-opened cursor id first → expects Null.
34407            let r_unopened = b.alloc_reg();
34408            b.emit_op(Opcode::IdxRowid, 99, r_unopened, 0, P4::None, 0);
34409            b.emit_op(Opcode::ResultRow, r_unopened, 1, 0, P4::None, 0);
34410
34411            b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
34412            b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt, P4::None, 0);
34413
34414            let body = b.current_addr();
34415            let r_out = b.alloc_reg();
34416            b.emit_op(Opcode::IdxRowid, 0, r_out, 0, P4::None, 0);
34417            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34418            let next_target =
34419                i32::try_from(body).expect("program counter should fit into i32 for tests");
34420            b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
34421
34422            b.resolve_label(halt);
34423            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34424        });
34425
34426        assert_eq!(
34427            rows,
34428            vec![
34429                vec![SqliteValue::Null],
34430                vec![SqliteValue::Integer(13)],
34431                vec![SqliteValue::Integer(17)],
34432            ]
34433        );
34434    }
34435
34436    // ── IfNotZero opcode test ────────────────────────────────────
34437
34438    #[test]
34439    fn test_if_not_zero_decrements_and_jumps() {
34440        // Start with 2 in register, loop with IfNotZero until it reaches 0.
34441        let rows = run_program(|b| {
34442            let end = b.emit_label();
34443            let loop_start = b.emit_label();
34444            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34445
34446            let r_counter = b.alloc_reg();
34447            let r_count = b.alloc_reg();
34448
34449            b.emit_op(Opcode::Integer, 3, r_counter, 0, P4::None, 0);
34450            b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
34451
34452            b.resolve_label(loop_start);
34453            // Count iterations
34454            b.emit_op(Opcode::AddImm, r_count, 1, 0, P4::None, 0);
34455            // Decrement and jump if not zero
34456            b.emit_jump_to_label(Opcode::IfNotZero, r_counter, 0, loop_start, P4::None, 0);
34457
34458            // When counter reaches 0, output iteration count
34459            b.emit_op(Opcode::ResultRow, r_count, 1, 0, P4::None, 0);
34460            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34461            b.resolve_label(end);
34462        });
34463
34464        // Loop: AddImm then IfNotZero.
34465        // iter 1: count=1, 3→2 jump; iter 2: count=2, 2→1 jump;
34466        // iter 3: count=3, 1→0 jump; iter 4: count=4, 0→fall through.
34467        assert_eq!(rows, vec![vec![SqliteValue::Integer(4)]]);
34468    }
34469
34470    // ── Pagecount / MaxPgcnt / JournalMode / IntegrityCk ─────────
34471
34472    #[test]
34473    fn test_pagecount_returns_table_count() {
34474        let rows = run_program(|b| {
34475            let end = b.emit_label();
34476            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34477
34478            let r_out = b.alloc_reg();
34479            b.emit_op(Opcode::Pagecount, 0, r_out, 0, P4::None, 0);
34480            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34481            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34482            b.resolve_label(end);
34483        });
34484
34485        // No database set → 0 pages
34486        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34487    }
34488
34489    #[test]
34490    fn test_max_pgcnt_returns_large_value() {
34491        let rows = run_program(|b| {
34492            let end = b.emit_label();
34493            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34494
34495            let r_out = b.alloc_reg();
34496            b.emit_op(Opcode::MaxPgcnt, 0, r_out, 0, P4::None, 0);
34497            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34498            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34499            b.resolve_label(end);
34500        });
34501
34502        assert_eq!(rows, vec![vec![SqliteValue::Integer(1_073_741_823)]]);
34503    }
34504
34505    #[test]
34506    fn test_journal_mode_returns_wal() {
34507        let rows = run_program(|b| {
34508            let end = b.emit_label();
34509            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34510
34511            let r_out = b.alloc_reg();
34512            b.emit_op(Opcode::JournalMode, 0, r_out, 0, P4::None, 0);
34513            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34514            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34515            b.resolve_label(end);
34516        });
34517
34518        assert_eq!(rows, vec![vec![SqliteValue::Text("wal".into())]]);
34519    }
34520
34521    #[test]
34522    fn test_integrity_ck_returns_ok() {
34523        let rows = run_program(|b| {
34524            let end = b.emit_label();
34525            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34526
34527            let r_root = b.alloc_reg();
34528            let r_out = b.alloc_reg();
34529            b.emit_op(Opcode::Integer, 1, r_root, 0, P4::None, 0);
34530            b.emit_op(Opcode::IntegrityCk, r_root, r_out, 1, P4::None, 0);
34531            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34532            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34533            b.resolve_label(end);
34534        });
34535
34536        assert_eq!(rows, vec![vec![SqliteValue::Text("ok".into())]]);
34537    }
34538
34539    // ── Vacuum and IncrVacuum ────────────────────────────────────
34540
34541    #[test]
34542    fn test_vacuum_is_noop() {
34543        let rows = run_program(|b| {
34544            let end = b.emit_label();
34545            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34546
34547            let r_out = b.alloc_reg();
34548            b.emit_op(Opcode::Vacuum, 0, 0, 0, P4::None, 0);
34549            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34550            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34551            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34552            b.resolve_label(end);
34553        });
34554
34555        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34556    }
34557    #[derive(Debug, Clone, PartialEq)]
34558    struct RecordedFilterCall {
34559        idx_num: i32,
34560        idx_str: Option<String>,
34561        args: Vec<SqliteValue>,
34562    }
34563
34564    /// Mock virtual table cursor for testing VTable opcodes.
34565    #[derive(Clone)]
34566    struct MockVtabCursor {
34567        rows: Vec<Vec<SqliteValue>>,
34568        pos: usize,
34569        filtered: bool,
34570        filter_capture: Option<Arc<std::sync::Mutex<Option<RecordedFilterCall>>>>,
34571        cancel_on_filter: bool,
34572        interrupt_on_filter: bool,
34573        cancel_on_column: Option<Cx>,
34574    }
34575
34576    impl MockVtabCursor {
34577        fn new(rows: Vec<Vec<SqliteValue>>) -> Self {
34578            Self {
34579                rows,
34580                pos: 0,
34581                filtered: false,
34582                filter_capture: None,
34583                cancel_on_filter: false,
34584                interrupt_on_filter: false,
34585                cancel_on_column: None,
34586            }
34587        }
34588
34589        fn with_filter_capture(
34590            rows: Vec<Vec<SqliteValue>>,
34591            filter_capture: Arc<std::sync::Mutex<Option<RecordedFilterCall>>>,
34592        ) -> Self {
34593            Self {
34594                rows,
34595                pos: 0,
34596                filtered: false,
34597                filter_capture: Some(filter_capture),
34598                cancel_on_filter: false,
34599                interrupt_on_filter: false,
34600                cancel_on_column: None,
34601            }
34602        }
34603
34604        fn with_filter_child_cancel(rows: Vec<Vec<SqliteValue>>) -> Self {
34605            Self {
34606                rows,
34607                pos: 0,
34608                filtered: false,
34609                filter_capture: None,
34610                cancel_on_filter: true,
34611                interrupt_on_filter: false,
34612                cancel_on_column: None,
34613            }
34614        }
34615
34616        fn with_filter_interrupt(rows: Vec<Vec<SqliteValue>>) -> Self {
34617            Self {
34618                rows,
34619                pos: 0,
34620                filtered: false,
34621                filter_capture: None,
34622                cancel_on_filter: false,
34623                interrupt_on_filter: true,
34624                cancel_on_column: None,
34625            }
34626        }
34627
34628        fn with_column_cancel(rows: Vec<Vec<SqliteValue>>, cancel_cx: Cx) -> Self {
34629            Self {
34630                rows,
34631                pos: 0,
34632                filtered: false,
34633                filter_capture: None,
34634                cancel_on_filter: false,
34635                interrupt_on_filter: false,
34636                cancel_on_column: Some(cancel_cx),
34637            }
34638        }
34639    }
34640
34641    impl VirtualTableCursor for MockVtabCursor {
34642        fn filter(
34643            &mut self,
34644            cx: &Cx,
34645            idx_num: i32,
34646            idx_str: Option<&str>,
34647            args: &[SqliteValue],
34648        ) -> Result<()> {
34649            if self.interrupt_on_filter {
34650                return Err(FrankenError::Abort);
34651            }
34652            if self.cancel_on_filter {
34653                cx.cancel();
34654            }
34655            if let Some(filter_capture) = &self.filter_capture {
34656                *filter_capture.lock().expect("filter capture lock poisoned") =
34657                    Some(RecordedFilterCall {
34658                        idx_num,
34659                        idx_str: idx_str.map(str::to_owned),
34660                        args: args.to_vec(),
34661                    });
34662            }
34663            self.pos = 0;
34664            self.filtered = true;
34665            Ok(())
34666        }
34667        fn next(&mut self, _cx: &Cx) -> Result<()> {
34668            self.pos += 1;
34669            Ok(())
34670        }
34671        fn eof(&self) -> bool {
34672            self.pos >= self.rows.len()
34673        }
34674        fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
34675            if let Some(cancel_cx) = &self.cancel_on_column {
34676                cancel_cx.cancel();
34677            }
34678            #[allow(clippy::cast_sign_loss)]
34679            let val = self
34680                .rows
34681                .get(self.pos)
34682                .and_then(|row| row.get(col as usize))
34683                .cloned()
34684                .unwrap_or(SqliteValue::Null);
34685            ctx.set_value(val);
34686            Ok(())
34687        }
34688        fn rowid(&self) -> Result<i64> {
34689            #[allow(clippy::cast_possible_wrap)]
34690            Ok(self.pos as i64 + 1)
34691        }
34692    }
34693
34694    struct MockVtab {
34695        cursor_template: MockVtabCursor,
34696        cancel_on_begin: bool,
34697        interrupt_on_begin: bool,
34698    }
34699
34700    impl MockVtab {
34701        fn new(cursor_template: MockVtabCursor) -> Self {
34702            Self {
34703                cursor_template,
34704                cancel_on_begin: false,
34705                interrupt_on_begin: false,
34706            }
34707        }
34708
34709        fn with_begin_child_cancel(cursor_template: MockVtabCursor) -> Self {
34710            Self {
34711                cursor_template,
34712                cancel_on_begin: true,
34713                interrupt_on_begin: false,
34714            }
34715        }
34716
34717        fn with_begin_interrupt(cursor_template: MockVtabCursor) -> Self {
34718            Self {
34719                cursor_template,
34720                cancel_on_begin: false,
34721                interrupt_on_begin: true,
34722            }
34723        }
34724    }
34725
34726    impl VirtualTable for MockVtab {
34727        type Cursor = MockVtabCursor;
34728
34729        fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
34730            Ok(Self::new(MockVtabCursor::new(Vec::new())))
34731        }
34732
34733        fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
34734            Ok(())
34735        }
34736
34737        fn open(&self) -> Result<Self::Cursor> {
34738            Ok(self.cursor_template.clone())
34739        }
34740
34741        fn begin(&mut self, cx: &Cx) -> Result<()> {
34742            if self.interrupt_on_begin {
34743                return Err(FrankenError::Abort);
34744            }
34745            if self.cancel_on_begin {
34746                cx.cancel();
34747            }
34748            Ok(())
34749        }
34750    }
34751
34752    /// Helper: build a program, register a vtab instance, then execute.
34753    fn run_vtab_program(
34754        cursor_id: i32,
34755        cursor: MockVtabCursor,
34756        build: impl FnOnce(&mut ProgramBuilder),
34757    ) -> (Vec<Vec<SqliteValue>>, ExecOutcome) {
34758        let mut b = ProgramBuilder::new();
34759        build(&mut b);
34760        let prog = b.finish().expect("program should build");
34761        let mut engine = VdbeEngine::new(prog.register_count());
34762        engine.register_vtab_instance(cursor_id, Box::new(MockVtab::new(cursor)));
34763        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
34764        (
34765            engine
34766                .take_results()
34767                .into_iter()
34768                .map(|v| v.into_vec())
34769                .collect(),
34770            outcome,
34771        )
34772    }
34773
34774    fn emit_vfilter_noargs(b: &mut ProgramBuilder, cursor_id: i32, jump_label: Label) {
34775        let r_idx_num = b.alloc_reg();
34776        let r_argc = b.alloc_reg();
34777        debug_assert_eq!(r_argc, r_idx_num + 1);
34778        b.emit_op(Opcode::Integer, 0, r_idx_num, 0, P4::None, 0);
34779        b.emit_op(Opcode::Integer, 0, r_argc, 0, P4::None, 0);
34780        b.emit_jump_to_label(
34781            Opcode::VFilter,
34782            cursor_id,
34783            r_idx_num,
34784            jump_label,
34785            P4::None,
34786            0,
34787        );
34788    }
34789
34790    #[test]
34791    fn test_vopen_is_noop() {
34792        let rows = run_program(|b| {
34793            let end = b.emit_label();
34794            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34795            let r_out = b.alloc_reg();
34796            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34797            b.emit_op(Opcode::Integer, 42, r_out, 0, P4::None, 0);
34798            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34799            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34800            b.resolve_label(end);
34801        });
34802        assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
34803    }
34804
34805    #[test]
34806    fn test_vcreate_is_noop() {
34807        let rows = run_program(|b| {
34808            let end = b.emit_label();
34809            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34810            let r_out = b.alloc_reg();
34811            b.emit_op(
34812                Opcode::VCreate,
34813                0,
34814                0,
34815                0,
34816                P4::Str("test_module".to_owned()),
34817                0,
34818            );
34819            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34820            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34821            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34822            b.resolve_label(end);
34823        });
34824        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34825    }
34826
34827    #[test]
34828    fn test_vdestroy_is_noop() {
34829        let rows = run_program(|b| {
34830            let end = b.emit_label();
34831            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34832            let r_out = b.alloc_reg();
34833            b.emit_op(
34834                Opcode::VDestroy,
34835                0,
34836                0,
34837                0,
34838                P4::Str("test_table".to_owned()),
34839                0,
34840            );
34841            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34842            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34843            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34844            b.resolve_label(end);
34845        });
34846        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34847    }
34848    #[test]
34849    fn test_vcheck_stores_null_in_p2_without_clobbering_p3_argument() {
34850        let rows = run_program(|b| {
34851            let end = b.emit_label();
34852            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34853            let r_out = b.alloc_reg();
34854            let r_arg = b.alloc_reg();
34855            b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
34856            b.emit_op(Opcode::Integer, 123, r_arg, 0, P4::None, 0);
34857            b.emit_op(Opcode::VCheck, 0, r_out, r_arg, P4::None, 0);
34858            b.emit_op(Opcode::ResultRow, r_out, 2, 0, P4::None, 0);
34859            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34860            b.resolve_label(end);
34861        });
34862        assert_eq!(
34863            rows,
34864            vec![vec![SqliteValue::Null, SqliteValue::Integer(123)]]
34865        );
34866    }
34867
34868    #[test]
34869    fn test_vupdate_stores_null_in_dest() {
34870        // VUpdate P1=cursor, P2=n_args, P3=first_arg_reg, P5=dest_reg.
34871        // When no vtab instance is registered for the cursor, VUpdate
34872        // writes Null into dest_reg (P5).
34873        let rows = run_program(|b| {
34874            let end = b.emit_label();
34875            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34876            let r_arg = b.alloc_reg();
34877            let r_dest = b.alloc_reg();
34878            b.emit_op(Opcode::Integer, 77, r_arg, 0, P4::None, 0);
34879            b.emit_op(Opcode::Integer, 99, r_dest, 0, P4::None, 0);
34880            // P1=cursor 0, P2=1 arg, P3=r_arg, P5=r_dest as dest
34881            #[allow(clippy::cast_possible_truncation)]
34882            b.emit_op(Opcode::VUpdate, 0, 1, r_arg, P4::None, r_dest as u16);
34883            b.emit_op(Opcode::ResultRow, r_dest, 1, 0, P4::None, 0);
34884            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34885            b.resolve_label(end);
34886        });
34887        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
34888    }
34889
34890    #[test]
34891    fn test_vinitin_copies_register() {
34892        let rows = run_program(|b| {
34893            let end = b.emit_label();
34894            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34895            let r_src = b.alloc_reg();
34896            let r_dst = b.alloc_reg();
34897            b.emit_op(Opcode::Integer, 123, r_src, 0, P4::None, 0);
34898            b.emit_op(Opcode::VInitIn, 0, r_src, r_dst, P4::None, 0);
34899            b.emit_op(Opcode::ResultRow, r_dst, 1, 0, P4::None, 0);
34900            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34901            b.resolve_label(end);
34902        });
34903        assert_eq!(rows, vec![vec![SqliteValue::Integer(123)]]);
34904    }
34905
34906    #[test]
34907    fn test_vfilter_jumps_on_empty_cursor() {
34908        let empty_cursor = MockVtabCursor::new(vec![]);
34909        let (rows, outcome) = run_vtab_program(0, empty_cursor, |b| {
34910            let end = b.emit_label();
34911            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34912            let r_out = b.alloc_reg();
34913            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34914            let after_scan = b.emit_label();
34915            emit_vfilter_noargs(b, 0, after_scan);
34916            b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
34917            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34918            b.resolve_label(after_scan);
34919            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
34920            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34921            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34922            b.resolve_label(end);
34923        });
34924        assert_eq!(outcome, ExecOutcome::Done);
34925        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34926    }
34927
34928    #[test]
34929    fn test_vfilter_jumps_when_cursor_is_missing() {
34930        let rows = run_program(|b| {
34931            let end = b.emit_label();
34932            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34933            let r_out = b.alloc_reg();
34934            let after_scan = b.emit_label();
34935            emit_vfilter_noargs(b, 99, after_scan);
34936            b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
34937            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34938            b.resolve_label(after_scan);
34939            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
34940            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34941            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34942            b.resolve_label(end);
34943        });
34944        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34945    }
34946
34947    #[test]
34948    fn test_vfilter_reads_idx_num_idx_str_and_args_from_registers() {
34949        let capture = Arc::new(std::sync::Mutex::new(None));
34950        let cursor = MockVtabCursor::with_filter_capture(
34951            vec![vec![SqliteValue::Integer(1)]],
34952            Arc::clone(&capture),
34953        );
34954
34955        let mut b = ProgramBuilder::new();
34956        let end = b.emit_label();
34957        let done = b.emit_label();
34958        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34959        let r_idx_num = b.alloc_reg();
34960        let r_argc = b.alloc_reg();
34961        let r_arg0 = b.alloc_reg();
34962        let r_arg1 = b.alloc_reg();
34963        debug_assert_eq!(r_argc, r_idx_num + 1);
34964        debug_assert_eq!(r_arg0, r_idx_num + 2);
34965        debug_assert_eq!(r_arg1, r_idx_num + 3);
34966        b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34967        b.emit_op(Opcode::Integer, 7, r_idx_num, 0, P4::None, 0);
34968        b.emit_op(Opcode::Integer, 2, r_argc, 0, P4::None, 0);
34969        b.emit_op(Opcode::Integer, 42, r_arg0, 0, P4::None, 0);
34970        b.emit_op(
34971            Opcode::String8,
34972            0,
34973            r_arg1,
34974            0,
34975            P4::Str("needle".to_owned()),
34976            0,
34977        );
34978        b.emit_jump_to_label(
34979            Opcode::VFilter,
34980            0,
34981            r_idx_num,
34982            done,
34983            P4::Str("range:eq".to_owned()),
34984            0,
34985        );
34986        b.resolve_label(done);
34987        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34988        b.resolve_label(end);
34989        let prog = b.finish().expect("program should build");
34990
34991        let mut engine = VdbeEngine::new(prog.register_count());
34992        engine.register_vtab_instance(0, Box::new(MockVtab::new(cursor)));
34993        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
34994        assert_eq!(outcome, ExecOutcome::Done);
34995        assert_eq!(
34996            capture
34997                .lock()
34998                .expect("filter capture lock poisoned")
34999                .clone(),
35000            Some(RecordedFilterCall {
35001                idx_num: 7,
35002                idx_str: Some("range:eq".to_owned()),
35003                args: vec![SqliteValue::Integer(42), SqliteValue::Text("needle".into()),],
35004            })
35005        );
35006    }
35007
35008    #[test]
35009    fn test_execute_observes_execution_cx_cancellation_immediately_after_vfilter_opcode() {
35010        let root_cx = Cx::new();
35011
35012        let mut b = ProgramBuilder::new();
35013        let end = b.emit_label();
35014        let done = b.emit_label();
35015        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35016        let r_out = b.alloc_reg();
35017        b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35018        emit_vfilter_noargs(&mut b, 0, done);
35019        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35020        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35021        b.resolve_label(done);
35022        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35023        b.resolve_label(end);
35024        let prog = b.finish().expect("program should build");
35025
35026        let mut engine =
35027            VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35028        let vtab = MockVtab::new(MockVtabCursor::with_filter_child_cancel(vec![vec![
35029            SqliteValue::Integer(7),
35030        ]]));
35031        engine.register_vtab_instance(0, Box::new(vtab));
35032
35033        let err = run_async(engine.execute(&prog))
35034            .expect_err("cancellation should be observed before VFilter advances execution");
35035        assert!(matches!(err, FrankenError::Abort));
35036        assert!(engine.take_results().is_empty());
35037    }
35038
35039    #[test]
35040    fn test_vfilter_propagates_interrupt_from_cursor() {
35041        let root_cx = Cx::new();
35042
35043        let mut b = ProgramBuilder::new();
35044        let end = b.emit_label();
35045        let done = b.emit_label();
35046        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35047        let r_out = b.alloc_reg();
35048        b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35049        emit_vfilter_noargs(&mut b, 0, done);
35050        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35051        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35052        b.resolve_label(done);
35053        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35054        b.resolve_label(end);
35055        let prog = b.finish().expect("program should build");
35056
35057        let mut engine =
35058            VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35059        let vtab = MockVtab::new(MockVtabCursor::with_filter_interrupt(vec![vec![
35060            SqliteValue::Integer(7),
35061        ]]));
35062        engine.register_vtab_instance(0, Box::new(vtab));
35063
35064        let err = run_async(engine.execute(&prog))
35065            .expect_err("VFilter interrupt should propagate without being wrapped");
35066        assert!(matches!(err, FrankenError::Abort));
35067        assert!(engine.take_results().is_empty());
35068    }
35069
35070    #[test]
35071    fn test_vfilter_vcolumn_vnext_scan_loop() {
35072        let cursor = MockVtabCursor::new(vec![
35073            vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
35074            vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
35075        ]);
35076        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35077            let end = b.emit_label();
35078            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35079            let r1 = b.alloc_reg();
35080            let r2 = b.alloc_reg();
35081            let done_label = b.emit_label();
35082            let loop_label = b.emit_label();
35083            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35084            emit_vfilter_noargs(b, 0, done_label);
35085            b.resolve_label(loop_label);
35086            b.emit_op(Opcode::VColumn, 0, 0, r1, P4::None, 0);
35087            b.emit_op(Opcode::VColumn, 0, 1, r2, P4::None, 0);
35088            b.emit_op(Opcode::ResultRow, r1, 2, 0, P4::None, 0);
35089            b.emit_jump_to_label(Opcode::VNext, 0, 0, loop_label, P4::None, 0);
35090            b.resolve_label(done_label);
35091            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35092            b.resolve_label(end);
35093        });
35094        assert_eq!(outcome, ExecOutcome::Done);
35095        assert_eq!(
35096            rows,
35097            vec![
35098                vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
35099                vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
35100            ]
35101        );
35102    }
35103
35104    #[test]
35105    fn test_vnext_no_cursor_falls_through() {
35106        let rows = run_program(|b| {
35107            let end = b.emit_label();
35108            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35109            let r_out = b.alloc_reg();
35110            let loop_label = b.emit_label();
35111            b.resolve_label(loop_label);
35112            b.emit_jump_to_label(Opcode::VNext, 99, 0, loop_label, P4::None, 0);
35113            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35114            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35115            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35116            b.resolve_label(end);
35117        });
35118        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35119    }
35120
35121    #[test]
35122    fn test_vbegin_no_instance_is_noop() {
35123        let rows = run_program(|b| {
35124            let end = b.emit_label();
35125            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35126            let r_out = b.alloc_reg();
35127            b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35128            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35129            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35130            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35131            b.resolve_label(end);
35132        });
35133        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35134    }
35135
35136    #[test]
35137    fn test_vbegin_observes_child_cx_cancellation_immediately() {
35138        let root_cx = Cx::new();
35139
35140        let mut b = ProgramBuilder::new();
35141        let end = b.emit_label();
35142        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35143        let r_out = b.alloc_reg();
35144        b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35145        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35146        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35147        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35148        b.resolve_label(end);
35149        let prog = b.finish().expect("program should build");
35150
35151        let mut engine =
35152            VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35153        let vtab = MockVtab::with_begin_child_cancel(MockVtabCursor::new(Vec::new()));
35154        engine.register_vtab_instance(0, Box::new(vtab));
35155
35156        let err = run_async(engine.execute(&prog))
35157            .expect_err("VBegin child cancellation should abort execution immediately");
35158        assert!(matches!(err, FrankenError::Abort));
35159        assert!(engine.take_results().is_empty());
35160    }
35161
35162    #[test]
35163    fn test_vbegin_propagates_interrupt_from_vtab() {
35164        let root_cx = Cx::new();
35165
35166        let mut b = ProgramBuilder::new();
35167        let end = b.emit_label();
35168        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35169        let r_out = b.alloc_reg();
35170        b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35171        b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35172        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35173        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35174        b.resolve_label(end);
35175        let prog = b.finish().expect("program should build");
35176
35177        let mut engine =
35178            VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35179        let vtab = MockVtab::with_begin_interrupt(MockVtabCursor::new(Vec::new()));
35180        engine.register_vtab_instance(0, Box::new(vtab));
35181
35182        let err = run_async(engine.execute(&prog))
35183            .expect_err("VBegin interrupt should propagate without being wrapped");
35184        assert!(matches!(err, FrankenError::Abort));
35185        assert!(engine.take_results().is_empty());
35186    }
35187
35188    #[test]
35189    fn test_vrename_no_instance_is_noop() {
35190        let rows = run_program(|b| {
35191            let end = b.emit_label();
35192            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35193            let r_out = b.alloc_reg();
35194            b.emit_op(Opcode::VRename, 0, 0, 0, P4::Str("new_name".to_owned()), 0);
35195            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35196            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35197            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35198            b.resolve_label(end);
35199        });
35200        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35201    }
35202
35203    #[test]
35204    fn test_vcolumn_no_cursor_stores_null() {
35205        let rows = run_program(|b| {
35206            let end = b.emit_label();
35207            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35208            let r_out = b.alloc_reg();
35209            b.emit_op(Opcode::Integer, 55, r_out, 0, P4::None, 0);
35210            b.emit_op(Opcode::VColumn, 99, 0, r_out, P4::None, 0);
35211            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35212            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35213            b.resolve_label(end);
35214        });
35215        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
35216    }
35217
35218    #[test]
35219    fn test_rowid_reads_from_vtab_cursor() {
35220        let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35221        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35222            let end = b.emit_label();
35223            let done = b.emit_label();
35224            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35225            let r_out = b.alloc_reg();
35226            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35227            emit_vfilter_noargs(b, 0, done);
35228            b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
35229            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35230            b.resolve_label(done);
35231            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35232            b.resolve_label(end);
35233        });
35234        assert_eq!(outcome, ExecOutcome::Done);
35235        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35236    }
35237
35238    #[test]
35239    fn test_rowid_on_vtab_cursor_at_eof_stores_null() {
35240        let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35241        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35242            let end = b.emit_label();
35243            let done = b.emit_label();
35244            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35245            let r_out = b.alloc_reg();
35246            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35247            emit_vfilter_noargs(b, 0, done);
35248            b.emit_jump_to_label(Opcode::VNext, 0, 0, done, P4::None, 0);
35249            b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
35250            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35251            b.resolve_label(done);
35252            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35253            b.resolve_label(end);
35254        });
35255        assert_eq!(outcome, ExecOutcome::Done);
35256        assert_eq!(rows, vec![vec![SqliteValue::Null]]);
35257    }
35258
35259    #[test]
35260    fn test_nullrow_on_vtab_cursor_forces_vcolumn_and_rowid_to_null() {
35261        let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35262        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35263            let end = b.emit_label();
35264            let done = b.emit_label();
35265            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35266            let r_col = b.alloc_reg();
35267            let r_rowid = b.alloc_reg();
35268            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35269            emit_vfilter_noargs(b, 0, done);
35270            b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35271            b.emit_op(Opcode::VColumn, 0, 0, r_col, P4::None, 0);
35272            b.emit_op(Opcode::Rowid, 0, r_rowid, 0, P4::None, 0);
35273            b.emit_op(Opcode::ResultRow, r_col, 2, 0, P4::None, 0);
35274            b.resolve_label(done);
35275            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35276            b.resolve_label(end);
35277        });
35278        assert_eq!(outcome, ExecOutcome::Done);
35279        assert_eq!(rows, vec![vec![SqliteValue::Null, SqliteValue::Null]]);
35280    }
35281
35282    #[test]
35283    fn test_ifnullrow_jumps_for_vtab_nullrow() {
35284        let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35285        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35286            let end = b.emit_label();
35287            let skip = b.emit_label();
35288            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35289            let r_out = b.alloc_reg();
35290            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35291            emit_vfilter_noargs(b, 0, skip);
35292            b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35293            b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, skip, P4::None, 0);
35294            b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
35295            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35296            b.resolve_label(skip);
35297            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
35298            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35299            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35300            b.resolve_label(end);
35301        });
35302        assert_eq!(outcome, ExecOutcome::Done);
35303        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
35304    }
35305
35306    #[test]
35307    fn test_vnext_on_vtab_nullrow_falls_through() {
35308        let cursor = MockVtabCursor::new(vec![
35309            vec![SqliteValue::Integer(10)],
35310            vec![SqliteValue::Integer(20)],
35311        ]);
35312        let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35313            let end = b.emit_label();
35314            let loop_body = b.emit_label();
35315            let after_loop = b.emit_label();
35316            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35317            let r_out = b.alloc_reg();
35318            b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35319            emit_vfilter_noargs(b, 0, after_loop);
35320            b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35321            b.emit_jump_to_label(Opcode::VNext, 0, 0, loop_body, P4::None, 0);
35322            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35323            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35324            b.emit_jump_to_label(Opcode::Goto, 0, 0, after_loop, P4::None, 0);
35325            b.resolve_label(loop_body);
35326            b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
35327            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35328            b.resolve_label(after_loop);
35329            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35330            b.resolve_label(end);
35331        });
35332        assert_eq!(outcome, ExecOutcome::Done);
35333        assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35334    }
35335
35336    #[test]
35337    fn test_execute_observes_execution_cx_cancellation_immediately_after_vcolumn_opcode() {
35338        let root_cx = Cx::new();
35339
35340        let mut b = ProgramBuilder::new();
35341        let end = b.emit_label();
35342        let done = b.emit_label();
35343        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35344        let r_out = b.alloc_reg();
35345        b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35346        emit_vfilter_noargs(&mut b, 0, done);
35347        b.emit_op(Opcode::VColumn, 0, 0, r_out, P4::None, 0);
35348        b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35349        b.resolve_label(done);
35350        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35351        b.resolve_label(end);
35352        let prog = b.finish().expect("program should build");
35353
35354        let mut engine =
35355            VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35356        let vtab = MockVtab::new(MockVtabCursor::with_column_cancel(
35357            vec![vec![SqliteValue::Integer(7)]],
35358            root_cx.clone(),
35359        ));
35360        engine.register_vtab_instance(0, Box::new(vtab));
35361
35362        let err = run_async(engine.execute(&prog))
35363            .expect_err("cancellation should be observed before VColumn publishes a value");
35364        assert!(matches!(err, FrankenError::Abort));
35365        assert!(engine.take_results().is_empty());
35366    }
35367
35368    // ── Time-travel (SetSnapshot) tests ──────────────────────────────────
35369
35370    #[test]
35371    fn test_set_snapshot_stores_time_travel_marker() {
35372        // Verify that SetSnapshot stores a TimeTravelMarker on the engine
35373        // AND upgrades the cursor when VersionStore, CommitLog, and GC
35374        // horizon are all provided (marker-only without infrastructure is
35375        // no longer supported after the empty-VersionStore hardening).
35376        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35377
35378        let pager = MockMvccPager;
35379        let cx = Cx::new();
35380        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35381
35382        let mut db = MemDatabase::new();
35383        let root = db.create_table(1);
35384        let table = db.get_table_mut(root).unwrap();
35385        table.insert(1, vec![SqliteValue::Integer(42)]);
35386
35387        let mut b = ProgramBuilder::new();
35388        let end = b.emit_label();
35389        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35390        b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
35391        // SetSnapshot with commit sequence 5 on cursor 0.
35392        b.emit_op(Opcode::SetSnapshot, 0, 0, 0, P4::TimeTravelCommitSeq(5), 0);
35393        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35394        b.resolve_label(end);
35395        let prog = b.finish().expect("program should build");
35396
35397        let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35398
35399        // Build a CommitLog with entries so SetSnapshot validation passes.
35400        let commit_log = {
35401            use fsqlite_types::TxnId;
35402            let mut log = CommitLog::new(CommitSeq::new(1));
35403            for seq in 1..=5 {
35404                log.append(fsqlite_mvcc::core_types::CommitRecord {
35405                    txn_id: TxnId::new(seq).unwrap(),
35406                    commit_seq: CommitSeq::new(seq),
35407                    pages: smallvec::smallvec![PageNumber::new(1).unwrap()],
35408                    timestamp_unix_ns: 1_700_000_000_000_000_000 + seq * 1_000_000_000,
35409                });
35410            }
35411            Arc::new(Mutex::new(log))
35412        };
35413
35414        let mut engine = VdbeEngine::new(prog.register_count());
35415        engine.set_database(db);
35416        engine.set_transaction(txn);
35417        engine.set_version_store(Arc::clone(&vs));
35418        engine.set_time_travel_commit_log(Arc::clone(&commit_log));
35419        engine.set_time_travel_gc_horizon(CommitSeq::new(1));
35420
35421        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
35422        assert_eq!(outcome, ExecOutcome::Done);
35423
35424        // The marker should be recorded.
35425        let marker = engine.time_travel_marker(0);
35426        assert!(
35427            marker.is_some(),
35428            "time-travel marker should be set on cursor 0"
35429        );
35430        assert!(
35431            matches!(marker, Some(TimeTravelMarker::CommitSeq(seq)) if *seq == 5),
35432            "expected CommitSeq marker"
35433        );
35434    }
35435
35436    #[test]
35437    fn test_set_snapshot_upgrades_txn_cursor_to_time_travel() {
35438        // Verify that when a VersionStore is available, SetSnapshot replaces
35439        // the cursor backend with TimeTravelPageIo.
35440        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35441
35442        let pager = MockMvccPager;
35443        let cx = Cx::new();
35444        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35445
35446        let mut db = MemDatabase::new();
35447        let root = db.create_table(1);
35448        let table = db.get_table_mut(root).unwrap();
35449        table.insert(1, vec![SqliteValue::Integer(99)]);
35450
35451        let mut b = ProgramBuilder::new();
35452        let end = b.emit_label();
35453        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35454        b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
35455        b.emit_op(Opcode::SetSnapshot, 0, 0, 0, P4::TimeTravelCommitSeq(3), 0);
35456        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35457        b.resolve_label(end);
35458        let prog = b.finish().expect("program should build");
35459
35460        let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35461
35462        // Build a CommitLog with entries so SetSnapshot validation passes.
35463        let commit_log = {
35464            use fsqlite_types::TxnId;
35465            let mut log = CommitLog::new(CommitSeq::new(1));
35466            for seq in 1..=5 {
35467                log.append(fsqlite_mvcc::core_types::CommitRecord {
35468                    txn_id: TxnId::new(seq).unwrap(),
35469                    commit_seq: CommitSeq::new(seq),
35470                    pages: smallvec::smallvec![PageNumber::new(1).unwrap()],
35471                    timestamp_unix_ns: 1_700_000_000_000_000_000 + seq * 1_000_000_000,
35472                });
35473            }
35474            Arc::new(Mutex::new(log))
35475        };
35476
35477        let mut engine = VdbeEngine::new(prog.register_count());
35478        engine.set_database(db);
35479        engine.set_transaction(txn);
35480        engine.set_version_store(Arc::clone(&vs));
35481        engine.set_time_travel_commit_log(Arc::clone(&commit_log));
35482        engine.set_time_travel_gc_horizon(CommitSeq::new(1));
35483
35484        let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
35485        assert_eq!(outcome, ExecOutcome::Done);
35486
35487        // Verify the cursor was upgraded to a TimeTravel backend.
35488        let sc = engine
35489            .storage_cursors
35490            .get(&0)
35491            .expect("cursor 0 should exist");
35492        assert!(
35493            sc.cursor.is_time_travel(),
35494            "cursor should be upgraded to TimeTravel backend"
35495        );
35496        // The cursor should be marked read-only.
35497        assert!(!sc.writable, "time-travel cursor should be read-only");
35498    }
35499
35500    #[test]
35501    fn test_time_travel_page_io_empty_version_store_rejects_read() {
35502        // Unit test for TimeTravelPageIo: when the VersionStore is empty
35503        // (page_count == 0), read_page must return an explicit error rather
35504        // than silently falling through to current data.
35505        //
35506        // This tests the TimeTravelPageIo directly without going through
35507        // the full SetSnapshot opcode path, which has additional
35508        // requirements (CommitLog, GC horizon).
35509        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35510
35511        let pager = MockMvccPager;
35512        let cx = Cx::new();
35513        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35514        let inner_io = SharedTxnPageIo::new(txn);
35515
35516        let empty_vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35517
35518        let tt_snapshot =
35519            TimeTravelSnapshot::new_for_commit_seq(CommitSeq::new(5), SchemaEpoch::new(1));
35520
35521        let tt_page_io = TimeTravelPageIo {
35522            inner: inner_io,
35523            version_store: empty_vs,
35524            snapshot: tt_snapshot,
35525        };
35526
35527        // Attempt to read page 1. The VersionStore is empty, so this
35528        // should fail with an explicit error.
35529        let result = run_async(tt_page_io.read_page(&cx, PageNumber::new(1).unwrap()));
35530
35531        assert!(
35532            result.is_err(),
35533            "reading from TimeTravelPageIo with empty VersionStore \
35534             should return an error, not silently fall through"
35535        );
35536
35537        let err_msg = format!("{}", result.unwrap_err());
35538        assert!(
35539            err_msg.contains("historical data not available")
35540                || err_msg.contains("time-travel")
35541                || err_msg.contains("version store"),
35542            "expected error about missing historical data, got: {err_msg}"
35543        );
35544    }
35545
35546    #[test]
35547    fn test_time_travel_page_io_populated_store_falls_through_for_unchanged_page() {
35548        // When the VersionStore IS populated (has at least one page
35549        // version), but a specific page was never versioned, the
35550        // read_page should fall through to the underlying transaction
35551        // (the page hasn't changed since the target commit).
35552        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35553        use fsqlite_types::{PageVersion, TxnEpoch, TxnId, TxnToken};
35554
35555        let pager = MockMvccPager;
35556        let cx = Cx::new();
35557        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35558        let inner_io = SharedTxnPageIo::new(txn);
35559
35560        let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35561
35562        // Publish a version for page 1 at commit_seq=3 so the store is
35563        // not empty.
35564        let version = PageVersion {
35565            pgno: PageNumber::new(1).unwrap(),
35566            commit_seq: CommitSeq::new(3),
35567            created_by: TxnToken::new(TxnId::new(3).unwrap(), TxnEpoch::new(1)),
35568            data: PageData::from_vec(vec![0xAA; 4096]),
35569            prev: None,
35570        };
35571        vs.publish(version);
35572
35573        // Snapshot at commit 5 -- page 1 is versioned, page 2 is not.
35574        let tt_snapshot =
35575            TimeTravelSnapshot::new_for_commit_seq(CommitSeq::new(5), SchemaEpoch::new(1));
35576
35577        let tt_page_io = TimeTravelPageIo {
35578            inner: inner_io,
35579            version_store: vs,
35580            snapshot: tt_snapshot,
35581        };
35582
35583        // Reading page 2 (not versioned) should fall through to the
35584        // underlying transaction, not error. The MockMvccPager's
35585        // transaction will likely return an error because it has no real
35586        // pages, but the important thing is it does NOT return the
35587        // "historical data not available" error -- it falls through.
35588        let result = run_async(tt_page_io.read_page(&cx, PageNumber::new(2).unwrap()));
35589
35590        // The result may be Ok (if the mock provides data) or Err (if
35591        // the mock doesn't), but it should NOT be the "historical data
35592        // not available" error.
35593        if let Err(e) = &result {
35594            let msg = format!("{e}");
35595            assert!(
35596                !msg.contains("historical data not available"),
35597                "populated VersionStore should fall through for unknown pages, \
35598                 not return 'historical data not available'"
35599            );
35600        }
35601    }
35602
35603    #[test]
35604    fn test_shared_txn_page_io_zero_busy_timeout_preserves_losing_writer_state() {
35605        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35606        use fsqlite_types::Snapshot;
35607
35608        let pager = MockMvccPager;
35609        let cx = Cx::new();
35610        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35611
35612        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35613        let lock_table = Arc::new(InProcessPageLockTable::new());
35614        let commit_index = Arc::new(CommitIndex::new());
35615        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35616        let contested_page = PageNumber::ONE;
35617        let page_bytes = vec![0xAB; PageSize::DEFAULT.as_usize()];
35618
35619        let (holder_session, writer_session, writer_handle) = {
35620            let mut guard = registry
35621                .lock()
35622                .unwrap_or_else(std::sync::PoisonError::into_inner);
35623            let holder_session = guard
35624                .begin_concurrent(snapshot)
35625                .expect("holder session should register");
35626            let writer_session = guard
35627                .begin_concurrent(snapshot)
35628                .expect("writer session should register");
35629
35630            let mut holder = guard
35631                .get_mut(holder_session)
35632                .expect("holder session must be present");
35633            concurrent_write_page(
35634                &mut holder,
35635                &lock_table,
35636                holder_session,
35637                contested_page,
35638                PageData::from_vec(page_bytes.clone()),
35639            )
35640            .expect("holder should acquire the contested page lock");
35641            let writer_handle = guard
35642                .handle(writer_session)
35643                .expect("writer session handle must be present");
35644            (holder_session, writer_session, writer_handle)
35645        };
35646
35647        let mut page_io = SharedTxnPageIo::with_concurrent(
35648            txn,
35649            writer_session,
35650            writer_handle,
35651            Arc::clone(&lock_table),
35652            Arc::clone(&commit_index),
35653            0,
35654        );
35655
35656        let err = run_async(page_io.write_page(&cx, contested_page, &page_bytes))
35657            .expect_err("losing writer should time out with SQLITE_BUSY");
35658        assert!(
35659            matches!(err, FrankenError::Busy),
35660            "expected SQLITE_BUSY on timed-out handoff, got {err}"
35661        );
35662
35663        let guard = registry
35664            .lock()
35665            .unwrap_or_else(std::sync::PoisonError::into_inner);
35666        {
35667            let holder = guard
35668                .get(holder_session)
35669                .expect("holder session should remain registered");
35670            assert!(
35671                holder.write_set().contains_key(&contested_page),
35672                "winning writer must retain the contested page in its write set"
35673            );
35674        }
35675        {
35676            let writer = guard
35677                .get(writer_session)
35678                .expect("losing writer session should remain registered");
35679            assert!(
35680                writer.write_set().is_empty(),
35681                "timed-out writer must not leak page data into its write set"
35682            );
35683            assert!(
35684                writer.held_locks().is_empty(),
35685                "timed-out writer must not retain the contested page lock"
35686            );
35687        }
35688        drop(guard);
35689
35690        assert_eq!(
35691            lock_table.total_lock_count(),
35692            1,
35693            "contested page lock must remain owned only by the winning writer"
35694        );
35695    }
35696
35697    #[test]
35698    fn test_shared_txn_page_io_revalidates_fcw_after_page_lock_acquisition() {
35699        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35700        use fsqlite_types::Snapshot;
35701
35702        let pager = MemoryMockMvccPager;
35703        let cx = Cx::new();
35704        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35705
35706        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35707        let lock_table = Arc::new(InProcessPageLockTable::new());
35708        let commit_index = Arc::new(CommitIndex::new());
35709        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35710        let target_page = PageNumber::new(97).expect("test page must be non-zero");
35711        let page_bytes = vec![0x5A; PageSize::DEFAULT.as_usize()];
35712
35713        let (session_id, handle) = {
35714            let mut guard = registry
35715                .lock()
35716                .unwrap_or_else(std::sync::PoisonError::into_inner);
35717            let session_id = guard
35718                .begin_concurrent(snapshot)
35719                .expect("writer session should register");
35720            let handle = guard
35721                .handle(session_id)
35722                .expect("writer session handle must be present");
35723            (session_id, handle)
35724        };
35725
35726        let hook_commit_index = Arc::clone(&commit_index);
35727        install_concurrent_page_lock_window_hook(move || {
35728            hook_commit_index.update(target_page, CommitSeq::new(8));
35729        });
35730
35731        let mut page_io = SharedTxnPageIo::with_concurrent(
35732            txn,
35733            session_id,
35734            handle,
35735            Arc::clone(&lock_table),
35736            Arc::clone(&commit_index),
35737            0,
35738        );
35739        let error = run_async(page_io.write_page(&cx, target_page, &page_bytes))
35740            .expect_err("a commit published in the precheck/acquire window must reject the writer");
35741        assert!(
35742            matches!(error, FrankenError::BusySnapshot { .. }),
35743            "expected post-acquire BusySnapshot, got {error}"
35744        );
35745        assert_eq!(
35746            lock_table.total_lock_count(),
35747            0,
35748            "post-acquire FCW rejection must release the stale writer's page lock"
35749        );
35750
35751        let guard = registry
35752            .lock()
35753            .unwrap_or_else(std::sync::PoisonError::into_inner);
35754        let writer = guard
35755            .get(session_id)
35756            .expect("writer session should remain registered for rollback");
35757        assert!(
35758            writer.write_set().is_empty(),
35759            "post-acquire FCW rejection must not stage a stale page marker"
35760        );
35761        assert!(
35762            writer.held_locks().is_empty(),
35763            "post-acquire FCW rejection must restore the prior page-lock state"
35764        );
35765        drop(writer);
35766        drop(guard);
35767        assert!(
35768            !page_io.txn.borrow().has_pending_writes(),
35769            "post-acquire FCW rejection must not reach the pager write set"
35770        );
35771    }
35772
35773    #[test]
35774    fn test_shared_txn_page_io_owned_lock_skips_busy_snapshot_after_savepoint_rollback() {
35775        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35776        use fsqlite_types::Snapshot;
35777
35778        let pager = MemoryMockMvccPager;
35779        let cx = Cx::new();
35780        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35781
35782        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35783        let lock_table = Arc::new(InProcessPageLockTable::new());
35784        let commit_index = Arc::new(CommitIndex::new());
35785        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35786        let target_page = PageNumber::new(2).unwrap();
35787        let first_bytes = vec![0xAB; PageSize::DEFAULT.as_usize()];
35788        let second_bytes = vec![0xCD; PageSize::DEFAULT.as_usize()];
35789
35790        let (session_id, handle) = {
35791            let mut guard = registry
35792                .lock()
35793                .unwrap_or_else(std::sync::PoisonError::into_inner);
35794            let session_id = guard
35795                .begin_concurrent(snapshot)
35796                .expect("session should register");
35797            let handle = guard
35798                .handle(session_id)
35799                .expect("session handle must be present");
35800            (session_id, handle)
35801        };
35802
35803        let savepoint = {
35804            let guard = handle.lock();
35805            fsqlite_mvcc::concurrent_savepoint(&guard, "sp1").unwrap()
35806        };
35807
35808        let mut page_io = SharedTxnPageIo::with_concurrent(
35809            txn,
35810            session_id,
35811            Arc::clone(&handle),
35812            Arc::clone(&lock_table),
35813            Arc::clone(&commit_index),
35814            0,
35815        );
35816
35817        run_async(page_io.write_page(&cx, target_page, &first_bytes))
35818            .expect("initial concurrent write should succeed");
35819
35820        {
35821            let mut guard = handle.lock();
35822            fsqlite_mvcc::concurrent_rollback_to_savepoint(
35823                &mut guard,
35824                &lock_table,
35825                session_id,
35826                &savepoint,
35827            )
35828            .unwrap();
35829            assert!(
35830                guard.holds_page_lock(target_page),
35831                "rollback-to-savepoint must preserve page ownership"
35832            );
35833            assert!(
35834                !guard.tracks_write_conflict_page(target_page),
35835                "rollback-to-savepoint should clear staged tracking for the rolled-back page"
35836            );
35837        }
35838
35839        commit_index.update(target_page, CommitSeq::new(8));
35840
35841        run_async(page_io.write_page(&cx, target_page, &second_bytes))
35842            .expect("already-owned page should bypass stale-snapshot rejection");
35843
35844        let read_back = run_async(page_io.read_page_data(&cx, target_page))
35845            .expect("pager must keep marker-backed read-your-writes data");
35846        assert_eq!(
35847            read_back.as_bytes(),
35848            second_bytes.as_slice(),
35849            "rewrite after savepoint rollback should remain readable through the pager"
35850        );
35851
35852        let guard = registry
35853            .lock()
35854            .unwrap_or_else(std::sync::PoisonError::into_inner);
35855        let writer = guard
35856            .get(session_id)
35857            .expect("writer session should remain registered");
35858        assert_eq!(
35859            concurrent_page_read_status(&writer, target_page),
35860            (false, true),
35861            "rewrite after savepoint rollback should restage the owned page in the MVCC surface"
35862        );
35863        assert!(
35864            concurrent_read_page(&writer, target_page).is_none(),
35865            "marker-backed VDBE writes keep payload bytes in the pager, not MVCC"
35866        );
35867    }
35868
35869    #[test]
35870    fn test_shared_txn_page_io_waits_for_page_lock_release_and_succeeds() {
35871        use fsqlite_mvcc::concurrent_abort;
35872        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35873        use fsqlite_types::Snapshot;
35874
35875        let pager = MockMvccPager;
35876        let cx = Cx::new();
35877        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35878
35879        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35880        let lock_table = Arc::new(InProcessPageLockTable::new());
35881        let commit_index = Arc::new(CommitIndex::new());
35882        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35883        let contested_page = PageNumber::ONE;
35884        let page_bytes = vec![0xCD; PageSize::DEFAULT.as_usize()];
35885
35886        let (holder_session, writer_session, writer_handle) = {
35887            let mut guard = registry
35888                .lock()
35889                .unwrap_or_else(std::sync::PoisonError::into_inner);
35890            let holder_session = guard
35891                .begin_concurrent(snapshot)
35892                .expect("holder session should register");
35893            let writer_session = guard
35894                .begin_concurrent(snapshot)
35895                .expect("writer session should register");
35896
35897            let mut holder = guard
35898                .get_mut(holder_session)
35899                .expect("holder session must be present");
35900            concurrent_write_page(
35901                &mut holder,
35902                &lock_table,
35903                holder_session,
35904                contested_page,
35905                PageData::from_vec(page_bytes.clone()),
35906            )
35907            .expect("holder should acquire the contested page lock");
35908            let writer_handle = guard
35909                .handle(writer_session)
35910                .expect("writer session handle must be present");
35911            (holder_session, writer_session, writer_handle)
35912        };
35913
35914        let release_registry = Arc::clone(&registry);
35915        let release_lock_table = Arc::clone(&lock_table);
35916        let releaser = std::thread::spawn(move || {
35917            std::thread::sleep(Duration::from_millis(20));
35918            let guard = release_registry
35919                .lock()
35920                .unwrap_or_else(std::sync::PoisonError::into_inner);
35921            let mut holder = guard
35922                .get_mut(holder_session)
35923                .expect("holder session must still be present before release");
35924            concurrent_abort(&mut holder, &release_lock_table, holder_session);
35925        });
35926
35927        let mut page_io = SharedTxnPageIo::with_concurrent(
35928            txn,
35929            writer_session,
35930            writer_handle,
35931            Arc::clone(&lock_table),
35932            Arc::clone(&commit_index),
35933            250,
35934        );
35935
35936        run_async(page_io.write_page(&cx, contested_page, &page_bytes))
35937            .expect("writer should wake and acquire the page after holder release");
35938        releaser
35939            .join()
35940            .expect("holder release helper thread must complete cleanly");
35941
35942        let guard = registry
35943            .lock()
35944            .unwrap_or_else(std::sync::PoisonError::into_inner);
35945        {
35946            let writer = guard
35947                .get(writer_session)
35948                .expect("writer session should remain registered");
35949            assert!(
35950                writer.write_set().contains_key(&contested_page),
35951                "woken writer must stage the contested page in its write set"
35952            );
35953            assert!(
35954                writer.held_locks().contains(&contested_page),
35955                "woken writer must own the contested page lock after retrying"
35956            );
35957        }
35958        drop(guard);
35959
35960        assert_eq!(
35961            lock_table.total_lock_count(),
35962            1,
35963            "after wake/retry exactly one writer must hold the contested page lock"
35964        );
35965    }
35966
35967    #[test]
35968    fn test_shared_txn_page_io_short_concurrent_write_preserves_page_size_on_read() {
35969        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35970        use fsqlite_types::Snapshot;
35971
35972        let pager = MemoryMockMvccPager;
35973        let cx = Cx::new();
35974        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35975
35976        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35977        let lock_table = Arc::new(InProcessPageLockTable::new());
35978        let commit_index = Arc::new(CommitIndex::new());
35979        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35980
35981        let (session_id, handle) = {
35982            let mut guard = registry
35983                .lock()
35984                .unwrap_or_else(std::sync::PoisonError::into_inner);
35985            let session_id = guard
35986                .begin_concurrent(snapshot)
35987                .expect("session should register");
35988            let handle = guard
35989                .handle(session_id)
35990                .expect("session handle must be present");
35991            (session_id, handle)
35992        };
35993
35994        let mut page_io = SharedTxnPageIo::with_concurrent(
35995            txn,
35996            session_id,
35997            handle,
35998            Arc::clone(&lock_table),
35999            Arc::clone(&commit_index),
36000            0,
36001        );
36002        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36003        let expected = vec![0xA5; 32];
36004
36005        run_async(page_io.write_page(&cx, page_no, &expected))
36006            .expect("short concurrent write should succeed");
36007
36008        let bytes = run_async(page_io.read_page(&cx, page_no))
36009            .expect("read-your-writes should return the normalized page image");
36010        assert_eq!(
36011            bytes.len(),
36012            PageSize::DEFAULT.as_usize(),
36013            "concurrent read-your-writes must preserve the pager page-size invariant"
36014        );
36015        assert_eq!(&bytes[..expected.len()], expected.as_slice());
36016        assert!(
36017            bytes[expected.len()..].iter().all(|byte| *byte == 0),
36018            "concurrent read-your-writes should zero-fill any unwritten tail bytes"
36019        );
36020    }
36021
36022    #[test]
36023    fn test_shared_txn_page_io_short_concurrent_owned_write_preserves_page_size_on_read() {
36024        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36025        use fsqlite_types::Snapshot;
36026
36027        let pager = MemoryMockMvccPager;
36028        let cx = Cx::new();
36029        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36030
36031        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36032        let lock_table = Arc::new(InProcessPageLockTable::new());
36033        let commit_index = Arc::new(CommitIndex::new());
36034        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36035
36036        let (session_id, handle) = {
36037            let mut guard = registry
36038                .lock()
36039                .unwrap_or_else(std::sync::PoisonError::into_inner);
36040            let session_id = guard
36041                .begin_concurrent(snapshot)
36042                .expect("session should register");
36043            let handle = guard
36044                .handle(session_id)
36045                .expect("session handle must be present");
36046            (session_id, handle)
36047        };
36048
36049        let mut page_io = SharedTxnPageIo::with_concurrent(
36050            txn,
36051            session_id,
36052            handle,
36053            Arc::clone(&lock_table),
36054            Arc::clone(&commit_index),
36055            0,
36056        );
36057        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36058        let expected = vec![0x5A; 32];
36059
36060        run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(expected.clone())))
36061            .expect("short concurrent owned write should succeed");
36062
36063        let bytes = run_async(page_io.read_page(&cx, page_no))
36064            .expect("read-your-writes should return the normalized owned page image");
36065        assert_eq!(
36066            bytes.len(),
36067            PageSize::DEFAULT.as_usize(),
36068            "concurrent owned writes must preserve the pager page-size invariant"
36069        );
36070        assert_eq!(&bytes[..expected.len()], expected.as_slice());
36071        assert!(
36072            bytes[expected.len()..].iter().all(|byte| *byte == 0),
36073            "concurrent owned writes should zero-fill any unwritten tail bytes"
36074        );
36075    }
36076
36077    #[test]
36078    fn test_shared_txn_page_io_try_mutate_staged_page_data_updates_read_your_writes() {
36079        use std::path::PathBuf;
36080
36081        use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36082        use fsqlite_types::Snapshot;
36083        use fsqlite_vfs::MemoryVfs;
36084
36085        let _guard = VDBE_OBSERVABILITY_LOCK
36086            .lock()
36087            .unwrap_or_else(|e| e.into_inner());
36088        let vfs = MemoryVfs::new();
36089        let path = PathBuf::from("/shared_txn_page_io_try_mutate_staged_page_data.db");
36090        let cx = Cx::new();
36091        let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36092        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36093
36094        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36095        let lock_table = Arc::new(InProcessPageLockTable::new());
36096        let commit_index = Arc::new(CommitIndex::new());
36097        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36098
36099        let (session_id, handle) = {
36100            let mut guard = registry
36101                .lock()
36102                .unwrap_or_else(std::sync::PoisonError::into_inner);
36103            let session_id = guard
36104                .begin_concurrent(snapshot)
36105                .expect("session should register");
36106            let handle = guard
36107                .handle(session_id)
36108                .expect("session handle must be present");
36109            (session_id, handle)
36110        };
36111
36112        let mut page_io = SharedTxnPageIo::with_concurrent(
36113            txn,
36114            session_id,
36115            handle,
36116            Arc::clone(&lock_table),
36117            Arc::clone(&commit_index),
36118            0,
36119        );
36120        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36121        let mut page = vec![0x11; PageSize::MIN.as_usize()];
36122
36123        run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(page.clone())))
36124            .expect("initial concurrent owned write should succeed");
36125
36126        let mut missing_page_closure_called = false;
36127        assert!(
36128            !page_io.try_mutate_staged_page_data(
36129                PageNumber::new(3).expect("page number must be non-zero"),
36130                &mut |data| {
36131                    missing_page_closure_called = true;
36132                    data.as_bytes_mut()[0] = 0xEE;
36133                },
36134            ),
36135            "unstaged pages should not report a successful mutation"
36136        );
36137        assert!(
36138            !missing_page_closure_called,
36139            "unstaged-page mutation must not invoke the caller closure"
36140        );
36141
36142        assert!(
36143            page_io.try_mutate_staged_page_data(page_no, &mut |data| {
36144                let bytes = data.as_bytes_mut();
36145                bytes[0] = 0xA5;
36146                bytes[PageSize::MIN.as_usize() - 1] = 0x5A;
36147            }),
36148            "staged concurrent pages should support in-place pager mutation"
36149        );
36150        page[0] = 0xA5;
36151        page[PageSize::MIN.as_usize() - 1] = 0x5A;
36152
36153        let read_back = run_async(page_io.read_page_data(&cx, page_no))
36154            .expect("read-your-writes should use the mutated pager image");
36155        assert_eq!(read_back.as_bytes(), page.as_slice());
36156
36157        let guard = registry
36158            .lock()
36159            .unwrap_or_else(std::sync::PoisonError::into_inner);
36160        let writer = guard
36161            .get(session_id)
36162            .expect("writer session should remain registered");
36163        assert!(
36164            writer.write_set().contains_key(&page_no),
36165            "in-place mutation must preserve the concurrent write-set surface"
36166        );
36167        assert!(
36168            writer.held_locks().contains(&page_no),
36169            "in-place mutation must preserve the page lock"
36170        );
36171    }
36172
36173    #[test]
36174    fn test_shared_txn_page_io_short_owned_write_tracks_in_place_zero_extend_metrics() {
36175        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36176
36177        let _guard = VDBE_OBSERVABILITY_LOCK
36178            .lock()
36179            .unwrap_or_else(|e| e.into_inner());
36180        let prev_metrics_enabled = vdbe_metrics_enabled();
36181        reset_vdbe_metrics();
36182        set_vdbe_metrics_enabled(true);
36183
36184        let pager = MemoryMockMvccPager;
36185        let cx = Cx::new();
36186        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36187        let mut page_io = SharedTxnPageIo::new(txn);
36188        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36189        let expected = vec![0x6B; 32];
36190
36191        let before = vdbe_metrics_snapshot();
36192        run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(expected.clone())))
36193            .expect("short owned write should succeed");
36194        let after = vdbe_metrics_snapshot();
36195
36196        let bytes = run_async(page_io.read_page(&cx, page_no))
36197            .expect("write should remain readable through the pager");
36198        assert_eq!(bytes.len(), PageSize::DEFAULT.as_usize());
36199        assert_eq!(&bytes[..expected.len()], expected.as_slice());
36200        assert!(
36201            bytes[expected.len()..].iter().all(|byte| *byte == 0),
36202            "short owned writes should still zero-fill their unwritten tail"
36203        );
36204        assert_eq!(
36205            after.page_data_motion.owned_write_normalization_calls_total
36206                - before
36207                    .page_data_motion
36208                    .owned_write_normalization_calls_total,
36209            1
36210        );
36211        assert_eq!(
36212            after.page_data_motion.owned_in_place_zero_extends_total
36213                - before.page_data_motion.owned_in_place_zero_extends_total,
36214            1
36215        );
36216        assert_eq!(
36217            after.page_data_motion.owned_resized_copies_total
36218                - before.page_data_motion.owned_resized_copies_total,
36219            0
36220        );
36221        assert_eq!(
36222            after.page_data_motion.normalized_payload_bytes_total
36223                - before.page_data_motion.normalized_payload_bytes_total,
36224            0
36225        );
36226        assert_eq!(
36227            after.page_data_motion.normalized_zero_fill_bytes_total
36228                - before.page_data_motion.normalized_zero_fill_bytes_total,
36229            u64::try_from(PageSize::DEFAULT.as_usize() - expected.len()).unwrap()
36230        );
36231
36232        set_vdbe_metrics_enabled(prev_metrics_enabled);
36233    }
36234
36235    #[test]
36236    fn test_shared_txn_page_io_rejects_oversized_write_buffer() {
36237        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36238
36239        let pager = MemoryMockMvccPager;
36240        let cx = Cx::new();
36241        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36242        let mut page_io = SharedTxnPageIo::new(txn);
36243        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36244        let oversized = vec![0xCC; PageSize::DEFAULT.as_usize() + 1];
36245
36246        let err = run_async(page_io.write_page(&cx, page_no, &oversized))
36247            .expect_err("oversized page buffer should be rejected");
36248
36249        assert!(
36250            matches!(err, FrankenError::Internal(ref message) if message.contains("page buffer exceeds page size invariant")),
36251            "unexpected error for oversized page buffer: {err}"
36252        );
36253    }
36254
36255    #[test]
36256    fn test_shared_txn_page_io_rejects_oversized_owned_page_buffer() {
36257        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36258
36259        let pager = MemoryMockMvccPager;
36260        let cx = Cx::new();
36261        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36262        let mut page_io = SharedTxnPageIo::new(txn);
36263        let page_no = PageNumber::new(2).expect("page number must be non-zero");
36264        let oversized = PageData::from_vec(vec![0xDD; PageSize::DEFAULT.as_usize() + 1]);
36265
36266        let err = run_async(page_io.write_page_data(&cx, page_no, oversized))
36267            .expect_err("oversized owned page buffer should be rejected");
36268
36269        assert!(
36270            matches!(err, FrankenError::Internal(ref message) if message.contains("page buffer exceeds page size invariant")),
36271            "unexpected error for oversized owned page buffer: {err}"
36272        );
36273    }
36274
36275    #[test]
36276    fn test_shared_txn_page_io_busy_snapshot_restores_page_one_tracking() {
36277        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36278        use fsqlite_types::Snapshot;
36279
36280        let pager = MockMvccPager;
36281        let cx = Cx::new();
36282        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36283
36284        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36285        let lock_table = Arc::new(InProcessPageLockTable::new());
36286        let commit_index = Arc::new(CommitIndex::new());
36287        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36288        let target_page = PageNumber::new(2).unwrap();
36289        let page_bytes = vec![0xEF; PageSize::DEFAULT.as_usize()];
36290
36291        let (writer_session, writer_handle) = {
36292            let mut guard = registry
36293                .lock()
36294                .unwrap_or_else(std::sync::PoisonError::into_inner);
36295            let writer_session = guard
36296                .begin_concurrent(snapshot)
36297                .expect("writer session should register");
36298            let writer_handle = guard
36299                .handle(writer_session)
36300                .expect("writer session handle must be present");
36301            (writer_session, writer_handle)
36302        };
36303
36304        commit_index.update(target_page, CommitSeq::new(8));
36305
36306        let mut page_io = SharedTxnPageIo::with_concurrent(
36307            txn,
36308            writer_session,
36309            writer_handle,
36310            Arc::clone(&lock_table),
36311            Arc::clone(&commit_index),
36312            250,
36313        );
36314
36315        let err = run_async(page_io.write_page(&cx, target_page, &page_bytes))
36316            .expect_err("stale snapshot should reject the write");
36317        assert!(
36318            matches!(err, FrankenError::BusySnapshot { .. }),
36319            "expected SQLITE_BUSY_SNAPSHOT on stale page write, got {err}"
36320        );
36321
36322        let guard = registry
36323            .lock()
36324            .unwrap_or_else(std::sync::PoisonError::into_inner);
36325        let writer = guard
36326            .get(writer_session)
36327            .expect("writer session should remain registered");
36328        assert!(
36329            writer.write_set().is_empty(),
36330            "stale snapshot must not leak staged page bytes into the write set"
36331        );
36332        assert!(
36333            !writer.tracks_write_conflict_page(PageNumber::ONE),
36334            "failed write must restore the synthetic page-one conflict surface"
36335        );
36336        assert!(
36337            !writer.held_locks().contains(&PageNumber::ONE),
36338            "failed write must not retain the synthetic page-one lock"
36339        );
36340        assert_eq!(
36341            lock_table.total_lock_count(),
36342            0,
36343            "stale snapshot failure should leave the lock table unchanged"
36344        );
36345    }
36346
36347    #[test]
36348    fn test_shared_txn_page_io_net_zero_growth_clears_synthetic_page_one_tracking() {
36349        use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36350        use fsqlite_types::Snapshot;
36351
36352        let pager = MemoryMockMvccPager;
36353        let cx = Cx::new();
36354        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36355
36356        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36357        let lock_table = Arc::new(InProcessPageLockTable::new());
36358        let commit_index = Arc::new(CommitIndex::new());
36359        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36360
36361        let (session_id, handle) = {
36362            let mut guard = registry
36363                .lock()
36364                .unwrap_or_else(std::sync::PoisonError::into_inner);
36365            let session_id = guard
36366                .begin_concurrent(snapshot)
36367                .expect("session should register");
36368            let handle = guard
36369                .handle(session_id)
36370                .expect("session handle must be present");
36371            (session_id, handle)
36372        };
36373
36374        let mut page_io = SharedTxnPageIo::with_concurrent(
36375            txn,
36376            session_id,
36377            Arc::clone(&handle),
36378            Arc::clone(&lock_table),
36379            Arc::clone(&commit_index),
36380            0,
36381        );
36382
36383        let page_no = run_async(page_io.allocate_page(&cx))
36384            .expect("allocate_page should succeed for concurrent txn");
36385        let page_bytes = vec![0x5A; PageSize::DEFAULT.as_usize()];
36386        run_async(page_io.write_page(&cx, page_no, &page_bytes))
36387            .expect("write_page should succeed for allocated page");
36388
36389        {
36390            let guard = handle.lock();
36391            assert!(
36392                !guard.tracks_write_conflict_page(PageNumber::ONE),
36393                "successful writes must reconcile synthetic page-one tracking when the pager does not expose page one in pending_commit_pages"
36394            );
36395            assert!(
36396                !guard.held_locks().contains(&PageNumber::ONE),
36397                "successful writes must release any synthetic page-one lock when reconciliation drops page one from the conflict surface"
36398            );
36399        }
36400
36401        run_async(page_io.free_page(&cx, page_no))
36402            .expect("free_page should succeed for net-zero growth");
36403
36404        let guard = handle.lock();
36405        assert!(
36406            !guard.tracks_write_conflict_page(PageNumber::ONE),
36407            "net-zero growth should drop the synthetic page-one conflict surface"
36408        );
36409        assert!(
36410            !guard.held_locks().contains(&PageNumber::ONE),
36411            "net-zero growth should release the synthetic page-one lock"
36412        );
36413    }
36414
36415    #[test]
36416    fn test_shared_txn_page_io_concurrent_growth_write_does_not_block_on_page_one_pretracking() {
36417        use std::path::PathBuf;
36418
36419        use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36420        use fsqlite_types::Snapshot;
36421        use fsqlite_vfs::MemoryVfs;
36422
36423        let vfs = MemoryVfs::new();
36424        let path = PathBuf::from("/leased_growth_write_skips_page_one_pretracking.db");
36425        let cx = Cx::new();
36426        let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36427
36428        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36429        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36430        let lock_table = Arc::new(InProcessPageLockTable::new());
36431        let commit_index = Arc::new(CommitIndex::new());
36432        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36433
36434        let (session_id, handle, blocker_session) = {
36435            let mut guard = registry
36436                .lock()
36437                .unwrap_or_else(std::sync::PoisonError::into_inner);
36438            let session_id = guard
36439                .begin_concurrent(snapshot)
36440                .expect("session should register");
36441            let handle = guard
36442                .handle(session_id)
36443                .expect("session handle must be present");
36444            let blocker_session = guard
36445                .begin_concurrent(snapshot)
36446                .expect("blocker session should register");
36447            (session_id, handle, blocker_session)
36448        };
36449
36450        {
36451            let guard = registry
36452                .lock()
36453                .unwrap_or_else(std::sync::PoisonError::into_inner);
36454            let mut blocker = guard
36455                .get_mut(blocker_session)
36456                .expect("blocker handle should be present");
36457            concurrent_track_write_conflict_page(
36458                &mut blocker,
36459                &lock_table,
36460                blocker_session,
36461                PageNumber::ONE,
36462            )
36463            .expect("blocker must hold synthetic page-one tracking");
36464        }
36465
36466        let mut page_io = SharedTxnPageIo::with_concurrent(
36467            txn,
36468            session_id,
36469            Arc::clone(&handle),
36470            Arc::clone(&lock_table),
36471            Arc::clone(&commit_index),
36472            0,
36473        );
36474
36475        let page_no =
36476            run_async(page_io.allocate_page(&cx)).expect("allocate_page should not need page one");
36477        run_async(page_io.write_page(&cx, page_no, &vec![0x5A; PageSize::MIN.as_usize()]))
36478            .expect("leased growth write should not block on unrelated page-one tracking");
36479
36480        let guard = handle.lock();
36481        assert!(
36482            guard.tracks_write_conflict_page(page_no),
36483            "the actual high page must still enter the write-conflict surface"
36484        );
36485        assert!(
36486            !guard.tracks_write_conflict_page(PageNumber::ONE),
36487            "tier-1 leased growth should not synthesize page-one conflict tracking before commit planning"
36488        );
36489    }
36490
36491    #[test]
36492    fn test_shared_txn_page_io_high_page_write_skips_conservative_page_one_probe() {
36493        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36494        use fsqlite_types::Snapshot;
36495
36496        let pager = MockMvccPager;
36497        let cx = Cx::new();
36498        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36499
36500        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36501        let lock_table = Arc::new(InProcessPageLockTable::new());
36502        let commit_index = Arc::new(CommitIndex::new());
36503        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36504        let target_page = PageNumber::new(2).unwrap();
36505
36506        let (session_id, handle, blocker_session) = {
36507            let mut guard = registry
36508                .lock()
36509                .unwrap_or_else(std::sync::PoisonError::into_inner);
36510            let session_id = guard
36511                .begin_concurrent(snapshot)
36512                .expect("session should register");
36513            let handle = guard
36514                .handle(session_id)
36515                .expect("session handle must be present");
36516            let blocker_session = guard
36517                .begin_concurrent(snapshot)
36518                .expect("blocker session should register");
36519            (session_id, handle, blocker_session)
36520        };
36521
36522        {
36523            let guard = registry
36524                .lock()
36525                .unwrap_or_else(std::sync::PoisonError::into_inner);
36526            let mut blocker = guard
36527                .get_mut(blocker_session)
36528                .expect("blocker handle should be present");
36529            concurrent_track_write_conflict_page(
36530                &mut blocker,
36531                &lock_table,
36532                blocker_session,
36533                PageNumber::ONE,
36534            )
36535            .expect("blocker must hold page-one tracking");
36536        }
36537
36538        let mut page_io = SharedTxnPageIo::with_concurrent(
36539            txn,
36540            session_id,
36541            Arc::clone(&handle),
36542            Arc::clone(&lock_table),
36543            Arc::clone(&commit_index),
36544            0,
36545        );
36546
36547        run_async(page_io.write_page(&cx, target_page, &vec![0x7B; PageSize::DEFAULT.as_usize()]))
36548            .expect("ordinary high-page concurrent writes must not probe page-one tracking");
36549
36550        let guard = handle.lock();
36551        assert!(
36552            guard.tracks_write_conflict_page(target_page),
36553            "the high page must still enter the write-conflict surface"
36554        );
36555        assert!(
36556            !guard.tracks_write_conflict_page(PageNumber::ONE),
36557            "high-page writes must not synthesize page-one conflict tracking"
36558        );
36559    }
36560
36561    #[test]
36562    fn test_shared_txn_page_io_free_does_not_late_acquire_existing_freelist_trunk_pages() {
36563        use std::path::PathBuf;
36564
36565        use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36566        use fsqlite_types::Snapshot;
36567        use fsqlite_vfs::MemoryVfs;
36568
36569        let vfs = MemoryVfs::new();
36570        let path = PathBuf::from("/late_pending_commit_freelist_trunk.db");
36571        let cx = Cx::new();
36572        let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36573        let ps = PageSize::MIN.as_usize();
36574
36575        let (p2, p3) = {
36576            let mut seed = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36577            let p2 = run_async(seed.allocate_page(&cx)).unwrap();
36578            let p3 = run_async(seed.allocate_page(&cx)).unwrap();
36579            run_async(seed.write_page(&cx, p2, &vec![0x11; ps])).unwrap();
36580            run_async(seed.write_page(&cx, p3, &vec![0x22; ps])).unwrap();
36581            run_async(seed.commit(&cx)).unwrap();
36582            (p2, p3)
36583        };
36584
36585        {
36586            let mut establish_committed_freelist =
36587                run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36588            run_async(establish_committed_freelist.free_page(&cx, p2)).unwrap();
36589            run_async(establish_committed_freelist.commit(&cx)).unwrap();
36590        }
36591
36592        {
36593            let mut preview = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36594            run_async(preview.free_page(&cx, p3)).unwrap();
36595            let predicted = preview.pending_commit_pages().unwrap();
36596            assert!(
36597                predicted
36598                    .iter()
36599                    .any(|page| *page != PageNumber::ONE && page.get() <= p3.get()),
36600                "freeing a durable page should expose a real freelist page in the commit surface"
36601            );
36602            run_async(preview.rollback(&cx)).unwrap();
36603        }
36604
36605        let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36606        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36607        let lock_table = Arc::new(InProcessPageLockTable::new());
36608        let commit_index = Arc::new(CommitIndex::new());
36609        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36610
36611        let (session_id, handle, blocker_session) = {
36612            let mut guard = registry
36613                .lock()
36614                .unwrap_or_else(std::sync::PoisonError::into_inner);
36615            let session_id = guard
36616                .begin_concurrent(snapshot)
36617                .expect("session should register");
36618            let handle = guard
36619                .handle(session_id)
36620                .expect("session handle must be present");
36621            let blocker_session = guard
36622                .begin_concurrent(snapshot)
36623                .expect("blocker session should register");
36624            (session_id, handle, blocker_session)
36625        };
36626
36627        {
36628            let guard = registry
36629                .lock()
36630                .unwrap_or_else(std::sync::PoisonError::into_inner);
36631            let mut blocker = guard
36632                .get_mut(blocker_session)
36633                .expect("blocker handle should be present");
36634            for page in txn.pending_commit_pages().unwrap() {
36635                if page == PageNumber::ONE {
36636                    continue;
36637                }
36638                concurrent_track_write_conflict_page(
36639                    &mut blocker,
36640                    &lock_table,
36641                    blocker_session,
36642                    page,
36643                )
36644                .expect("blocker must hold predicted freelist commit page locks");
36645            }
36646        }
36647
36648        let predicted_commit_pages = txn.pending_commit_pages().unwrap();
36649
36650        let mut page_io = SharedTxnPageIo::with_concurrent(
36651            txn,
36652            session_id,
36653            Arc::clone(&handle),
36654            Arc::clone(&lock_table),
36655            Arc::clone(&commit_index),
36656            0,
36657        );
36658
36659        run_async(page_io.free_page(&cx, p3))
36660            .expect("free_page must not fail just because commit-time trunk rewrites will later need an existing freelist page");
36661
36662        let guard = handle.lock();
36663        for page in predicted_commit_pages {
36664            assert!(
36665                !guard.tracks_write_conflict_page(page),
36666                "the per-op path must not late-acquire freelist commit-surface pages after mutating the pager state"
36667            );
36668            assert!(
36669                !guard.held_locks().contains(&page),
36670                "the per-op path must not steal freelist commit-surface page locks before commit planning"
36671            );
36672        }
36673    }
36674
36675    #[test]
36676    fn test_shared_txn_page_io_clears_preexisting_synthetic_page_one_tracking_when_unneeded() {
36677        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36678
36679        // Simulate the state after an earlier allocator operation has already
36680        // installed synthetic page-one tracking, but the current pager surface
36681        // no longer needs page one in `pending_commit_pages()`.
36682        let cx = Cx::new();
36683        let pager = MockMvccPager;
36684        let txn = run_async(pager.begin(&cx, TransactionMode::Immediate))
36685            .expect("transaction should start");
36686
36687        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36688        let lock_table = Arc::new(InProcessPageLockTable::new());
36689        let commit_index = Arc::new(CommitIndex::new());
36690        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36691
36692        let (session_id, handle) = {
36693            let mut guard = registry
36694                .lock()
36695                .unwrap_or_else(std::sync::PoisonError::into_inner);
36696            let session_id = guard
36697                .begin_concurrent(snapshot)
36698                .expect("session should register");
36699            let handle = guard
36700                .handle(session_id)
36701                .expect("session handle must be present");
36702            (session_id, handle)
36703        };
36704
36705        {
36706            let mut guard = handle.lock();
36707            concurrent_track_write_conflict_page(
36708                &mut guard,
36709                &lock_table,
36710                session_id,
36711                PageNumber::ONE,
36712            )
36713            .expect("synthetic page-one tracking should be installed");
36714        }
36715
36716        let mut page_io = SharedTxnPageIo::with_concurrent(
36717            txn,
36718            session_id,
36719            Arc::clone(&handle),
36720            Arc::clone(&lock_table),
36721            Arc::clone(&commit_index),
36722            0,
36723        );
36724
36725        {
36726            let guard = handle.lock();
36727            assert!(
36728                guard.tracks_write_conflict_page(PageNumber::ONE),
36729                "test precondition should start with synthetic page-one tracking already present"
36730            );
36731        }
36732
36733        run_async(page_io.free_page(
36734            &cx,
36735            PageNumber::new(2).expect("page number must be non-zero"),
36736        ))
36737        .expect("free_page should succeed while pending commit pages stay empty");
36738
36739        let guard = handle.lock();
36740        assert!(
36741            !guard.tracks_write_conflict_page(PageNumber::ONE),
36742            "reconciliation must clear stale synthetic page-one tracking even when the current op did not introduce it"
36743        );
36744        assert!(
36745            !guard.held_locks().contains(&PageNumber::ONE),
36746            "reconciliation must release the stale synthetic page-one lock"
36747        );
36748    }
36749
36750    #[test]
36751    fn test_shared_txn_page_io_wait_is_cancellation_responsive() {
36752        use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36753        use fsqlite_types::Snapshot;
36754
36755        let pager = MockMvccPager;
36756        let root_cx = Cx::new();
36757        let write_cx = root_cx.create_child();
36758        let txn = run_async(pager.begin(&root_cx, TransactionMode::Immediate)).unwrap();
36759
36760        let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36761        let lock_table = Arc::new(InProcessPageLockTable::new());
36762        let commit_index = Arc::new(CommitIndex::new());
36763        let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36764        let contested_page = PageNumber::ONE;
36765        let page_bytes = vec![0xAA; PageSize::DEFAULT.as_usize()];
36766
36767        let (holder_session, writer_session, writer_handle) = {
36768            let mut guard = registry
36769                .lock()
36770                .unwrap_or_else(std::sync::PoisonError::into_inner);
36771            let holder_session = guard
36772                .begin_concurrent(snapshot)
36773                .expect("holder session should register");
36774            let writer_session = guard
36775                .begin_concurrent(snapshot)
36776                .expect("writer session should register");
36777
36778            let mut holder = guard
36779                .get_mut(holder_session)
36780                .expect("holder session must be present");
36781            concurrent_write_page(
36782                &mut holder,
36783                &lock_table,
36784                holder_session,
36785                contested_page,
36786                PageData::from_vec(page_bytes.clone()),
36787            )
36788            .expect("holder should acquire the contested page lock");
36789            let writer_handle = guard
36790                .handle(writer_session)
36791                .expect("writer session handle must be present");
36792            (holder_session, writer_session, writer_handle)
36793        };
36794
36795        let cancel_cx = write_cx.clone();
36796        let cancel_helper = std::thread::spawn(move || {
36797            std::thread::sleep(Duration::from_millis(20));
36798            cancel_cx.cancel();
36799        });
36800
36801        let mut page_io = SharedTxnPageIo::with_concurrent(
36802            txn,
36803            writer_session,
36804            writer_handle,
36805            Arc::clone(&lock_table),
36806            Arc::clone(&commit_index),
36807            500,
36808        );
36809
36810        let started = std::time::Instant::now();
36811        let err = run_async(page_io.write_page(&write_cx, contested_page, &page_bytes))
36812            .expect_err("cancelled waiter should abort before busy timeout");
36813        cancel_helper
36814            .join()
36815            .expect("cancel helper thread must complete cleanly");
36816
36817        assert!(matches!(err, FrankenError::Abort));
36818        assert!(
36819            started.elapsed() < Duration::from_millis(250),
36820            "cancelled wait should return promptly instead of sleeping until busy_timeout"
36821        );
36822
36823        let guard = registry
36824            .lock()
36825            .unwrap_or_else(std::sync::PoisonError::into_inner);
36826        let holder = guard
36827            .get(holder_session)
36828            .expect("holder session should remain registered");
36829        assert!(
36830            holder.held_locks().contains(&contested_page),
36831            "cancellation must not disturb the winning writer's held lock"
36832        );
36833        let writer = guard
36834            .get(writer_session)
36835            .expect("writer session should remain registered");
36836        assert!(
36837            writer.write_set().is_empty(),
36838            "cancelled write must not stage page bytes"
36839        );
36840        assert!(
36841            writer.held_locks().is_empty(),
36842            "cancelled write must not retain the contested page lock"
36843        );
36844    }
36845
36846    // ── Subtype opcode tests ─────────────────────────────────────────
36847
36848    #[test]
36849    fn test_subtype_set_get_clr_roundtrip() {
36850        // SetSubtype P1=subtype_reg P2=target_reg,
36851        // GetSubtype P1=target_reg P2=result_reg,
36852        // ClrSubtype P1=target_reg.
36853        let rows = run_program(|b| {
36854            let end = b.emit_label();
36855            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36856
36857            let r_subtype = b.alloc_reg(); // holds subtype value (74 = 'J')
36858            let r_target = b.alloc_reg(); // register to tag
36859            let r_out1 = b.alloc_reg(); // result before SetSubtype
36860            let r_out2 = b.alloc_reg(); // result after SetSubtype
36861            let r_out3 = b.alloc_reg(); // result after ClrSubtype
36862
36863            // Put a value into r_target.
36864            b.emit_op(
36865                Opcode::String8,
36866                0,
36867                r_target,
36868                0,
36869                P4::Str(r#"{"a":1}"#.to_owned()),
36870                0,
36871            );
36872
36873            // GetSubtype before any SetSubtype → 0.
36874            b.emit_op(Opcode::GetSubtype, r_target, r_out1, 0, P4::None, 0);
36875
36876            // SetSubtype: tag r_target with subtype 74 ('J' for JSON).
36877            b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
36878            b.emit_op(Opcode::SetSubtype, r_subtype, r_target, 0, P4::None, 0);
36879
36880            // GetSubtype → should be 74.
36881            b.emit_op(Opcode::GetSubtype, r_target, r_out2, 0, P4::None, 0);
36882
36883            // ClrSubtype → clear the tag.
36884            b.emit_op(Opcode::ClrSubtype, r_target, 0, 0, P4::None, 0);
36885            b.emit_op(Opcode::GetSubtype, r_target, r_out3, 0, P4::None, 0);
36886
36887            b.emit_op(Opcode::ResultRow, r_out1, 3, 0, P4::None, 0);
36888            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36889            b.resolve_label(end);
36890        });
36891        assert_eq!(rows.len(), 1);
36892        assert_eq!(rows[0][0], SqliteValue::Integer(0)); // before set
36893        assert_eq!(rows[0][1], SqliteValue::Integer(74)); // after set
36894        assert_eq!(rows[0][2], SqliteValue::Integer(0)); // after clear
36895    }
36896
36897    #[test]
36898    fn test_subtype_set_zero_clears() {
36899        // Setting subtype to 0 should effectively clear it.
36900        let rows = run_program(|b| {
36901            let end = b.emit_label();
36902            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36903
36904            let r_st = b.alloc_reg();
36905            let r_target = b.alloc_reg();
36906            let r_out = b.alloc_reg();
36907
36908            b.emit_op(Opcode::Integer, 42, r_target, 0, P4::None, 0);
36909            // Set subtype to 74.
36910            b.emit_op(Opcode::Integer, 74, r_st, 0, P4::None, 0);
36911            b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36912            // Set subtype to 0 → should clear.
36913            b.emit_op(Opcode::Integer, 0, r_st, 0, P4::None, 0);
36914            b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36915            b.emit_op(Opcode::GetSubtype, r_target, r_out, 0, P4::None, 0);
36916
36917            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
36918            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36919            b.resolve_label(end);
36920        });
36921        assert_eq!(rows[0][0], SqliteValue::Integer(0));
36922    }
36923
36924    #[test]
36925    fn test_subtype_is_cleared_when_register_value_is_overwritten() {
36926        let rows = run_program(|b| {
36927            let end = b.emit_label();
36928            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36929
36930            let r_st = b.alloc_reg();
36931            let r_target = b.alloc_reg();
36932            let r_out = b.alloc_reg();
36933
36934            b.emit_op(
36935                Opcode::String8,
36936                0,
36937                r_target,
36938                0,
36939                P4::Str(r#"{"a":1}"#.to_owned()),
36940                0,
36941            );
36942            b.emit_op(Opcode::Integer, 74, r_st, 0, P4::None, 0);
36943            b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36944
36945            // Any subsequent register write replaces the logical value, so the
36946            // prior JSON subtype must not survive.
36947            b.emit_op(Opcode::Integer, 42, r_target, 0, P4::None, 0);
36948            b.emit_op(Opcode::GetSubtype, r_target, r_out, 0, P4::None, 0);
36949
36950            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
36951            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36952            b.resolve_label(end);
36953        });
36954
36955        assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
36956    }
36957
36958    #[test]
36959    fn test_take_reg_clears_subtype_metadata() {
36960        let mut engine = VdbeEngine::new(4);
36961        engine.set_reg(1, SqliteValue::Text("payload".into()));
36962        engine.set_register_subtype(1, 74);
36963
36964        assert_eq!(engine.take_reg(1), SqliteValue::Text("payload".into()));
36965        assert_eq!(engine.get_reg(1), &SqliteValue::Null);
36966        assert!(
36967            engine.register_subtype(1).is_none(),
36968            "moving a register value out must clear any stale subtype metadata"
36969        );
36970    }
36971
36972    #[test]
36973    fn test_execute_reuse_clears_subtype_metadata() {
36974        let mut first_builder = ProgramBuilder::new();
36975        first_builder.emit_op(
36976            Opcode::String8,
36977            0,
36978            1,
36979            0,
36980            P4::Str(r#"{"a":1}"#.to_owned()),
36981            0,
36982        );
36983        first_builder.emit_op(Opcode::Integer, 74, 2, 0, P4::None, 0);
36984        first_builder.emit_op(Opcode::SetSubtype, 2, 1, 0, P4::None, 0);
36985        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36986        let first_program = first_builder.finish().expect("first program should build");
36987
36988        let mut second_builder = ProgramBuilder::new();
36989        second_builder.emit_op(Opcode::GetSubtype, 1, 3, 0, P4::None, 0);
36990        second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
36991        second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36992        let second_program = second_builder
36993            .finish()
36994            .expect("second program should build");
36995
36996        let mut engine = VdbeEngine::new(
36997            first_program
36998                .register_count()
36999                .max(second_program.register_count()),
37000        );
37001        assert_eq!(
37002            run_async(engine.execute(&first_program)).expect("first execution"),
37003            ExecOutcome::Done
37004        );
37005        assert_eq!(
37006            run_async(engine.execute(&second_program)).expect("second execution"),
37007            ExecOutcome::Done
37008        );
37009
37010        assert_eq!(
37011            engine
37012                .results()
37013                .iter()
37014                .map(|row| row.clone().into_vec())
37015                .collect::<Vec<_>>(),
37016            vec![vec![SqliteValue::Integer(0)]]
37017        );
37018    }
37019
37020    #[test]
37021    fn test_simple_statement_does_not_allocate_cold_state_sidecar() {
37022        let mut builder = ProgramBuilder::new();
37023        builder.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
37024        builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
37025        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37026        let program = builder.finish().expect("program should build");
37027
37028        let mut engine = VdbeEngine::new(program.register_count());
37029        assert_eq!(
37030            run_async(engine.execute(&program)).expect("execution should succeed"),
37031            ExecOutcome::Done
37032        );
37033
37034        assert!(
37035            !engine.has_cold_state_allocated(),
37036            "simple non-feature statements should not allocate the cold sidecar"
37037        );
37038        assert!(engine.statement_cold_state.is_empty());
37039    }
37040
37041    #[test]
37042    fn test_aggregate_statement_allocates_cold_state_sidecar() {
37043        let mut builder = ProgramBuilder::new();
37044        builder.emit_op(Opcode::Integer, 7, 1, 0, P4::None, 0);
37045        builder.emit_op(Opcode::AggStep, 0, 1, 2, P4::FuncName("sum".to_owned()), 1);
37046        builder.emit_op(Opcode::AggFinal, 2, 1, 0, P4::FuncName("sum".to_owned()), 0);
37047        builder.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
37048        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37049        let program = builder.finish().expect("program should build");
37050
37051        let mut engine = VdbeEngine::new(program.register_count());
37052        let mut registry = FunctionRegistry::new();
37053        register_builtins(&mut registry);
37054        engine.set_function_registry(Arc::new(registry));
37055
37056        assert_eq!(
37057            run_async(engine.execute(&program)).expect("aggregate execution should succeed"),
37058            ExecOutcome::Done
37059        );
37060        assert!(engine.has_cold_state_allocated());
37061        assert!(
37062            engine
37063                .statement_cold_state
37064                .contains(StatementColdState::AGGREGATES)
37065        );
37066        assert_eq!(
37067            engine
37068                .results()
37069                .iter()
37070                .map(|row| row.clone().into_vec())
37071                .collect::<Vec<_>>(),
37072            vec![vec![SqliteValue::Integer(7)]]
37073        );
37074    }
37075
37076    #[test]
37077    fn test_execute_reuse_drops_cold_state_sidecar() {
37078        let mut aggregate_builder = ProgramBuilder::new();
37079        aggregate_builder.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
37080        aggregate_builder.emit_op(Opcode::AggStep, 0, 1, 2, P4::FuncName("sum".to_owned()), 1);
37081        aggregate_builder.emit_op(Opcode::AggFinal, 2, 1, 0, P4::FuncName("sum".to_owned()), 0);
37082        aggregate_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37083        let aggregate_program = aggregate_builder
37084            .finish()
37085            .expect("aggregate program should build");
37086
37087        let mut simple_builder = ProgramBuilder::new();
37088        simple_builder.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
37089        simple_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37090        let simple_program = simple_builder
37091            .finish()
37092            .expect("simple program should build");
37093
37094        let mut engine = VdbeEngine::new(
37095            aggregate_program
37096                .register_count()
37097                .max(simple_program.register_count()),
37098        );
37099        let mut registry = FunctionRegistry::new();
37100        register_builtins(&mut registry);
37101        engine.set_function_registry(Arc::new(registry));
37102
37103        assert_eq!(
37104            run_async(engine.execute(&aggregate_program))
37105                .expect("aggregate execution should succeed"),
37106            ExecOutcome::Done
37107        );
37108        assert!(engine.has_cold_state_allocated());
37109
37110        assert_eq!(
37111            run_async(engine.execute(&simple_program)).expect("simple execution should succeed"),
37112            ExecOutcome::Done
37113        );
37114        assert!(
37115            !engine.has_cold_state_allocated(),
37116            "reuse should drop the cold sidecar before the next simple statement"
37117        );
37118        assert!(engine.statement_cold_state.is_empty());
37119    }
37120
37121    #[test]
37122    fn test_vdbe_engine_inline_size_stays_bounded() {
37123        assert!(
37124            std::mem::size_of::<VdbeEngine>() <= VDBE_ENGINE_INLINE_SIZE_BUDGET_BYTES,
37125            "VdbeEngine inline size regressed to {} bytes",
37126            std::mem::size_of::<VdbeEngine>()
37127        );
37128    }
37129
37130    // ── Bloom filter opcode tests ────────────────────────────────────
37131
37132    #[test]
37133    fn test_bloom_filter_add_and_test() {
37134        // FilterAdd adds a hash; Filter should NOT jump (entry present).
37135        // Filter: jump to P2 if NOT found, fall through if possibly found.
37136        let rows = run_program(|b| {
37137            let end = b.emit_label();
37138            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37139
37140            let r_filter = b.alloc_reg();
37141            let r_key = b.alloc_reg();
37142            let r_out = b.alloc_reg();
37143
37144            // Add key "hello" to filter.
37145            b.emit_op(Opcode::String8, 0, r_key, 0, P4::Str("hello".to_owned()), 0);
37146            b.emit_op(Opcode::FilterAdd, r_filter, 0, r_key, P4::None, 0);
37147
37148            // Test "hello" — should be found (falls through past Filter).
37149            let not_found = b.emit_label();
37150            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); // default: not found
37151            b.emit_jump_to_label(Opcode::Filter, r_filter, r_key, not_found, P4::None, 0);
37152            // Fell through → found. Overwrite with 1.
37153            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37154            b.resolve_label(not_found);
37155
37156            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37157            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37158            b.resolve_label(end);
37159        });
37160        assert_eq!(rows[0][0], SqliteValue::Integer(1)); // found
37161    }
37162
37163    #[test]
37164    fn test_bloom_filter_miss_jumps() {
37165        // Test a key NOT in the filter → Filter should jump to P2.
37166        // Filter: jump to P2 if NOT found, fall through if found.
37167        let rows = run_program(|b| {
37168            let end = b.emit_label();
37169            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37170
37171            let r_filter = b.alloc_reg();
37172            let r_key1 = b.alloc_reg();
37173            let r_key2 = b.alloc_reg();
37174            let r_out = b.alloc_reg();
37175
37176            // Add "hello" to filter.
37177            b.emit_op(
37178                Opcode::String8,
37179                0,
37180                r_key1,
37181                0,
37182                P4::Str("hello".to_owned()),
37183                0,
37184            );
37185            b.emit_op(Opcode::FilterAdd, r_filter, 0, r_key1, P4::None, 0);
37186
37187            // Test "world" — likely NOT found → jumps to not_found.
37188            b.emit_op(
37189                Opcode::String8,
37190                0,
37191                r_key2,
37192                0,
37193                P4::Str("world".to_owned()),
37194                0,
37195            );
37196            let not_found = b.emit_label();
37197            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); // default: not found
37198            b.emit_jump_to_label(Opcode::Filter, r_filter, r_key2, not_found, P4::None, 0);
37199            // Fell through → found (false positive).
37200            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37201            b.resolve_label(not_found);
37202
37203            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37204            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37205            b.resolve_label(end);
37206        });
37207        // Due to Bloom filter false positives, we can't assert the exact value.
37208        assert!(matches!(rows[0][0], SqliteValue::Integer(0 | 1)));
37209    }
37210
37211    #[test]
37212    fn test_bloom_filter_no_filter_falls_through() {
37213        // Filter with no FilterAdd → no filter exists → conservatively
37214        // falls through (never skips).
37215        let rows = run_program(|b| {
37216            let end = b.emit_label();
37217            b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37218
37219            let r_filter = b.alloc_reg();
37220            let r_key = b.alloc_reg();
37221            let r_out = b.alloc_reg();
37222
37223            b.emit_op(Opcode::Integer, 42, r_key, 0, P4::None, 0);
37224            let not_found = b.emit_label();
37225            b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); // default: not found
37226            b.emit_jump_to_label(Opcode::Filter, r_filter, r_key, not_found, P4::None, 0);
37227            // Fell through → found (or no filter). Set to 1.
37228            b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37229            b.resolve_label(not_found);
37230
37231            b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37232            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37233            b.resolve_label(end);
37234        });
37235        assert_eq!(rows[0][0], SqliteValue::Integer(1)); // fell through
37236    }
37237
37238    #[test]
37239    fn test_execute_reuse_clears_bloom_filters() {
37240        let added_value = SqliteValue::Text("hello".into());
37241        let missing_value = SqliteValue::Text("world".into());
37242        let bloom_bits = (BLOOM_FILTER_WORDS * 64) as u64;
37243        assert_ne!(
37244            bloom_hash(&added_value) % bloom_bits,
37245            bloom_hash(&missing_value) % bloom_bits,
37246            "test fixture must exercise a genuinely missing bloom-filter bit"
37247        );
37248
37249        let mut first_builder = ProgramBuilder::new();
37250        first_builder.emit_op(Opcode::String8, 0, 2, 0, P4::Str("hello".to_owned()), 0);
37251        first_builder.emit_op(Opcode::FilterAdd, 1, 0, 2, P4::None, 0);
37252        first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37253        let first_program = first_builder.finish().expect("first program should build");
37254
37255        let mut second_builder = ProgramBuilder::new();
37256        let not_present = second_builder.emit_label();
37257        let end = second_builder.emit_label();
37258        second_builder.emit_op(Opcode::String8, 0, 2, 0, P4::Str("world".to_owned()), 0);
37259        second_builder.emit_jump_to_label(Opcode::Filter, 1, 0, not_present, P4::None, 2);
37260        second_builder.emit_op(Opcode::Integer, 1, 3, 0, P4::None, 0);
37261        second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
37262        second_builder.emit_jump_to_label(Opcode::Goto, 0, 0, end, P4::None, 0);
37263        second_builder.resolve_label(not_present);
37264        second_builder.emit_op(Opcode::Integer, 0, 3, 0, P4::None, 0);
37265        second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
37266        second_builder.resolve_label(end);
37267        second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37268        let second_program = second_builder
37269            .finish()
37270            .expect("second program should build");
37271
37272        let mut engine = VdbeEngine::new(
37273            first_program
37274                .register_count()
37275                .max(second_program.register_count()),
37276        );
37277        assert_eq!(
37278            run_async(engine.execute(&first_program)).expect("first execution"),
37279            ExecOutcome::Done
37280        );
37281        assert_eq!(
37282            run_async(engine.execute(&second_program)).expect("second execution"),
37283            ExecOutcome::Done
37284        );
37285
37286        assert_eq!(
37287            engine
37288                .results()
37289                .iter()
37290                .map(|row| row.clone().into_vec())
37291                .collect::<Vec<_>>(),
37292            vec![vec![SqliteValue::Integer(1)]],
37293            "second execution should observe no inherited bloom filter"
37294        );
37295    }
37296
37297    // ── IMPL-13: Fused Integer + ResultRow ──────────────────────────────
37298    //
37299    // Verifies the `FusedLiteralResultRow` opcode and the peephole pass
37300    // that rewrites `Integer(lit, reg) + ResultRow(reg, 1)` pairs into the
37301    // fused form. The test compiles the unfused program, executes it, then
37302    // compiles+peephole-fuses the same program and byte-compares the
37303    // emitted result rows.
37304
37305    fn opcode_fusion_build_unfused_program(literal: i32) -> crate::VdbeProgram {
37306        let mut b = ProgramBuilder::new();
37307        let r = b.alloc_reg();
37308        b.emit_op(Opcode::Integer, literal, r, 0, P4::None, 0);
37309        b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37310        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37311        b.finish().expect("unfused program should build")
37312    }
37313
37314    fn opcode_fusion_build_fused_program(literal: i32) -> (crate::VdbeProgram, usize) {
37315        let mut b = ProgramBuilder::new();
37316        let r = b.alloc_reg();
37317        b.emit_op(Opcode::Integer, literal, r, 0, P4::None, 0);
37318        b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37319        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37320        let fused_count = b.apply_fuse_literal_result_row();
37321        (b.finish().expect("fused program should build"), fused_count)
37322    }
37323
37324    #[test]
37325    fn opcode_fusion_literal_result_row_peephole_rewrites_pair() {
37326        let (program, fused_count) = opcode_fusion_build_fused_program(42);
37327        assert_eq!(fused_count, 1, "exactly one fusion site should match");
37328        let ops = program.ops();
37329        assert_eq!(
37330            ops[0].opcode,
37331            Opcode::FusedLiteralResultRow,
37332            "first op should be the fused opcode"
37333        );
37334        assert_eq!(ops[0].p1, 42);
37335        assert_eq!(
37336            ops[1].opcode,
37337            Opcode::Noop,
37338            "second op should be Noop to preserve program length"
37339        );
37340        assert_eq!(ops[2].opcode, Opcode::Halt);
37341    }
37342
37343    #[test]
37344    fn opcode_fusion_literal_result_row_byte_equivalent_to_unfused() {
37345        let literal = 42;
37346
37347        let unfused = opcode_fusion_build_unfused_program(literal);
37348        let mut eng_unfused = VdbeEngine::new(unfused.register_count());
37349        assert_eq!(
37350            run_async(eng_unfused.execute(&unfused)).expect("unfused exec"),
37351            ExecOutcome::Done
37352        );
37353        let unfused_rows: Vec<Vec<SqliteValue>> = eng_unfused
37354            .take_results()
37355            .into_iter()
37356            .map(|row| row.into_vec())
37357            .collect();
37358
37359        let (fused, fused_count) = opcode_fusion_build_fused_program(literal);
37360        assert_eq!(fused_count, 1);
37361        let mut eng_fused = VdbeEngine::new(fused.register_count());
37362        assert_eq!(
37363            run_async(eng_fused.execute(&fused)).expect("fused exec"),
37364            ExecOutcome::Done
37365        );
37366        let fused_rows: Vec<Vec<SqliteValue>> = eng_fused
37367            .take_results()
37368            .into_iter()
37369            .map(|row| row.into_vec())
37370            .collect();
37371
37372        assert_eq!(
37373            fused_rows, unfused_rows,
37374            "fused program must emit byte-equivalent rows"
37375        );
37376        assert_eq!(
37377            fused_rows,
37378            vec![vec![SqliteValue::Integer(i64::from(literal))]]
37379        );
37380    }
37381
37382    #[test]
37383    fn opcode_fusion_peephole_skips_multi_column_result_row() {
37384        // `ResultRow(reg, 2)` emits two columns — the single-column fast
37385        // path must not fuse here because the following register value
37386        // participates in the row.
37387        let mut b = ProgramBuilder::new();
37388        let r = b.alloc_reg();
37389        let _r2 = b.alloc_reg();
37390        b.emit_op(Opcode::Integer, 7, r, 0, P4::None, 0);
37391        b.emit_op(Opcode::ResultRow, r, 2, 0, P4::None, 0);
37392        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37393
37394        let count = b.apply_fuse_literal_result_row();
37395        assert_eq!(count, 0, "multi-column ResultRow must not fuse");
37396
37397        let program = b.finish().expect("program should build");
37398        assert_eq!(program.ops()[0].opcode, Opcode::Integer);
37399        assert_eq!(program.ops()[1].opcode, Opcode::ResultRow);
37400    }
37401
37402    #[test]
37403    fn opcode_fusion_peephole_skips_register_mismatch() {
37404        // Integer writes reg A, ResultRow reads reg B — not a fusion.
37405        let mut b = ProgramBuilder::new();
37406        let r_a = b.alloc_reg();
37407        let r_b = b.alloc_reg();
37408        b.emit_op(Opcode::Integer, 7, r_a, 0, P4::None, 0);
37409        b.emit_op(Opcode::Integer, 9, r_b, 0, P4::None, 0);
37410        b.emit_op(Opcode::ResultRow, r_b, 1, 0, P4::None, 0);
37411        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37412
37413        let count = b.apply_fuse_literal_result_row();
37414        // The second Integer + ResultRow pair has matching registers and
37415        // can fuse; the first Integer (writes r_a) cannot.
37416        assert_eq!(count, 1);
37417
37418        let program = b.finish().expect("program should build");
37419        assert_eq!(program.ops()[0].opcode, Opcode::Integer);
37420        assert_eq!(program.ops()[1].opcode, Opcode::FusedLiteralResultRow);
37421        assert_eq!(program.ops()[2].opcode, Opcode::Noop);
37422    }
37423
37424    #[test]
37425    fn opcode_fusion_peephole_skips_jump_target_on_result_row() {
37426        // Goto lands on the ResultRow mid-pair — fusion would change
37427        // behavior (Noop can't do the ResultRow work), so the peephole
37428        // must refuse this site.
37429        let mut b = ProgramBuilder::new();
37430        let r = b.alloc_reg();
37431        // 0: Goto -> 2 (the ResultRow address)
37432        b.emit_op(Opcode::Goto, 0, 2, 0, P4::None, 0);
37433        // 1: Integer 5 -> r
37434        b.emit_op(Opcode::Integer, 5, r, 0, P4::None, 0);
37435        // 2: ResultRow(r, 1)  <-- jump target
37436        b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37437        // 3: Halt
37438        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37439
37440        let count = b.apply_fuse_literal_result_row();
37441        assert_eq!(count, 0, "ResultRow that is a jump target must not fuse");
37442    }
37443}