Skip to main content

fsqlite_vdbe/
lib.rs

1// bd-gird: §10.7-10.8 VDBE Instruction Format + Coroutines
2//
3// This crate provides the VDBE (Virtual Database Engine) program builder,
4// label resolution, register allocation, coroutine mechanism, and disassembly.
5// The foundational types (Opcode, VdbeOp, P4) live in fsqlite-types.
6
7// bd-h9o9r: the engine's futures are deliberately not `Send` — execution is
8// strictly sequential per connection on a current-thread runtime, with
9// RefCell-based state throughout. Requiring `Send` futures contradicts that
10// design, so the lint is noise here (same rationale as fsqlite-pager); the
11// held-across-await sites each carry their own audit tags.
12#![allow(clippy::future_not_send)]
13
14use hashbrown::{HashMap, HashSet};
15
16use fsqlite_error::{FrankenError, Result};
17use fsqlite_types::opcode::{
18    Opcode, P4, SORTER_COMPARE_TOP_N_PREFLIGHT, SORTER_OPEN_TOP_N_REGISTER, VdbeOp,
19};
20use fsqlite_types::sync_primitives::Instant;
21use std::sync::Arc;
22
23pub mod codegen;
24pub mod dataflow;
25pub mod engine;
26pub mod frame;
27pub mod jit;
28#[cfg(test)]
29mod make_record_simd;
30#[cfg(test)]
31mod repro_delete_skip;
32pub mod vectorized;
33pub mod vectorized_agg;
34#[cfg(not(target_arch = "wasm32"))]
35pub mod vectorized_dispatch;
36pub mod vectorized_hash_join;
37pub mod vectorized_join;
38pub mod vectorized_ops;
39pub mod vectorized_scan;
40pub mod vectorized_sort;
41
42/// P1 bits reserved for [`SchemaEvaluationContext`] on `Function`/`PureFunc`.
43///
44/// SQLite uses the low P1 bits for its constant-argument mask, so schema
45/// context metadata lives in the two highest non-sign bits and leaves that
46/// existing opcode payload intact.
47pub const FUNCTION_SCHEMA_CONTEXT_MASK: i32 = 0x6000_0000;
48
49/// Schema-owned expression currently being evaluated by `Function`/`PureFunc`.
50///
51/// The execution engine needs the precise owner because SQLite applies
52/// different runtime determinism rules to CHECK constraints than it does to
53/// indexes and generated columns.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[repr(i32)]
56pub enum SchemaEvaluationContext {
57    /// Expression-index key or partial-index predicate.
58    Index = 0x2000_0000,
59    /// STORED or VIRTUAL generated-column expression.
60    GeneratedColumn = 0x4000_0000,
61    /// CHECK-constraint expression evaluated for a row mutation.
62    CheckConstraint = 0x6000_0000,
63}
64
65impl SchemaEvaluationContext {
66    /// Encode this context into the reserved `Function`/`PureFunc` P1 bits.
67    #[must_use]
68    pub const fn function_p1_bits(self) -> i32 {
69        self as i32
70    }
71
72    /// Decode schema-evaluation metadata from a `Function`/`PureFunc` P1.
73    #[must_use]
74    pub const fn from_function_p1(p1: i32) -> Option<Self> {
75        match p1 & FUNCTION_SCHEMA_CONTEXT_MASK {
76            0x2000_0000 => Some(Self::Index),
77            0x4000_0000 => Some(Self::GeneratedColumn),
78            0x6000_0000 => Some(Self::CheckConstraint),
79            _ => None,
80        }
81    }
82}
83
84#[cfg(test)]
85mod vectorized_prop_tests;
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88enum VdbePipelineStage {
89    Decode,
90    Execute,
91    Commit,
92}
93
94impl VdbePipelineStage {
95    const fn as_str(self) -> &'static str {
96        match self {
97            Self::Decode => "decode",
98            Self::Execute => "execute",
99            Self::Commit => "commit",
100        }
101    }
102}
103
104#[must_use]
105pub(crate) struct VdbeProfileMarker {
106    stage: VdbePipelineStage,
107    started: Option<Instant>,
108}
109
110impl Drop for VdbeProfileMarker {
111    fn drop(&mut self) {
112        let Some(started) = self.started.take() else {
113            return;
114        };
115        let elapsed_ns = u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX);
116        tracing::trace!(
117            target: "fsqlite_vdbe::profile",
118            stage = self.stage.as_str(),
119            event = "end",
120            elapsed_ns,
121            "vdbe pipeline stage"
122        );
123    }
124}
125
126#[inline(never)]
127fn enter_vdbe_profile_stage(stage: VdbePipelineStage) -> VdbeProfileMarker {
128    if tracing::enabled!(target: "fsqlite_vdbe::profile", tracing::Level::TRACE) {
129        tracing::trace!(
130            target: "fsqlite_vdbe::profile",
131            stage = stage.as_str(),
132            event = "begin",
133            "vdbe pipeline stage"
134        );
135        VdbeProfileMarker {
136            stage,
137            started: Some(Instant::now()),
138        }
139    } else {
140        VdbeProfileMarker {
141            stage,
142            started: None,
143        }
144    }
145}
146
147pub(crate) fn enter_vdbe_decode_profile_stage() -> VdbeProfileMarker {
148    enter_vdbe_profile_stage(VdbePipelineStage::Decode)
149}
150
151pub(crate) fn enter_vdbe_execute_profile_stage() -> VdbeProfileMarker {
152    enter_vdbe_profile_stage(VdbePipelineStage::Execute)
153}
154
155pub(crate) fn enter_vdbe_commit_profile_stage() -> VdbeProfileMarker {
156    enter_vdbe_profile_stage(VdbePipelineStage::Commit)
157}
158
159pub fn profile_vdbe_decode_stage<R>(f: impl FnOnce() -> R) -> R {
160    let _profile_stage = enter_vdbe_decode_profile_stage();
161    f()
162}
163
164pub fn profile_vdbe_execute_stage<R>(f: impl FnOnce() -> R) -> R {
165    let _profile_stage = enter_vdbe_execute_profile_stage();
166    f()
167}
168
169pub fn profile_vdbe_commit_stage<R>(f: impl FnOnce() -> R) -> R {
170    let _profile_stage = enter_vdbe_commit_profile_stage();
171    f()
172}
173
174/// Register spans touched by an opcode.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub(crate) struct OpcodeRegisterSpans {
177    pub(crate) read_start: i32,
178    pub(crate) read_len: i32,
179    pub(crate) write_start: i32,
180    pub(crate) write_len: i32,
181}
182
183impl OpcodeRegisterSpans {
184    pub(crate) const NONE: Self = Self {
185        read_start: -1,
186        read_len: 0,
187        write_start: -1,
188        write_len: 0,
189    };
190
191    pub(crate) fn max_touched_register(self) -> i32 {
192        let read_end = if self.read_start > 0 {
193            self.read_start + self.read_len - 1
194        } else {
195            0
196        };
197        let write_end = if self.write_start > 0 {
198            self.write_start + self.write_len - 1
199        } else {
200            0
201        };
202        read_end.max(write_end)
203    }
204}
205
206fn register_range(start: i32, len: i32) -> (i32, i32) {
207    if start <= 0 {
208        (-1, 0)
209    } else {
210        (start, len.max(1))
211    }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215enum JumpTargetBounds {
216    Instruction,
217    InitEntry,
218}
219
220fn verify_jump_target_operand(
221    pc: usize,
222    opcode: Opcode,
223    operand_name: &'static str,
224    target: i32,
225    op_count: usize,
226    bounds: JumpTargetBounds,
227) -> Result<()> {
228    let Ok(target_usize) = usize::try_from(target) else {
229        return Err(FrankenError::Internal(format!(
230            "bytecode verification failed at pc {pc}: {} {operand_name} target {target} is negative",
231            opcode.name()
232        )));
233    };
234
235    let in_bounds = match bounds {
236        JumpTargetBounds::Instruction => target_usize < op_count,
237        JumpTargetBounds::InitEntry => target_usize <= op_count,
238    };
239    if in_bounds {
240        return Ok(());
241    }
242
243    let allowed_range = match bounds {
244        JumpTargetBounds::Instruction => format!("0..{op_count}"),
245        JumpTargetBounds::InitEntry => format!("0..={op_count}"),
246    };
247    Err(FrankenError::Internal(format!(
248        "bytecode verification failed at pc {pc}: {} {operand_name} target {target} is outside {allowed_range}",
249        opcode.name()
250    )))
251}
252
253pub(crate) fn opcode_register_spans(op: &VdbeOp) -> OpcodeRegisterSpans {
254    let (read_start, read_len, write_start, write_len) = match op.opcode {
255        Opcode::Integer
256        | Opcode::Int64
257        | Opcode::Real
258        | Opcode::String
259        | Opcode::String8
260        | Opcode::Blob
261        | Opcode::Variable => {
262            let (write_start, write_len) = register_range(op.p2, 1);
263            (-1, 0, write_start, write_len)
264        }
265        Opcode::Null => {
266            let write_count = if op.p3 > 0 { op.p3 - op.p2 + 1 } else { 1 };
267            let (write_start, write_len) = register_range(op.p2, write_count);
268            (-1, 0, write_start, write_len)
269        }
270        Opcode::SoftNull
271        | Opcode::Cast
272        | Opcode::RealAffinity
273        | Opcode::AddImm
274        | Opcode::MustBeInt
275        | Opcode::InitCoroutine
276        | Opcode::Yield
277        | Opcode::EndCoroutine => {
278            let (start, len) = register_range(op.p1, 1);
279            (start, len, start, len)
280        }
281        Opcode::Move => {
282            let (read_start, read_len) = register_range(op.p1, op.p3);
283            let (write_start, write_len) = register_range(op.p2, op.p3);
284            (read_start, read_len, write_start, write_len)
285        }
286        Opcode::Copy => {
287            let copy_len = op.p3.saturating_add(1);
288            let (read_start, read_len) = register_range(op.p1, copy_len);
289            let (write_start, write_len) = register_range(op.p2, copy_len);
290            (read_start, read_len, write_start, write_len)
291        }
292        Opcode::SCopy | Opcode::IntCopy | Opcode::BitNot | Opcode::Not => {
293            let (read_start, read_len) = register_range(op.p1, 1);
294            let (write_start, write_len) = register_range(op.p2, 1);
295            (read_start, read_len, write_start, write_len)
296        }
297        Opcode::ResultRow => {
298            let (read_start, read_len) = register_range(op.p1, op.p2);
299            (read_start, read_len, -1, 0)
300        }
301        // IMPL-13: Fused Integer+ResultRow. P2 is the (write+drain) register.
302        Opcode::FusedLiteralResultRow => {
303            let (start, len) = register_range(op.p2, 1);
304            // The opcode writes the literal into `p2`, then drains it. From a
305            // liveness standpoint it both reads and writes that single slot.
306            (start, len, start, len)
307        }
308        Opcode::ColumnSubstrPrefix | Opcode::ColumnOctetLength => {
309            let (write_start, write_len) = register_range(op.p3, 1);
310            (-1, 0, write_start, write_len)
311        }
312        Opcode::Add
313        | Opcode::Subtract
314        | Opcode::Multiply
315        | Opcode::Divide
316        | Opcode::Remainder
317        | Opcode::Concat
318        | Opcode::BitAnd
319        | Opcode::BitOr
320        | Opcode::ShiftLeft
321        | Opcode::ShiftRight
322        | Opcode::And
323        | Opcode::Or => {
324            let (read_start, read_len) = register_range(op.p1, 2);
325            let (write_start, write_len) = register_range(op.p3, 1);
326            (read_start, read_len, write_start, write_len)
327        }
328        Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
329            let (lhs_start, lhs_len) = register_range(op.p1, 1);
330            let (rhs_start, rhs_len) = register_range(op.p3, 1);
331            let (normalized_start, normalized_len) = if lhs_start > 0 && rhs_start > 0 {
332                let start = lhs_start.min(rhs_start);
333                let end = (lhs_start + lhs_len - 1).max(rhs_start + rhs_len - 1);
334                (start, end - start + 1)
335            } else if lhs_start > 0 {
336                (lhs_start, lhs_len)
337            } else if rhs_start > 0 {
338                (rhs_start, rhs_len)
339            } else {
340                (-1, 0)
341            };
342            let (write_start, write_len) = if (op.p5 & 0x20) != 0 {
343                register_range(op.p2, 1)
344            } else {
345                (-1, 0)
346            };
347            (normalized_start, normalized_len, write_start, write_len)
348        }
349        Opcode::If | Opcode::IfNot | Opcode::IsNull | Opcode::NotNull | Opcode::IsTrue => {
350            let (read_start, read_len) = register_range(op.p1, 1);
351            (read_start, read_len, -1, 0)
352        }
353        Opcode::SorterCompare => {
354            let (read_start, read_len) = register_range(op.p3, 1);
355            let (write_start, write_len) = if (op.p5 & SORTER_COMPARE_TOP_N_PREFLIGHT) != 0 {
356                (read_start, read_len)
357            } else {
358                (-1, 0)
359            };
360            (read_start, read_len, write_start, write_len)
361        }
362        Opcode::SorterOpen if (op.p5 & SORTER_OPEN_TOP_N_REGISTER) != 0 => {
363            let (read_start, read_len) = register_range(op.p3, 1);
364            (read_start, read_len, -1, 0)
365        }
366        Opcode::MakeRecord => {
367            let (read_start, read_len) = register_range(op.p1, op.p2);
368            let (write_start, write_len) = register_range(op.p3, 1);
369            (read_start, read_len, write_start, write_len)
370        }
371        _ => (
372            OpcodeRegisterSpans::NONE.read_start,
373            OpcodeRegisterSpans::NONE.read_len,
374            OpcodeRegisterSpans::NONE.write_start,
375            OpcodeRegisterSpans::NONE.write_len,
376        ),
377    };
378
379    OpcodeRegisterSpans {
380        read_start,
381        read_len,
382        write_start,
383        write_len,
384    }
385}
386
387// ── Label System ────────────────────────────────────────────────────────────
388
389/// An opaque handle representing a forward-reference label.
390///
391/// Labels allow codegen to emit jump instructions before the target address
392/// is known. All labels MUST be resolved before execution begins; unresolved
393/// labels are a codegen bug.
394#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
395pub struct Label(usize);
396
397/// Internal tracking for label resolution.
398#[derive(Debug)]
399enum LabelState {
400    /// Not yet resolved. Contains the indices of instructions whose `p2`
401    /// field should be patched when the label is resolved.
402    Unresolved(Vec<usize>),
403    /// Resolved to a concrete instruction address.
404    Resolved(i32),
405}
406
407// ── Sort Order ──────────────────────────────────────────────────────────────
408
409/// Sort direction for key comparison.
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub enum SortOrder {
412    /// Ascending order (default).
413    Asc,
414    /// Descending order.
415    Desc,
416}
417
418// ── KeyInfo ─────────────────────────────────────────────────────────────────
419
420/// Describes the key structure for multi-column index comparisons.
421///
422/// Used by Compare, IdxInsert, IdxDelete, and seek operations. Each field
423/// has an associated collation sequence and sort order.
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct KeyInfo {
426    /// Number of key fields.
427    pub num_fields: u16,
428    /// Collation sequence name per field (one entry per `num_fields`).
429    pub collations: Vec<String>,
430    /// Sort direction per field.
431    pub sort_orders: Vec<SortOrder>,
432}
433
434// ── Coroutine State ─────────────────────────────────────────────────────────
435
436/// Tracks the execution state of a coroutine.
437///
438/// Coroutines in VDBE are cooperative PC-swap state machines (NOT async).
439/// `InitCoroutine` initializes the state, `Yield` swaps PCs bidirectionally,
440/// and `EndCoroutine` marks exhaustion and returns to the caller.
441#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct CoroutineState {
443    /// The register that stores the yield/resume PC.
444    pub yield_reg: i32,
445    /// The saved program counter (where to resume).
446    pub saved_pc: i32,
447    /// Whether the coroutine has been exhausted (EndCoroutine reached).
448    pub exhausted: bool,
449}
450
451impl CoroutineState {
452    /// Create a new coroutine state with the given yield register and
453    /// initial body address.
454    pub fn new(yield_reg: i32, body_pc: i32) -> Self {
455        Self {
456            yield_reg,
457            saved_pc: body_pc,
458            exhausted: false,
459        }
460    }
461
462    /// Perform a bidirectional PC swap (Yield semantics).
463    ///
464    /// The current PC is saved into this state, and the previously saved PC
465    /// is returned as the new PC to jump to.
466    pub fn yield_swap(&mut self, current_pc: i32) -> i32 {
467        let resume_at = self.saved_pc;
468        self.saved_pc = current_pc;
469        resume_at
470    }
471
472    /// Mark the coroutine as exhausted (EndCoroutine semantics).
473    ///
474    /// Returns the saved PC to return to the caller.
475    pub fn end(&mut self) -> i32 {
476        self.exhausted = true;
477        self.saved_pc
478    }
479}
480
481// ── Register Allocator ──────────────────────────────────────────────────────
482
483/// Sequential register allocator for the VDBE register file.
484///
485/// Registers are numbered starting at 1 (register 0 is reserved/unused,
486/// matching C SQLite convention). The allocator supports both persistent
487/// registers (held for statement lifetime) and temporary registers that
488/// can be returned to a reuse pool.
489#[derive(Debug)]
490pub struct RegisterAllocator {
491    /// The next register number to allocate (starts at 1).
492    next_reg: i32,
493    /// Pool of returned temporary registers available for reuse.
494    temp_pool: Vec<i32>,
495}
496
497impl RegisterAllocator {
498    /// Create a new allocator. First allocation returns register 1.
499    pub fn new() -> Self {
500        Self {
501            next_reg: 1,
502            temp_pool: Vec::new(),
503        }
504    }
505
506    /// Allocate a single persistent register.
507    pub fn alloc_reg(&mut self) -> i32 {
508        let reg = self.next_reg;
509        self.next_reg += 1;
510        reg
511    }
512
513    /// Allocate a contiguous block of `n` persistent registers.
514    ///
515    /// Returns the first register number. The block spans `[result, result+n)`.
516    pub fn alloc_regs(&mut self, n: i32) -> i32 {
517        let first = self.next_reg;
518        self.next_reg += n;
519        first
520    }
521
522    /// Allocate a temporary register (reuses from pool if available).
523    pub fn alloc_temp(&mut self) -> i32 {
524        self.temp_pool.pop().unwrap_or_else(|| {
525            let reg = self.next_reg;
526            self.next_reg += 1;
527            reg
528        })
529    }
530
531    /// Return a temporary register to the reuse pool.
532    pub fn free_temp(&mut self, reg: i32) {
533        self.temp_pool.push(reg);
534    }
535
536    /// The total number of registers allocated (high water mark).
537    pub fn count(&self) -> i32 {
538        self.next_reg - 1
539    }
540}
541
542impl Default for RegisterAllocator {
543    fn default() -> Self {
544        Self::new()
545    }
546}
547
548// ── VDBE Program Builder ────────────────────────────────────────────────────
549
550/// A VDBE bytecode program under construction.
551///
552/// Provides methods to emit instructions, create/resolve labels for forward
553/// jumps, and allocate registers. Once construction is complete, call
554/// [`finish`](Self::finish) to validate and extract the final instruction
555/// sequence.
556#[derive(Debug)]
557pub struct ProgramBuilder {
558    /// The instruction sequence.
559    ops: smallvec::SmallVec<[VdbeOp; 64]>,
560    /// Label states (indexed by `Label.0`).
561    labels: Vec<LabelState>,
562    /// Register allocator.
563    regs: RegisterAllocator,
564    /// Counter for anonymous placeholder numbering (1-based).
565    next_anon_placeholder: u32,
566    /// Next cursor reserved for nested subqueries and other auxiliary scans.
567    next_aux_cursor: i32,
568    /// Table-to-index cursor metadata for REPLACE conflict resolution.
569    table_index_meta: HashMap<i32, Vec<fsqlite_types::opcode::IndexCursorMeta>>,
570    /// Schema-owned expression surrounding the current emission, if any.
571    schema_evaluation_context: Option<SchemaEvaluationContext>,
572}
573
574impl ProgramBuilder {
575    /// Create a new empty program builder.
576    pub fn new() -> Self {
577        Self {
578            ops: smallvec::SmallVec::new(),
579            labels: Vec::new(),
580            regs: RegisterAllocator::new(),
581            next_anon_placeholder: 1,
582            next_aux_cursor: 16_384,
583            table_index_meta: HashMap::new(),
584            schema_evaluation_context: None,
585        }
586    }
587
588    /// Reserve a contiguous range of auxiliary cursor identifiers.
589    fn alloc_aux_cursor_range(&mut self, count: i32) -> i32 {
590        assert!(count > 0, "auxiliary cursor range must be non-empty");
591        let base = self.next_aux_cursor;
592        self.next_aux_cursor = self
593            .next_aux_cursor
594            .checked_add(count)
595            .expect("auxiliary cursor identifier overflow");
596        base
597    }
598
599    /// Emit a complete schema-owned expression under `context`.
600    ///
601    /// Every scalar-function opcode emitted by `emit` is tagged so execution
602    /// can apply the owner-specific determinism policy before the surrounding
603    /// row mutation proceeds. Nested contexts restore the caller's metadata.
604    pub fn with_schema_evaluation_context<T>(
605        &mut self,
606        context: SchemaEvaluationContext,
607        emit: impl FnOnce(&mut Self) -> T,
608    ) -> T {
609        let previous = self.schema_evaluation_context.replace(context);
610        let result = emit(self);
611        self.schema_evaluation_context = previous;
612        result
613    }
614
615    /// Get the next anonymous placeholder index (1-based) and increment the counter.
616    pub fn next_anon_placeholder_idx(&mut self) -> u32 {
617        let idx = self.next_anon_placeholder;
618        self.next_anon_placeholder += 1;
619        idx
620    }
621
622    /// Set the anonymous placeholder counter to a specific value.
623    /// Used when codegen emission order differs from SQL textual order.
624    pub fn set_next_anon_placeholder(&mut self, val: u32) {
625        self.next_anon_placeholder = val;
626    }
627
628    /// Get the current anonymous placeholder counter without incrementing.
629    pub fn current_anon_placeholder(&self) -> u32 {
630        self.next_anon_placeholder
631    }
632
633    // ── Instruction emission ────────────────────────────────────────────
634
635    /// Emit a single instruction and return its address (index in `ops`).
636    pub fn emit(&mut self, op: VdbeOp) -> usize {
637        let addr = self.ops.len();
638        self.ops.push(op);
639        addr
640    }
641
642    /// Emit a simple instruction from parts.
643    pub fn emit_op(&mut self, opcode: Opcode, p1: i32, p2: i32, p3: i32, p4: P4, p5: u16) -> usize {
644        let p1 = if matches!(opcode, Opcode::Function | Opcode::PureFunc) {
645            (p1 & !FUNCTION_SCHEMA_CONTEXT_MASK)
646                | self
647                    .schema_evaluation_context
648                    .map_or(0, SchemaEvaluationContext::function_p1_bits)
649        } else {
650            p1
651        };
652        self.emit(VdbeOp {
653            opcode,
654            p1,
655            p2,
656            p3,
657            p4,
658            p5,
659        })
660    }
661
662    /// The current address (index of the next instruction to be emitted).
663    pub fn current_addr(&self) -> usize {
664        self.ops.len()
665    }
666
667    /// Get a reference to the instruction at `addr`.
668    pub fn op_at(&self, addr: usize) -> Option<&VdbeOp> {
669        self.ops.get(addr)
670    }
671
672    /// Get a mutable reference to the instruction at `addr`.
673    pub fn op_at_mut(&mut self, addr: usize) -> Option<&mut VdbeOp> {
674        self.ops.get_mut(addr)
675    }
676
677    // ── Label system ────────────────────────────────────────────────────
678
679    /// Create a new label for forward-reference jumps.
680    pub fn emit_label(&mut self) -> Label {
681        let id = self.labels.len();
682        self.labels.push(LabelState::Unresolved(Vec::new()));
683        Label(id)
684    }
685
686    /// Emit a jump instruction whose p2 target is a label (forward reference).
687    ///
688    /// The label's address will be patched into p2 when `resolve_label` is called.
689    pub fn emit_jump_to_label(
690        &mut self,
691        opcode: Opcode,
692        p1: i32,
693        p3: i32,
694        label: Label,
695        p4: P4,
696        p5: u16,
697    ) -> usize {
698        let addr = self.emit(VdbeOp {
699            opcode,
700            p1,
701            p2: -1, // placeholder; will be patched
702            p3,
703            p4,
704            p5,
705        });
706
707        let idx = label.0;
708        match &mut self.labels[idx] {
709            LabelState::Unresolved(refs) => refs.push(addr),
710            LabelState::Resolved(target) => {
711                // Label already resolved; patch immediately.
712                self.ops[addr].p2 = *target;
713            }
714        }
715
716        addr
717    }
718
719    /// Resolve a label to the current instruction address.
720    ///
721    /// All instructions that reference this label have their `p2` patched.
722    pub fn resolve_label(&mut self, label: Label) {
723        let Ok(target) = i32::try_from(self.ops.len()) else {
724            // Keep label unresolved so finish() returns a deterministic internal
725            // error instead of panicking on oversized programs.
726            return;
727        };
728        let idx = label.0;
729
730        let refs = match std::mem::replace(&mut self.labels[idx], LabelState::Resolved(target)) {
731            LabelState::Unresolved(refs) => refs,
732            LabelState::Resolved(_) => {
733                // Double resolve is a codegen bug, but we tolerate it
734                // if the target is the same.
735                return;
736            }
737        };
738
739        for op_idx in refs {
740            self.ops[op_idx].p2 = target;
741        }
742    }
743
744    /// Resolve a label to a specific address (not necessarily current).
745    pub fn resolve_label_to(&mut self, label: Label, address: i32) {
746        let idx = label.0;
747
748        let refs = match std::mem::replace(&mut self.labels[idx], LabelState::Resolved(address)) {
749            LabelState::Unresolved(refs) => refs,
750            LabelState::Resolved(_) => return,
751        };
752
753        for op_idx in refs {
754            self.ops[op_idx].p2 = address;
755        }
756    }
757
758    // ── Register allocation (delegates to RegisterAllocator) ────────────
759
760    /// Allocate a single persistent register.
761    pub fn alloc_reg(&mut self) -> i32 {
762        self.regs.alloc_reg()
763    }
764
765    /// Allocate a contiguous block of `n` persistent registers.
766    pub fn alloc_regs(&mut self, n: i32) -> i32 {
767        self.regs.alloc_regs(n)
768    }
769
770    /// Allocate a temporary register (reusable).
771    pub fn alloc_temp(&mut self) -> i32 {
772        self.regs.alloc_temp()
773    }
774
775    /// Return a temporary register to the pool.
776    pub fn free_temp(&mut self, reg: i32) {
777        self.regs.free_temp(reg);
778    }
779
780    /// Total registers allocated (high water mark).
781    pub fn register_count(&self) -> i32 {
782        self.regs.count()
783    }
784
785    // ── Table-index metadata ─────────────────────────────────────────────
786
787    /// Register the index cursors associated with a table cursor.
788    ///
789    /// Used by the engine during REPLACE conflict resolution to delete
790    /// orphaned secondary index entries before replacing the table row.
791    pub fn register_table_indexes(
792        &mut self,
793        table_cursor: i32,
794        indexes: Vec<fsqlite_types::opcode::IndexCursorMeta>,
795    ) {
796        if !indexes.is_empty() {
797            self.table_index_meta
798                .entry(table_cursor)
799                .or_default()
800                .extend(indexes);
801        }
802    }
803
804    // ── Peephole Passes (IMPL-13) ───────────────────────────────────────
805
806    /// Fuse `Integer(lit, reg) + ResultRow(reg, 1)` pairs into
807    /// `FusedLiteralResultRow(lit, reg)` + `Noop`.
808    ///
809    /// Rewrites in place so program counters, jump targets, and the label
810    /// tables remain valid without rewiring. The `ResultRow` is replaced
811    /// with a `Noop` rather than removed so no following instruction
812    /// shifts.
813    ///
814    /// Conservative preconditions per fusion site:
815    /// - The `Integer`'s target register equals the `ResultRow`'s start
816    ///   register.
817    /// - The `ResultRow` emits exactly one column (`p2 == 1`).
818    /// - The `ResultRow` is NOT a resolved jump target from any prior jump
819    ///   in this program (a mid-pair jump would otherwise skip the
820    ///   `Integer` write and run `ResultRow` against an unrelated register
821    ///   value).
822    /// - Neither instruction carries a non-`None` P4 payload.
823    /// - Both instructions carry P5 == 0 and P3 == 0.
824    ///
825    /// Returns the number of fusions performed.
826    pub fn apply_fuse_literal_result_row(&mut self) -> usize {
827        let mut jump_targets: HashSet<i32> = HashSet::new();
828        for op in self.ops.iter() {
829            if op.opcode.is_jump() {
830                jump_targets.insert(op.p2);
831            }
832        }
833
834        let mut fused = 0usize;
835        let len = self.ops.len();
836        let mut i = 0;
837        while i + 1 < len {
838            let is_int = matches!(self.ops[i].opcode, Opcode::Integer)
839                && self.ops[i].p3 == 0
840                && self.ops[i].p5 == 0
841                && matches!(self.ops[i].p4, P4::None);
842            let is_row = matches!(self.ops[i + 1].opcode, Opcode::ResultRow)
843                && self.ops[i + 1].p2 == 1
844                && self.ops[i + 1].p3 == 0
845                && self.ops[i + 1].p5 == 0
846                && matches!(self.ops[i + 1].p4, P4::None);
847            let same_reg = is_int && is_row && self.ops[i].p2 == self.ops[i + 1].p1;
848            let row_addr = i32::try_from(i + 1).ok();
849            let row_is_target = row_addr.is_some_and(|a| jump_targets.contains(&a));
850
851            if same_reg && !row_is_target {
852                let lit = self.ops[i].p1;
853                let reg = self.ops[i].p2;
854                self.ops[i] = VdbeOp {
855                    opcode: Opcode::FusedLiteralResultRow,
856                    p1: lit,
857                    p2: reg,
858                    p3: 0,
859                    p4: P4::None,
860                    p5: 0,
861                };
862                self.ops[i + 1] = VdbeOp {
863                    opcode: Opcode::Noop,
864                    p1: 0,
865                    p2: 0,
866                    p3: 0,
867                    p4: P4::None,
868                    p5: 0,
869                };
870                fused += 1;
871                i += 2;
872            } else {
873                i += 1;
874            }
875        }
876        fused
877    }
878
879    // ── Finalization ────────────────────────────────────────────────────
880
881    /// Validate all labels are resolved and return the finished program.
882    pub fn finish(self) -> Result<VdbeProgram> {
883        // Check for unresolved labels.
884        for (i, state) in self.labels.iter().enumerate() {
885            if let LabelState::Unresolved(refs) = state
886                && !refs.is_empty()
887            {
888                return Err(FrankenError::Internal(format!(
889                    "unresolved label {i} referenced by {} instruction(s)",
890                    refs.len()
891                )));
892            }
893        }
894        let bind_parameter_requirement = compute_bind_parameter_requirement(&self.ops);
895        let table_index_meta = self
896            .table_index_meta
897            .into_iter()
898            .map(|(table_cursor, indexes)| (table_cursor, indexes.into_boxed_slice()))
899            .collect();
900
901        let inferred_register_count = self.ops.iter().fold(0, |max_register, op| {
902            max_register.max(opcode_register_spans(op).max_touched_register())
903        });
904        let has_insert = self.ops.iter().any(|op| op.opcode == Opcode::Insert);
905        // bd-perf (V2.1): Peephole pass — fuse NewRowid+MakeRecord+Insert
906        // into FusedAppendInsert for simple sequential append patterns.
907        let mut ops = self.ops;
908        peephole_fuse_append_insert(&mut ops);
909        // SAFETY: The VDBE dispatch loop relies on every non-empty program
910        // terminating via OP_Halt (bounds check was removed in V2.3).
911        // Empty programs are fine — the loop's debug_assert catches pc=0 >= len=0.
912        if !ops.is_empty() {
913            debug_assert!(
914                ops.last().is_some_and(|op| op.opcode == Opcode::Halt),
915                "VDBE program does not end with Halt — last opcode is {:?}",
916                ops.last().map(|op| op.opcode)
917            );
918        }
919        let requires_attached_memdb = compute_requires_attached_memdb(&ops);
920        let requires_version_store = ops.iter().any(|op| op.opcode == Opcode::SetSnapshot);
921        let program = VdbeProgram {
922            ops,
923            register_count: self.regs.count().max(inferred_register_count),
924            bind_parameter_requirement,
925            table_index_meta: Arc::new(table_index_meta),
926            has_insert,
927            requires_attached_memdb,
928            requires_version_store,
929        };
930        program.verify_control_flow_targets()?;
931        Ok(program)
932    }
933}
934
935impl Default for ProgramBuilder {
936    fn default() -> Self {
937        Self::new()
938    }
939}
940
941/// bd-perf (V2.1): Peephole optimizer — fuse NewRowid+MakeRecord+Insert into
942/// FusedAppendInsert. Called from `ProgramBuilder::finish()` after label resolution.
943fn peephole_fuse_append_insert(ops: &mut smallvec::SmallVec<[VdbeOp; 64]>) {
944    let len = ops.len();
945    if len < 3 {
946        return;
947    }
948    let mut i = 0;
949    while i + 2 < len {
950        if ops[i].opcode == Opcode::NewRowid
951            && ops[i + 1].opcode == Opcode::MakeRecord
952            && ops[i + 2].opcode == Opcode::Insert
953        {
954            let cursor = ops[i].p1;
955            let r_rowid = ops[i].p2;
956            let r_start = ops[i + 1].p1;
957            let n_cols = ops[i + 1].p2;
958            let r_record = ops[i + 1].p3;
959            let make_record_p4 = ops[i + 1].p4.clone();
960            let insert_cursor = ops[i + 2].p1;
961            let insert_record_reg = ops[i + 2].p2;
962            let insert_rowid_reg = ops[i + 2].p3;
963            let insert_flags = ops[i + 2].p5;
964            let oe_flag = insert_flags & 0x0F;
965
966            if cursor == insert_cursor
967                && r_record == insert_record_reg
968                && r_rowid == insert_rowid_reg
969                && oe_flag == 2
970            // OE_ABORT only
971            {
972                ops[i] = VdbeOp {
973                    opcode: Opcode::FusedAppendInsert,
974                    p1: cursor,
975                    p2: r_start,
976                    p3: n_cols,
977                    p4: make_record_p4,
978                    p5: insert_flags,
979                };
980                ops[i + 1] = VdbeOp {
981                    opcode: Opcode::Noop,
982                    p1: 0,
983                    p2: 0,
984                    p3: 0,
985                    p4: P4::None,
986                    p5: 0,
987                };
988                ops[i + 2] = VdbeOp {
989                    opcode: Opcode::Noop,
990                    p1: 0,
991                    p2: 0,
992                    p3: 0,
993                    p4: P4::None,
994                    p5: 0,
995                };
996                i += 3;
997                continue;
998            }
999        }
1000        i += 1;
1001    }
1002
1003    // bd-perf (V2.2): FusedOpenWriteLast DISABLED — Last has a P2 jump-if-empty
1004    // target that must be preserved. The Noop replacement silently dropped the
1005    // jump, causing data corruption when tables are empty (rowid 0 inserted
1006    // instead of jumping past the insert body). The ~5-7ns savings isn't worth
1007    // the correctness risk. Keep opcode defined for future proper implementation.
1008}
1009
1010/// Returns `true` when a finalized program still needs an attached
1011/// `MemDatabase` to preserve its current semantics.
1012///
1013/// This is intentionally conservative. It only returns `false` for programs
1014/// that stay on storage cursors plus pure register/control-flow opcodes, which
1015/// lets hot prepared table executions skip the `MemDatabase` handoff entirely.
1016fn compute_requires_attached_memdb(ops: &[VdbeOp]) -> bool {
1017    compute_attached_memdb_requirement_reason(ops).is_some()
1018}
1019
1020/// Return the first conservative reason a finalized program still needs an
1021/// attached `MemDatabase`.
1022fn compute_attached_memdb_requirement_reason(ops: &[VdbeOp]) -> Option<&'static str> {
1023    let mut storage_cursor_ids = HashSet::new();
1024    let mut sorter_cursor_ids = HashSet::new();
1025
1026    for op in ops {
1027        match op.opcode {
1028            Opcode::OpenRead | Opcode::OpenWrite | Opcode::FusedOpenWriteLast => {
1029                if op.p3 == 1 {
1030                    return Some("temp_database_cursor");
1031                }
1032                storage_cursor_ids.insert(op.p1);
1033            }
1034            Opcode::SorterOpen => {
1035                sorter_cursor_ids.insert(op.p1);
1036            }
1037            Opcode::Close => {
1038                storage_cursor_ids.remove(&op.p1);
1039                sorter_cursor_ids.remove(&op.p1);
1040            }
1041            Opcode::OpenEphemeral
1042            | Opcode::OpenAutoindex
1043            | Opcode::OpenPseudo
1044            | Opcode::OpenDup
1045            | Opcode::ReopenIdx
1046            | Opcode::CreateBtree
1047            | Opcode::Clear
1048            | Opcode::Destroy
1049            | Opcode::Pagecount
1050            | Opcode::Program
1051            | Opcode::VBegin
1052            | Opcode::VCreate
1053            | Opcode::VDestroy
1054            | Opcode::VOpen
1055            | Opcode::VCheck
1056            | Opcode::VInitIn
1057            | Opcode::VFilter
1058            | Opcode::VColumn
1059            | Opcode::VNext
1060            | Opcode::VRename
1061            | Opcode::VUpdate => return Some("memdb_or_virtual_table_opcode"),
1062            Opcode::Rewind
1063            | Opcode::Last
1064            | Opcode::Next
1065            | Opcode::Prev
1066            | Opcode::Column
1067            | Opcode::ColumnSubstrPrefix
1068            | Opcode::ColumnOctetLength
1069            | Opcode::Count
1070            | Opcode::SeekLT
1071            | Opcode::SeekLE
1072            | Opcode::SeekGE
1073            | Opcode::SeekGT
1074            | Opcode::IfNoHope
1075            | Opcode::NoConflict
1076            | Opcode::NotFound
1077            | Opcode::Found
1078            | Opcode::SeekRowid
1079            | Opcode::NotExists
1080            | Opcode::Insert
1081            | Opcode::Delete
1082            | Opcode::RowData
1083            | Opcode::Rowid
1084            | Opcode::NullRow
1085            | Opcode::IfNullRow
1086            | Opcode::IfEmpty
1087            | Opcode::IfSizeBetween
1088            | Opcode::IdxInsert
1089            | Opcode::IdxDelete
1090            | Opcode::DeferredSeek
1091            | Opcode::IdxRowid
1092            | Opcode::FinishSeek
1093            | Opcode::IdxLE
1094            | Opcode::IdxGT
1095            | Opcode::IdxLT
1096            | Opcode::IdxGE
1097            | Opcode::SetSnapshot
1098            | Opcode::CountIndexEqRun
1099            | Opcode::FusedAppendInsert
1100                if !storage_cursor_ids.contains(&op.p1) && !sorter_cursor_ids.contains(&op.p1) =>
1101            {
1102                return Some("cursor_opcode_without_storage_or_sorter_open");
1103            }
1104            _ => {}
1105        }
1106    }
1107
1108    None
1109}
1110
1111// ── VDBE Program ────────────────────────────────────────────────────────────
1112
1113pub(crate) type TableIndexMetaMap = HashMap<i32, Box<[fsqlite_types::opcode::IndexCursorMeta]>>;
1114
1115/// Static storage role inferred from a VDBE root cursor open.
1116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1117pub enum StorageRootRole {
1118    Table,
1119    Index,
1120    Unknown,
1121}
1122
1123/// Static storage access kind inferred from a VDBE root cursor open.
1124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125pub enum StorageRootAccess {
1126    Read,
1127    Write,
1128}
1129
1130/// Deterministic storage-root usage emitted by finalized bytecode.
1131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1132pub struct StorageRootUsage {
1133    pub pc: usize,
1134    pub cursor_id: i32,
1135    pub root_page: i32,
1136    pub access: StorageRootAccess,
1137    pub role: StorageRootRole,
1138}
1139
1140fn storage_root_usage_for_op(pc: usize, op: &VdbeOp) -> Option<StorageRootUsage> {
1141    let access = match op.opcode {
1142        Opcode::OpenRead => StorageRootAccess::Read,
1143        Opcode::OpenWrite | Opcode::FusedOpenWriteLast => StorageRootAccess::Write,
1144        _ => return None,
1145    };
1146    let role = match &op.p4 {
1147        P4::Table(_) => StorageRootRole::Table,
1148        P4::Index(_) => StorageRootRole::Index,
1149        _ => StorageRootRole::Unknown,
1150    };
1151
1152    Some(StorageRootUsage {
1153        pc,
1154        cursor_id: op.p1,
1155        root_page: op.p2,
1156        access,
1157        role,
1158    })
1159}
1160
1161/// A finalized VDBE bytecode program ready for execution.
1162#[derive(Debug, Clone, PartialEq)]
1163pub struct VdbeProgram {
1164    /// The instruction sequence.
1165    ops: smallvec::SmallVec<[VdbeOp; 64]>,
1166    /// Number of registers needed (high water mark from allocation).
1167    register_count: i32,
1168    /// Precomputed bind parameter requirement for `Opcode::Variable` opcodes.
1169    ///
1170    /// `Ok(max_index)` means all variable opcodes carry valid 1-based indexes.
1171    /// `Err(raw_index)` stores the first invalid raw index encountered.
1172    bind_parameter_requirement: std::result::Result<usize, i32>,
1173    /// Table-to-index cursor metadata for REPLACE conflict resolution.
1174    table_index_meta: Arc<TableIndexMetaMap>,
1175    /// Precomputed flag: true when the program contains at least one Insert
1176    /// opcode, meaning column defaults may be needed during execution.
1177    has_insert: bool,
1178    /// Precomputed flag: true when execution still needs an attached
1179    /// `MemDatabase` to preserve current opcode semantics.
1180    requires_attached_memdb: bool,
1181    /// Precomputed flag: true when execution can request historical pages.
1182    requires_version_store: bool,
1183}
1184
1185impl VdbeProgram {
1186    /// Route storage-root opens for connection-local TEMP objects through
1187    /// SQLite's database-number 1 namespace.
1188    ///
1189    /// Code generation deliberately operates on schema metadata rather than
1190    /// connection state, so it cannot know which otherwise ordinary table
1191    /// roots belong to the TEMP database. The connection applies this final
1192    /// annotation after codegen. Execution then uses the attached
1193    /// [`MemDatabase`](crate::engine::MemDatabase) instead of ever consulting
1194    /// or mutating pages in the main pager.
1195    pub fn route_storage_roots_to_temp_database(
1196        &mut self,
1197        temp_roots: impl IntoIterator<Item = i32>,
1198    ) {
1199        let temp_roots: HashSet<i32> = temp_roots.into_iter().collect();
1200        if temp_roots.is_empty() {
1201            return;
1202        }
1203
1204        let mut routed_any = false;
1205        for op in &mut self.ops {
1206            if matches!(
1207                op.opcode,
1208                Opcode::OpenRead | Opcode::OpenWrite | Opcode::FusedOpenWriteLast
1209            ) && temp_roots.contains(&op.p2)
1210            {
1211                op.p3 = 1;
1212                routed_any = true;
1213            }
1214        }
1215        if routed_any {
1216            self.requires_attached_memdb = true;
1217        }
1218    }
1219
1220    fn verify_control_flow_targets(&self) -> Result<()> {
1221        let op_count = self.ops.len();
1222        for (pc, op) in self.ops.iter().enumerate() {
1223            match op.opcode {
1224                Opcode::Init => verify_jump_target_operand(
1225                    pc,
1226                    op.opcode,
1227                    "p2",
1228                    op.p2,
1229                    op_count,
1230                    JumpTargetBounds::InitEntry,
1231                )?,
1232                Opcode::Goto
1233                | Opcode::Gosub
1234                | Opcode::Once
1235                | Opcode::If
1236                | Opcode::IfNot
1237                | Opcode::IsNull
1238                | Opcode::NotNull
1239                | Opcode::Rewind
1240                | Opcode::Sort
1241                | Opcode::SorterSort
1242                | Opcode::Last
1243                | Opcode::Next
1244                | Opcode::SorterNext
1245                | Opcode::Prev
1246                | Opcode::SeekRowid
1247                | Opcode::SeekGE
1248                | Opcode::SeekGT
1249                | Opcode::SeekLE
1250                | Opcode::SeekLT
1251                | Opcode::NotFound
1252                | Opcode::NotExists
1253                | Opcode::IfNoHope
1254                | Opcode::Found
1255                | Opcode::NoConflict
1256                | Opcode::SorterCompare
1257                | Opcode::IfNullRow
1258                | Opcode::IfNotOpen
1259                | Opcode::IsType
1260                | Opcode::IfEmpty
1261                | Opcode::IfSizeBetween
1262                | Opcode::IdxRowid
1263                | Opcode::IdxLE
1264                | Opcode::IdxGT
1265                | Opcode::IdxLT
1266                | Opcode::IdxGE
1267                | Opcode::DecrJumpZero
1268                | Opcode::IfPos
1269                | Opcode::RowSetRead
1270                | Opcode::RowSetTest
1271                | Opcode::FkIfZero
1272                | Opcode::IfNotZero
1273                | Opcode::IncrVacuum
1274                | Opcode::Filter
1275                | Opcode::VFilter
1276                | Opcode::VNext => verify_jump_target_operand(
1277                    pc,
1278                    op.opcode,
1279                    "p2",
1280                    op.p2,
1281                    op_count,
1282                    JumpTargetBounds::Instruction,
1283                )?,
1284                Opcode::MustBeInt | Opcode::InitCoroutine if op.p2 > 0 => {
1285                    verify_jump_target_operand(
1286                        pc,
1287                        op.opcode,
1288                        "p2",
1289                        op.p2,
1290                        op_count,
1291                        JumpTargetBounds::Instruction,
1292                    )?;
1293                }
1294                Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge
1295                    if (op.p5 & 0x20) == 0 =>
1296                {
1297                    verify_jump_target_operand(
1298                        pc,
1299                        op.opcode,
1300                        "p2",
1301                        op.p2,
1302                        op_count,
1303                        JumpTargetBounds::Instruction,
1304                    )?;
1305                }
1306                Opcode::Jump => {
1307                    verify_jump_target_operand(
1308                        pc,
1309                        op.opcode,
1310                        "p1",
1311                        op.p1,
1312                        op_count,
1313                        JumpTargetBounds::Instruction,
1314                    )?;
1315                    verify_jump_target_operand(
1316                        pc,
1317                        op.opcode,
1318                        "p2",
1319                        op.p2,
1320                        op_count,
1321                        JumpTargetBounds::Instruction,
1322                    )?;
1323                    verify_jump_target_operand(
1324                        pc,
1325                        op.opcode,
1326                        "p3",
1327                        op.p3,
1328                        op_count,
1329                        JumpTargetBounds::Instruction,
1330                    )?;
1331                }
1332                _ => {}
1333            }
1334        }
1335        Ok(())
1336    }
1337
1338    /// The instruction sequence.
1339    pub fn ops(&self) -> &[VdbeOp] {
1340        &self.ops
1341    }
1342
1343    /// Number of instructions.
1344    pub fn len(&self) -> usize {
1345        self.ops.len()
1346    }
1347
1348    /// Whether the program is empty.
1349    pub fn is_empty(&self) -> bool {
1350        self.ops.is_empty()
1351    }
1352
1353    /// Number of registers required.
1354    pub fn register_count(&self) -> i32 {
1355        self.register_count
1356    }
1357
1358    /// Highest 1-based bind parameter index referenced by the program.
1359    ///
1360    /// Returns `Ok(0)` when no `Variable` opcodes are present.
1361    /// Returns `Err(raw_index)` if the bytecode contains an invalid
1362    /// parameter index (`<= 0` or not representable as `usize`).
1363    pub fn max_bind_parameter_index(&self) -> std::result::Result<usize, i32> {
1364        self.bind_parameter_requirement
1365    }
1366
1367    /// Get the instruction at the given program counter.
1368    pub fn get(&self, pc: usize) -> Option<&VdbeOp> {
1369        self.ops.get(pc)
1370    }
1371
1372    /// Table-to-index cursor metadata for REPLACE conflict resolution.
1373    pub fn table_index_meta(&self) -> &TableIndexMetaMap {
1374        self.table_index_meta.as_ref()
1375    }
1376
1377    /// Returns storage B-tree root usage in deterministic instruction order.
1378    ///
1379    /// Conflict-topology and backend-identity diagnostics can use this to tie
1380    /// bytecode to root-page level heat without ad hoc opcode scans.
1381    pub fn storage_root_usages(&self) -> impl Iterator<Item = StorageRootUsage> + '_ {
1382        self.ops
1383            .iter()
1384            .enumerate()
1385            .filter_map(|(pc, op)| storage_root_usage_for_op(pc, op))
1386    }
1387
1388    pub(crate) fn shared_table_index_meta(&self) -> &Arc<TableIndexMetaMap> {
1389        &self.table_index_meta
1390    }
1391
1392    /// Returns `true` if the program contains any `Insert` opcodes,
1393    /// meaning column defaults may be needed during execution.
1394    /// Precomputed at build time — O(1) at call time.
1395    pub fn has_insert_ops(&self) -> bool {
1396        self.has_insert
1397    }
1398
1399    /// Returns `true` when this program still requires an attached
1400    /// `MemDatabase` for opcode semantics.
1401    pub fn requires_attached_memdb(&self) -> bool {
1402        self.requires_attached_memdb
1403    }
1404
1405    /// Returns the first conservative reason this program still requires an
1406    /// attached `MemDatabase`, or `None` for storage-only VDBE programs.
1407    pub fn attached_memdb_requirement_reason(&self) -> Option<&'static str> {
1408        compute_attached_memdb_requirement_reason(&self.ops)
1409    }
1410
1411    /// Returns `true` when this program can read historical page versions.
1412    pub fn requires_version_store(&self) -> bool {
1413        self.requires_version_store
1414    }
1415
1416    /// Disassemble the program to a human-readable string.
1417    ///
1418    /// Output format matches SQLite's `EXPLAIN` output:
1419    /// ```text
1420    /// addr  opcode         p1    p2    p3    p4             p5
1421    /// ----  ----------     ----  ----  ----  -----          --
1422    /// 0     Init           0     8     0                    0
1423    /// ```
1424    pub fn disassemble(&self) -> String {
1425        use std::fmt::Write;
1426
1427        let mut out = std::string::String::with_capacity(self.ops.len() * 60);
1428        out.push_str("addr  opcode           p1    p2    p3    p4                 p5\n");
1429        out.push_str("----  ---------------  ----  ----  ----  -----------------  --\n");
1430
1431        for (addr, op) in self.ops.iter().enumerate() {
1432            let p4_str = match &op.p4 {
1433                P4::None => String::new(),
1434                P4::Int(v) => format!("(int){v}"),
1435                P4::Int64(v) => format!("(i64){v}"),
1436                P4::Real(v) => format!("(real){v}"),
1437                P4::Str(s) => format!("(str){s}"),
1438                P4::Blob(b) => format!("(blob)[{}B]", b.len()),
1439                P4::Collation(c) => format!("(coll){c}"),
1440                P4::FuncName(f) => format!("(func){f}"),
1441                P4::FuncNameCollated(f, c) => format!("(func){f} coll={c}"),
1442                P4::Table(t) => format!("(tbl){t}"),
1443                P4::Index(i) => format!("(idx){i}"),
1444                P4::Affinity(a) => format!("(aff){a}"),
1445                P4::PrecomputedHeader(header) => format!("(hdr)[{}B]", header.template.len()),
1446                P4::TimeTravelCommitSeq(seq) => format!("(tt-seq){seq}"),
1447                P4::TimeTravelTimestamp(ts) => format!("(tt-ts){ts}"),
1448            };
1449
1450            let _ = writeln!(
1451                &mut out,
1452                "{addr:<4}  {:<15}  {:<4}  {:<4}  {:<4}  {:<17}  {:<2}",
1453                op.opcode.name(),
1454                op.p1,
1455                op.p2,
1456                op.p3,
1457                p4_str,
1458                op.p5,
1459            );
1460        }
1461
1462        out
1463    }
1464}
1465
1466fn compute_bind_parameter_requirement(ops: &[VdbeOp]) -> std::result::Result<usize, i32> {
1467    let mut max_required = 0_usize;
1468    for op in ops {
1469        if op.opcode != Opcode::Variable {
1470            continue;
1471        }
1472        let one_based = match usize::try_from(op.p1) {
1473            Ok(index) if index > 0 => index,
1474            _ => return Err(op.p1),
1475        };
1476        max_required = max_required.max(one_based);
1477    }
1478    Ok(max_required)
1479}
1480
1481// ── PRAGMA Handling ──────────────────────────────────────────────────────────
1482
1483/// Minimal PRAGMA dispatch for early phases.
1484///
1485/// The full engine will execute PRAGMA statements through the SQL pipeline,
1486/// but we keep these handlers in VDBE (the execution boundary) so higher layers
1487/// can remain declarative.
1488pub mod pragma {
1489    use std::path::Path;
1490
1491    use fsqlite_ast::{Expr, Literal, PragmaStatement, PragmaValue, QualifiedName, UnaryOp};
1492    use fsqlite_error::{FrankenError, Result};
1493    use fsqlite_mvcc::TransactionManager;
1494    use fsqlite_wal::{
1495        DEFAULT_RAPTORQ_REPAIR_SYMBOLS, MAX_RAPTORQ_REPAIR_SYMBOLS,
1496        persist_wal_fec_raptorq_repair_symbols, read_wal_fec_raptorq_repair_symbols,
1497    };
1498    use tracing::{debug, error, info, warn};
1499
1500    /// Result of applying a PRAGMA statement.
1501    #[derive(Debug, Clone, PartialEq, Eq)]
1502    pub enum PragmaOutput {
1503        /// PRAGMA not recognized by this handler.
1504        Unsupported,
1505        /// PRAGMA yields a boolean value (e.g. query or echo after set).
1506        Bool(bool),
1507        /// PRAGMA yields an integer value.
1508        Int(i64),
1509        /// PRAGMA yields a text value (e.g. `journal_mode`).
1510        Text(String),
1511    }
1512
1513    /// Connection-level settings controlled by PRAGMA statements.
1514    ///
1515    /// These mirror the standard SQLite PRAGMAs that the E2E harness needs to
1516    /// set consistently across both `sqlite3` and FrankenSQLite runs.  Values
1517    /// are stored here for future backend wiring (Phase 5+) and are immediately
1518    /// queryable via `PRAGMA <name>`.
1519    #[derive(Debug, Clone, Copy)]
1520    pub enum DifferentialViewsSetting {
1521        Off,
1522        On,
1523    }
1524
1525    impl DifferentialViewsSetting {
1526        #[must_use]
1527        pub const fn is_enabled(&self) -> bool {
1528            matches!(self, Self::On)
1529        }
1530
1531        #[must_use]
1532        pub const fn from_enabled(enabled: bool) -> Self {
1533            if enabled { Self::On } else { Self::Off }
1534        }
1535    }
1536
1537    #[derive(Debug, Clone)]
1538    #[allow(clippy::struct_excessive_bools)]
1539    pub struct ConnectionPragmaState {
1540        /// Journal mode (`delete`, `truncate`, `persist`, `memory`, `wal`, `off`).
1541        pub journal_mode: String,
1542        /// Synchronous level (`OFF`, `NORMAL`, `FULL`, `EXTRA`).
1543        pub synchronous: String,
1544        /// Page cache size (negative = KiB, positive = pages).
1545        pub cache_size: i64,
1546        /// Page size in bytes (512..=65536, power of two).
1547        pub page_size: u32,
1548        /// Busy timeout in milliseconds for lock contention.
1549        pub busy_timeout_ms: i64,
1550        /// Temporary storage mode (`0` default, `1` file, `2` memory).
1551        pub temp_store: i64,
1552        /// Memory-map size in bytes (`PRAGMA mmap_size`).
1553        pub mmap_size: i64,
1554        /// Auto-vacuum mode (`0` none, `1` full, `2` incremental).
1555        pub auto_vacuum: i64,
1556        /// WAL auto-checkpoint threshold in pages.
1557        pub wal_autocheckpoint: i64,
1558        /// User schema version (`PRAGMA user_version`).
1559        pub user_version: i64,
1560        /// Application ID (`PRAGMA application_id`).
1561        pub application_id: i64,
1562        /// Persistent suggested default page-cache size, header bytes 48..52
1563        /// (`PRAGMA default_cache_size`). `0` means unset — a bare query then
1564        /// reports the compiled default (`-2000`). Stock stores `abs(N)` as a
1565        /// page count (legacy `sqlite3AbsInt32`); loaded from the header at open.
1566        pub default_cache_size: i64,
1567        /// Foreign key enforcement toggle (`PRAGMA foreign_keys`).
1568        pub foreign_keys: bool,
1569        /// Recursive trigger toggle (`PRAGMA recursive_triggers`).
1570        pub recursive_triggers: bool,
1571        /// Query-only toggle (`PRAGMA query_only`).
1572        pub query_only: bool,
1573        /// Connection-level SSI toggle (`PRAGMA fsqlite.serializable`).
1574        pub serializable: bool,
1575        /// Differential-view streaming toggle (`PRAGMA fsqlite_differential_views`).
1576        pub differential_views: DifferentialViewsSetting,
1577        /// WAL-FEC repair symbol budget (`PRAGMA raptorq_repair_symbols`).
1578        pub raptorq_repair_symbols: u8,
1579        /// MVCC maximum committed versions per page chain before eager GC.
1580        /// `PRAGMA fsqlite.mvcc_max_chain_length`.
1581        pub mvcc_max_chain_length: usize,
1582        /// MVCC serialized writer lease duration in seconds.
1583        /// `PRAGMA fsqlite.mvcc_writer_lease_secs`.
1584        pub mvcc_writer_lease_secs: u64,
1585        /// `PRAGMA writable_schema` toggle — allows direct DML on sqlite_master.
1586        pub writable_schema: bool,
1587        /// `PRAGMA case_sensitive_like` toggle. When `false` (the default) LIKE
1588        /// folds ASCII case; when `true` LIKE is byte-exact (case-sensitive).
1589        pub case_sensitive_like: bool,
1590        /// `PRAGMA trusted_schema` toggle. Default ON (1), matching the C library
1591        /// default (`SQLITE_TRUSTED_SCHEMA` unset). Set/readback surface only.
1592        pub trusted_schema: bool,
1593        /// `PRAGMA read_uncommitted` toggle (default OFF). Set/readback surface.
1594        pub read_uncommitted: bool,
1595        /// `PRAGMA cell_size_check` toggle (default OFF). Set/readback surface.
1596        pub cell_size_check: bool,
1597        /// `PRAGMA checkpoint_fullfsync` toggle (default OFF). Set/readback surface.
1598        pub checkpoint_fullfsync: bool,
1599        /// `PRAGMA automatic_index` toggle (default ON). Set/readback surface.
1600        pub automatic_index: bool,
1601        /// `PRAGMA locking_mode` (`normal` or `exclusive`; default `normal`).
1602        /// Set/readback surface only.
1603        pub locking_mode: String,
1604        /// `PRAGMA secure_delete` tri-state: 0 = OFF, 1 = ON, 2 = FAST (default 0).
1605        /// Set/readback surface only.
1606        pub secure_delete: i64,
1607        /// `PRAGMA threads` auxiliary worker-thread limit (default 0). Advisory:
1608        /// FrankenSQLite does not spawn SQLite-style sort helper threads, so this
1609        /// is a stored limit clamped to the stock maximum (8).
1610        pub threads: i64,
1611    }
1612
1613    impl Default for ConnectionPragmaState {
1614        fn default() -> Self {
1615            Self {
1616                journal_mode: "wal".to_owned(),
1617                synchronous: "NORMAL".to_owned(),
1618                cache_size: -2000,
1619                page_size: 4096,
1620                busy_timeout_ms: 5000,
1621                temp_store: 0,
1622                mmap_size: 0,
1623                auto_vacuum: 0,
1624                wal_autocheckpoint: 1000,
1625                user_version: 0,
1626                application_id: 0,
1627                default_cache_size: 0,
1628                foreign_keys: false,
1629                recursive_triggers: false,
1630                query_only: false,
1631                serializable: true,
1632                differential_views: DifferentialViewsSetting::Off,
1633                raptorq_repair_symbols: DEFAULT_RAPTORQ_REPAIR_SYMBOLS,
1634                mvcc_max_chain_length: 64,
1635                mvcc_writer_lease_secs: 30,
1636                writable_schema: false,
1637                case_sensitive_like: false,
1638                trusted_schema: true,
1639                read_uncommitted: false,
1640                cell_size_check: false,
1641                checkpoint_fullfsync: false,
1642                automatic_index: true,
1643                locking_mode: "normal".to_owned(),
1644                secure_delete: 0,
1645                threads: 0,
1646            }
1647        }
1648    }
1649
1650    /// Apply a PRAGMA statement to the provided connection-scoped state.
1651    ///
1652    /// Currently supports:
1653    /// - `PRAGMA fsqlite.serializable`
1654    /// - `PRAGMA fsqlite.serializable = ON|OFF|TRUE|FALSE|1|0`
1655    /// - `PRAGMA raptorq_repair_symbols`
1656    /// - `PRAGMA raptorq_repair_symbols = N` (N in [0, 255])
1657    ///
1658    /// Unknown pragmas return [`PragmaOutput::Unsupported`].
1659    pub fn apply(mgr: &mut TransactionManager, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1660        apply_with_sidecar(mgr, stmt, None)
1661    }
1662
1663    /// Apply a PRAGMA statement with optional `.wal-fec` sidecar persistence.
1664    pub fn apply_with_sidecar(
1665        mgr: &mut TransactionManager,
1666        stmt: &PragmaStatement,
1667        wal_fec_sidecar_path: Option<&Path>,
1668    ) -> Result<PragmaOutput> {
1669        if is_fsqlite_serializable(&stmt.name) {
1670            return apply_serializable(mgr, stmt);
1671        }
1672        if is_raptorq_repair_symbols(&stmt.name) {
1673            return apply_raptorq_repair_symbols(mgr, stmt, wal_fec_sidecar_path);
1674        }
1675        Ok(PragmaOutput::Unsupported)
1676    }
1677
1678    /// Apply a PRAGMA to connection-level settings.
1679    ///
1680    /// Handles common connection-scoped PRAGMAs used by the harness and
1681    /// compatibility paths. Returns `Unsupported` for pragmas not handled at
1682    /// this layer, allowing the caller to chain with [`apply`].
1683    pub fn apply_connection_pragma(
1684        state: &mut ConnectionPragmaState,
1685        stmt: &PragmaStatement,
1686    ) -> Result<PragmaOutput> {
1687        let name = &stmt.name.name;
1688        if is_fsqlite_serializable(&stmt.name) {
1689            return apply_serializable_connection(state, stmt);
1690        }
1691        if is_fsqlite_differential_views(&stmt.name) {
1692            return apply_differential_views_connection(state, stmt);
1693        }
1694        if is_raptorq_repair_symbols(&stmt.name) {
1695            return apply_raptorq_repair_symbols_connection(state, stmt);
1696        }
1697        if name.eq_ignore_ascii_case("journal_mode") {
1698            return apply_journal_mode(state, stmt);
1699        }
1700        if name.eq_ignore_ascii_case("synchronous") {
1701            return apply_synchronous(state, stmt);
1702        }
1703        if name.eq_ignore_ascii_case("cache_size") {
1704            return apply_cache_size(state, stmt);
1705        }
1706        if name.eq_ignore_ascii_case("page_size") {
1707            return apply_page_size(state, stmt);
1708        }
1709        if name.eq_ignore_ascii_case("busy_timeout") {
1710            return apply_busy_timeout(state, stmt);
1711        }
1712        if name.eq_ignore_ascii_case("temp_store") {
1713            return apply_temp_store(state, stmt);
1714        }
1715        if name.eq_ignore_ascii_case("mmap_size") {
1716            return apply_mmap_size(state, stmt);
1717        }
1718        if name.eq_ignore_ascii_case("auto_vacuum") {
1719            return apply_auto_vacuum(state, stmt);
1720        }
1721        if name.eq_ignore_ascii_case("wal_autocheckpoint") {
1722            return apply_wal_autocheckpoint(state, stmt);
1723        }
1724        if name.eq_ignore_ascii_case("user_version") {
1725            return apply_user_version(state, stmt);
1726        }
1727        if name.eq_ignore_ascii_case("application_id") {
1728            return apply_application_id(state, stmt);
1729        }
1730        if name.eq_ignore_ascii_case("default_cache_size") {
1731            return apply_default_cache_size(state, stmt);
1732        }
1733        if name.eq_ignore_ascii_case("foreign_keys") {
1734            return apply_foreign_keys(state, stmt);
1735        }
1736        if name.eq_ignore_ascii_case("recursive_triggers") {
1737            return apply_recursive_triggers(state, stmt);
1738        }
1739        if name.eq_ignore_ascii_case("query_only") {
1740            return apply_query_only(state, stmt);
1741        }
1742        if name.eq_ignore_ascii_case("writable_schema") {
1743            return apply_writable_schema(state, stmt);
1744        }
1745        if name.eq_ignore_ascii_case("case_sensitive_like") {
1746            return apply_case_sensitive_like(state, stmt);
1747        }
1748        if name.eq_ignore_ascii_case("trusted_schema") {
1749            return apply_bool_toggle(&mut state.trusted_schema, stmt);
1750        }
1751        if name.eq_ignore_ascii_case("read_uncommitted") {
1752            return apply_bool_toggle(&mut state.read_uncommitted, stmt);
1753        }
1754        if name.eq_ignore_ascii_case("cell_size_check") {
1755            return apply_bool_toggle(&mut state.cell_size_check, stmt);
1756        }
1757        if name.eq_ignore_ascii_case("checkpoint_fullfsync") {
1758            return apply_bool_toggle(&mut state.checkpoint_fullfsync, stmt);
1759        }
1760        if name.eq_ignore_ascii_case("automatic_index") {
1761            return apply_bool_toggle(&mut state.automatic_index, stmt);
1762        }
1763        if name.eq_ignore_ascii_case("locking_mode") {
1764            return apply_locking_mode(state, stmt);
1765        }
1766        if name.eq_ignore_ascii_case("secure_delete") {
1767            return apply_secure_delete(state, stmt);
1768        }
1769        if name.eq_ignore_ascii_case("threads") {
1770            return apply_threads(state, stmt);
1771        }
1772        if is_fsqlite_mvcc_max_chain_length(&stmt.name) {
1773            return apply_mvcc_max_chain_length(state, stmt);
1774        }
1775        if is_fsqlite_mvcc_writer_lease_secs(&stmt.name) {
1776            return apply_mvcc_writer_lease_secs(state, stmt);
1777        }
1778        Ok(PragmaOutput::Unsupported)
1779    }
1780
1781    fn apply_serializable_connection(
1782        state: &mut ConnectionPragmaState,
1783        stmt: &PragmaStatement,
1784    ) -> Result<PragmaOutput> {
1785        match &stmt.value {
1786            None => Ok(PragmaOutput::Bool(state.serializable)),
1787            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1788                let enabled = parse_bool(expr)?;
1789                state.serializable = enabled;
1790                Ok(PragmaOutput::Bool(enabled))
1791            }
1792        }
1793    }
1794
1795    /// Generic boolean-toggle PRAGMA: a bare query echoes the current value and an
1796    /// assignment stores and echoes the new value, matching C SQLite's integer
1797    /// 0/1 readback. Used for the set/readback-only pragmas trusted_schema,
1798    /// read_uncommitted, cell_size_check, checkpoint_fullfsync, and
1799    /// automatic_index. (GH #282, #278, #262, #281, #283)
1800    fn apply_bool_toggle(flag: &mut bool, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1801        match &stmt.value {
1802            None => Ok(PragmaOutput::Bool(*flag)),
1803            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1804                let enabled = parse_bool(expr)?;
1805                *flag = enabled;
1806                Ok(PragmaOutput::Bool(enabled))
1807            }
1808        }
1809    }
1810
1811    /// `PRAGMA locking_mode [= NORMAL|EXCLUSIVE]`. A bare query echoes the current
1812    /// mode; an assignment accepts NORMAL/EXCLUSIVE case-insensitively and echoes
1813    /// the lowercased mode. Any other value is ignored and the current mode is
1814    /// echoed unchanged, matching C SQLite. (GH #273)
1815    fn apply_locking_mode(
1816        state: &mut ConnectionPragmaState,
1817        stmt: &PragmaStatement,
1818    ) -> Result<PragmaOutput> {
1819        match &stmt.value {
1820            None => Ok(PragmaOutput::Text(state.locking_mode.clone())),
1821            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1822                let requested = parse_text_expr(expr)?.to_ascii_lowercase();
1823                if requested == "normal" || requested == "exclusive" {
1824                    state.locking_mode = requested;
1825                }
1826                Ok(PragmaOutput::Text(state.locking_mode.clone()))
1827            }
1828        }
1829    }
1830
1831    /// `PRAGMA secure_delete [= OFF|ON|FAST|0|1|2]`. Tri-state: 0 = OFF, 1 = ON,
1832    /// 2 = FAST. C SQLite reports the integer on readback and echoes the new value
1833    /// on assignment. This is the set/readback surface only; the actual
1834    /// zero-on-delete storage semantics are tracked separately. (GH #277)
1835    fn apply_secure_delete(
1836        state: &mut ConnectionPragmaState,
1837        stmt: &PragmaStatement,
1838    ) -> Result<PragmaOutput> {
1839        match &stmt.value {
1840            None => Ok(PragmaOutput::Int(state.secure_delete)),
1841            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1842                let value = parse_secure_delete_value(expr)?;
1843                state.secure_delete = value;
1844                Ok(PragmaOutput::Int(value))
1845            }
1846        }
1847    }
1848
1849    fn parse_secure_delete_value(expr: &Expr) -> Result<i64> {
1850        if let Expr::Literal(Literal::Integer(n), _) = expr {
1851            return match *n {
1852                0..=2 => Ok(*n),
1853                _ => Err(FrankenError::OutOfRange {
1854                    what: "secure_delete".to_owned(),
1855                    value: n.to_string(),
1856                }),
1857            };
1858        }
1859        if let Ok(text) = parse_text_expr(expr)
1860            && text.eq_ignore_ascii_case("fast")
1861        {
1862            return Ok(2);
1863        }
1864        // OFF/ON/TRUE/FALSE map to 0/1.
1865        Ok(i64::from(parse_bool(expr)?))
1866    }
1867
1868    /// `PRAGMA threads [= N]`. A bare query reports the current limit; an
1869    /// assignment with a non-negative N clamps to the stock maximum (8), stores
1870    /// it, and echoes the effective value. A negative argument leaves the limit
1871    /// unchanged and just reports it, matching C SQLite. (GH #279)
1872    fn apply_threads(
1873        state: &mut ConnectionPragmaState,
1874        stmt: &PragmaStatement,
1875    ) -> Result<PragmaOutput> {
1876        /// Stock `SQLITE_MAX_WORKER_THREADS`.
1877        const MAX_WORKER_THREADS: i64 = 8;
1878        match &stmt.value {
1879            None => Ok(PragmaOutput::Int(state.threads)),
1880            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1881                let n = parse_integer_expr(expr)?;
1882                if n >= 0 {
1883                    state.threads = n.min(MAX_WORKER_THREADS);
1884                }
1885                Ok(PragmaOutput::Int(state.threads))
1886            }
1887        }
1888    }
1889
1890    /// `PRAGMA case_sensitive_like = ON|OFF`. SQLite treats this as write-only,
1891    /// but mirroring the other boolean toggles we also echo the current value on
1892    /// the no-argument query form. When ON, LIKE becomes byte-exact; the actual
1893    /// matching behavior is honored by the LIKE evaluation paths that read this
1894    /// flag (via the connection's pragma state).
1895    fn apply_case_sensitive_like(
1896        state: &mut ConnectionPragmaState,
1897        stmt: &PragmaStatement,
1898    ) -> Result<PragmaOutput> {
1899        match &stmt.value {
1900            None => Ok(PragmaOutput::Bool(state.case_sensitive_like)),
1901            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1902                let enabled = parse_bool(expr)?;
1903                state.case_sensitive_like = enabled;
1904                Ok(PragmaOutput::Bool(enabled))
1905            }
1906        }
1907    }
1908
1909    fn apply_raptorq_repair_symbols_connection(
1910        state: &mut ConnectionPragmaState,
1911        stmt: &PragmaStatement,
1912    ) -> Result<PragmaOutput> {
1913        match &stmt.value {
1914            None => Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols))),
1915            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1916                let value = parse_integer_expr(expr)?;
1917                if !(0..=i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS)).contains(&value) {
1918                    return Err(FrankenError::OutOfRange {
1919                        what: "raptorq_repair_symbols".to_owned(),
1920                        value: value.to_string(),
1921                    });
1922                }
1923                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1924                {
1925                    state.raptorq_repair_symbols = value as u8;
1926                }
1927                Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols)))
1928            }
1929        }
1930    }
1931
1932    fn apply_differential_views_connection(
1933        state: &mut ConnectionPragmaState,
1934        stmt: &PragmaStatement,
1935    ) -> Result<PragmaOutput> {
1936        match &stmt.value {
1937            None => Ok(PragmaOutput::Bool(state.differential_views.is_enabled())),
1938            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1939                let enabled = parse_bool(expr)?;
1940                state.differential_views = DifferentialViewsSetting::from_enabled(enabled);
1941                Ok(PragmaOutput::Bool(enabled))
1942            }
1943        }
1944    }
1945
1946    fn apply_journal_mode(
1947        state: &mut ConnectionPragmaState,
1948        stmt: &PragmaStatement,
1949    ) -> Result<PragmaOutput> {
1950        match &stmt.value {
1951            None => Ok(PragmaOutput::Text(state.journal_mode.clone())),
1952            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1953                let mode = parse_text_expr(expr)?;
1954                let lower = mode.to_ascii_lowercase();
1955                match lower.as_str() {
1956                    "delete" | "truncate" | "persist" | "memory" | "wal" | "off" => {
1957                        state.journal_mode.clone_from(&lower);
1958                        Ok(PragmaOutput::Text(lower))
1959                    }
1960                    _ => Err(FrankenError::TypeMismatch {
1961                        expected: "delete|truncate|persist|memory|wal|off".to_owned(),
1962                        actual: mode,
1963                    }),
1964                }
1965            }
1966        }
1967    }
1968
1969    fn apply_synchronous(
1970        state: &mut ConnectionPragmaState,
1971        stmt: &PragmaStatement,
1972    ) -> Result<PragmaOutput> {
1973        match &stmt.value {
1974            None => Ok(PragmaOutput::Text(state.synchronous.clone())),
1975            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1976                let val = parse_synchronous_value(expr)?;
1977                state.synchronous.clone_from(&val);
1978                Ok(PragmaOutput::Text(val))
1979            }
1980        }
1981    }
1982
1983    fn parse_synchronous_value(expr: &Expr) -> Result<String> {
1984        // Accept both text names and integer codes (0=OFF, 1=NORMAL, 2=FULL, 3=EXTRA).
1985        if let Expr::Literal(Literal::Integer(n), _) = expr {
1986            match n {
1987                0 => Ok("OFF".to_owned()),
1988                1 => Ok("NORMAL".to_owned()),
1989                2 => Ok("FULL".to_owned()),
1990                3 => Ok("EXTRA".to_owned()),
1991                _ => Err(FrankenError::OutOfRange {
1992                    what: "synchronous".to_owned(),
1993                    value: n.to_string(),
1994                }),
1995            }
1996        } else {
1997            let text = parse_text_expr(expr)?;
1998            let upper = text.to_ascii_uppercase();
1999            match upper.as_str() {
2000                "OFF" | "NORMAL" | "FULL" | "EXTRA" => Ok(upper),
2001                _ => Err(FrankenError::TypeMismatch {
2002                    expected: "OFF|NORMAL|FULL|EXTRA|0|1|2|3".to_owned(),
2003                    actual: text,
2004                }),
2005            }
2006        }
2007    }
2008
2009    fn apply_cache_size(
2010        state: &mut ConnectionPragmaState,
2011        stmt: &PragmaStatement,
2012    ) -> Result<PragmaOutput> {
2013        match &stmt.value {
2014            None => Ok(PragmaOutput::Int(state.cache_size)),
2015            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2016                let val = parse_integer_expr(expr)?;
2017                state.cache_size = val;
2018                Ok(PragmaOutput::Int(val))
2019            }
2020        }
2021    }
2022
2023    fn apply_page_size(
2024        state: &mut ConnectionPragmaState,
2025        stmt: &PragmaStatement,
2026    ) -> Result<PragmaOutput> {
2027        match &stmt.value {
2028            None => Ok(PragmaOutput::Int(i64::from(state.page_size))),
2029            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2030                let val = parse_integer_expr(expr)?;
2031                if !(512..=65536).contains(&val) || !is_power_of_two(val) {
2032                    return Err(FrankenError::OutOfRange {
2033                        what: "page_size".to_owned(),
2034                        value: val.to_string(),
2035                    });
2036                }
2037                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2038                {
2039                    state.page_size = val as u32;
2040                }
2041                Ok(PragmaOutput::Int(val))
2042            }
2043        }
2044    }
2045
2046    fn apply_busy_timeout(
2047        state: &mut ConnectionPragmaState,
2048        stmt: &PragmaStatement,
2049    ) -> Result<PragmaOutput> {
2050        match &stmt.value {
2051            None => Ok(PragmaOutput::Int(state.busy_timeout_ms)),
2052            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2053                let val = parse_integer_expr(expr)?;
2054                state.busy_timeout_ms = val.max(0);
2055                Ok(PragmaOutput::Int(state.busy_timeout_ms))
2056            }
2057        }
2058    }
2059
2060    fn apply_temp_store(
2061        state: &mut ConnectionPragmaState,
2062        stmt: &PragmaStatement,
2063    ) -> Result<PragmaOutput> {
2064        match &stmt.value {
2065            None => Ok(PragmaOutput::Int(state.temp_store)),
2066            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2067                let val = parse_temp_store_value(expr)?;
2068                state.temp_store = val;
2069                Ok(PragmaOutput::Int(val))
2070            }
2071        }
2072    }
2073
2074    fn apply_mmap_size(
2075        state: &mut ConnectionPragmaState,
2076        stmt: &PragmaStatement,
2077    ) -> Result<PragmaOutput> {
2078        match &stmt.value {
2079            None => Ok(PragmaOutput::Int(state.mmap_size)),
2080            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2081                let val = parse_integer_expr(expr)?;
2082                state.mmap_size = val.max(0);
2083                Ok(PragmaOutput::Int(state.mmap_size))
2084            }
2085        }
2086    }
2087
2088    fn apply_auto_vacuum(
2089        state: &mut ConnectionPragmaState,
2090        stmt: &PragmaStatement,
2091    ) -> Result<PragmaOutput> {
2092        match &stmt.value {
2093            None => Ok(PragmaOutput::Int(state.auto_vacuum)),
2094            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2095                let val = parse_auto_vacuum_value(expr)?;
2096                state.auto_vacuum = val;
2097                Ok(PragmaOutput::Int(val))
2098            }
2099        }
2100    }
2101
2102    fn apply_wal_autocheckpoint(
2103        state: &mut ConnectionPragmaState,
2104        stmt: &PragmaStatement,
2105    ) -> Result<PragmaOutput> {
2106        match &stmt.value {
2107            None => Ok(PragmaOutput::Int(state.wal_autocheckpoint)),
2108            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2109                let val = parse_integer_expr(expr)?;
2110                state.wal_autocheckpoint = val.max(0);
2111                Ok(PragmaOutput::Int(state.wal_autocheckpoint))
2112            }
2113        }
2114    }
2115
2116    fn apply_user_version(
2117        state: &mut ConnectionPragmaState,
2118        stmt: &PragmaStatement,
2119    ) -> Result<PragmaOutput> {
2120        match &stmt.value {
2121            None => Ok(PragmaOutput::Int(state.user_version)),
2122            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2123                let val = parse_integer_expr(expr)?;
2124                state.user_version = val;
2125                Ok(PragmaOutput::Int(val))
2126            }
2127        }
2128    }
2129
2130    fn apply_application_id(
2131        state: &mut ConnectionPragmaState,
2132        stmt: &PragmaStatement,
2133    ) -> Result<PragmaOutput> {
2134        match &stmt.value {
2135            None => Ok(PragmaOutput::Int(state.application_id)),
2136            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2137                let val = parse_integer_expr(expr)?;
2138                state.application_id = val;
2139                Ok(PragmaOutput::Int(val))
2140            }
2141        }
2142    }
2143
2144    /// `PRAGMA default_cache_size [= N]` — the persistent, header-backed cache
2145    /// size (header bytes 48..52). Stock stores `abs(N)` as a page count (legacy
2146    /// `sqlite3AbsInt32`); `0` means "unset", for which a bare query reports the
2147    /// compiled default `-2000`. Assigning also updates the connection's runtime
2148    /// `cache_size`, matching stock. The header write itself is performed by the
2149    /// caller (`Connection::update_database_header_metadata`), which reads the
2150    /// value stashed here.
2151    fn apply_default_cache_size(
2152        state: &mut ConnectionPragmaState,
2153        stmt: &PragmaStatement,
2154    ) -> Result<PragmaOutput> {
2155        // Header 0 ("unset") is reported to the caller as the compiled default.
2156        let report = |stored: i64| if stored == 0 { -2000 } else { stored };
2157        match &stmt.value {
2158            None => Ok(PragmaOutput::Int(report(state.default_cache_size))),
2159            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2160                let stored = parse_integer_expr(expr)?.saturating_abs();
2161                state.default_cache_size = stored;
2162                state.cache_size = stored;
2163                Ok(PragmaOutput::Int(report(stored)))
2164            }
2165        }
2166    }
2167
2168    fn apply_foreign_keys(
2169        state: &mut ConnectionPragmaState,
2170        stmt: &PragmaStatement,
2171    ) -> Result<PragmaOutput> {
2172        match &stmt.value {
2173            None => Ok(PragmaOutput::Int(i64::from(state.foreign_keys))),
2174            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2175                let enabled = parse_bool(expr)?;
2176                state.foreign_keys = enabled;
2177                Ok(PragmaOutput::Int(i64::from(enabled)))
2178            }
2179        }
2180    }
2181
2182    fn apply_recursive_triggers(
2183        state: &mut ConnectionPragmaState,
2184        stmt: &PragmaStatement,
2185    ) -> Result<PragmaOutput> {
2186        match &stmt.value {
2187            None => Ok(PragmaOutput::Int(i64::from(state.recursive_triggers))),
2188            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2189                let enabled = parse_bool(expr)?;
2190                state.recursive_triggers = enabled;
2191                Ok(PragmaOutput::Int(i64::from(enabled)))
2192            }
2193        }
2194    }
2195
2196    fn apply_query_only(
2197        state: &mut ConnectionPragmaState,
2198        stmt: &PragmaStatement,
2199    ) -> Result<PragmaOutput> {
2200        match &stmt.value {
2201            None => Ok(PragmaOutput::Int(i64::from(state.query_only))),
2202            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2203                let enabled = parse_bool(expr)?;
2204                state.query_only = enabled;
2205                Ok(PragmaOutput::Int(i64::from(enabled)))
2206            }
2207        }
2208    }
2209
2210    fn apply_writable_schema(
2211        state: &mut ConnectionPragmaState,
2212        stmt: &PragmaStatement,
2213    ) -> Result<PragmaOutput> {
2214        match &stmt.value {
2215            None => Ok(PragmaOutput::Int(i64::from(state.writable_schema))),
2216            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2217                // SQLite accepts `PRAGMA writable_schema = RESET`: it clears the
2218                // toggle and reparses the in-memory schema from sqlite_schema.
2219                // We honor the toggle-off; there is no separately cached parsed
2220                // schema to discard at this layer, so RESET is observably
2221                // equivalent to OFF. Without this, RESET errored
2222                // "expected ON|OFF|TRUE|FALSE|1|0, got RESET" and aborted the
2223                // rest of the script (GH #284).
2224                if expr_is_reset(expr) {
2225                    state.writable_schema = false;
2226                    return Ok(PragmaOutput::Int(0));
2227                }
2228                let enabled = parse_bool(expr)?;
2229                state.writable_schema = enabled;
2230                Ok(PragmaOutput::Int(i64::from(enabled)))
2231            }
2232        }
2233    }
2234
2235    /// Whether a `PRAGMA writable_schema = <expr>` value is the `RESET` keyword
2236    /// (a bareword identifier or a quoted string), case-insensitive.
2237    fn expr_is_reset(expr: &Expr) -> bool {
2238        match expr {
2239            Expr::Column(col, _) => col.column.eq_ignore_ascii_case("reset"),
2240            Expr::Literal(Literal::String(s), _) => s.eq_ignore_ascii_case("reset"),
2241            _ => false,
2242        }
2243    }
2244
2245    fn parse_temp_store_value(expr: &Expr) -> Result<i64> {
2246        if let Expr::Literal(Literal::Integer(n), _) = expr {
2247            return match *n {
2248                0..=2 => Ok(*n),
2249                _ => Err(FrankenError::OutOfRange {
2250                    what: "temp_store".to_owned(),
2251                    value: n.to_string(),
2252                }),
2253            };
2254        }
2255
2256        let text = parse_text_expr(expr)?;
2257        match text.to_ascii_lowercase().as_str() {
2258            "default" => Ok(0),
2259            "file" => Ok(1),
2260            "memory" => Ok(2),
2261            _ => Err(FrankenError::TypeMismatch {
2262                expected: "DEFAULT|FILE|MEMORY|0|1|2".to_owned(),
2263                actual: text,
2264            }),
2265        }
2266    }
2267
2268    fn parse_auto_vacuum_value(expr: &Expr) -> Result<i64> {
2269        if let Expr::Literal(Literal::Integer(n), _) = expr {
2270            return match *n {
2271                0..=2 => Ok(*n),
2272                _ => Err(FrankenError::OutOfRange {
2273                    what: "auto_vacuum".to_owned(),
2274                    value: n.to_string(),
2275                }),
2276            };
2277        }
2278
2279        let text = parse_text_expr(expr)?;
2280        match text.to_ascii_lowercase().as_str() {
2281            "none" => Ok(0),
2282            "full" => Ok(1),
2283            "incremental" => Ok(2),
2284            _ => Err(FrankenError::TypeMismatch {
2285                expected: "NONE|FULL|INCREMENTAL|0|1|2".to_owned(),
2286                actual: text,
2287            }),
2288        }
2289    }
2290
2291    fn is_power_of_two(n: i64) -> bool {
2292        n > 0 && (n & (n - 1)) == 0
2293    }
2294
2295    /// Extract a text value from a PRAGMA assignment expression.
2296    fn parse_text_expr(expr: &Expr) -> Result<String> {
2297        match expr {
2298            Expr::Literal(Literal::String(s), _) => Ok(s.clone()),
2299            Expr::Column(col, _) => Ok(col.column.to_string()),
2300            Expr::Literal(Literal::Integer(n), _) => Ok(n.to_string()),
2301            other => Err(FrankenError::TypeMismatch {
2302                expected: "text or identifier".to_owned(),
2303                actual: format!("{other:?}"),
2304            }),
2305        }
2306    }
2307
2308    fn apply_serializable(
2309        mgr: &mut TransactionManager,
2310        stmt: &PragmaStatement,
2311    ) -> Result<PragmaOutput> {
2312        match &stmt.value {
2313            None => Ok(PragmaOutput::Bool(mgr.ssi_enabled())),
2314            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2315                let enabled = parse_bool(expr)?;
2316                mgr.set_ssi_enabled(enabled);
2317                Ok(PragmaOutput::Bool(mgr.ssi_enabled()))
2318            }
2319        }
2320    }
2321
2322    fn is_fsqlite_serializable(name: &QualifiedName) -> bool {
2323        name.schema
2324            .as_deref()
2325            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2326            && name.name.eq_ignore_ascii_case("serializable")
2327    }
2328
2329    fn is_fsqlite_differential_views(name: &QualifiedName) -> bool {
2330        match name.schema.as_deref() {
2331            Some(schema) => {
2332                schema.eq_ignore_ascii_case("fsqlite")
2333                    && name.name.eq_ignore_ascii_case("differential_views")
2334            }
2335            None => name.name.eq_ignore_ascii_case("fsqlite_differential_views"),
2336        }
2337    }
2338
2339    fn is_raptorq_repair_symbols(name: &QualifiedName) -> bool {
2340        let schema_ok = match name.schema.as_deref() {
2341            None => true,
2342            Some(schema) => schema.eq_ignore_ascii_case("fsqlite"),
2343        };
2344        schema_ok && name.name.eq_ignore_ascii_case("raptorq_repair_symbols")
2345    }
2346
2347    fn is_fsqlite_mvcc_max_chain_length(name: &QualifiedName) -> bool {
2348        name.schema
2349            .as_deref()
2350            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2351            && name.name.eq_ignore_ascii_case("mvcc_max_chain_length")
2352    }
2353
2354    fn is_fsqlite_mvcc_writer_lease_secs(name: &QualifiedName) -> bool {
2355        name.schema
2356            .as_deref()
2357            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2358            && name.name.eq_ignore_ascii_case("mvcc_writer_lease_secs")
2359    }
2360
2361    fn apply_mvcc_max_chain_length(
2362        state: &mut ConnectionPragmaState,
2363        stmt: &PragmaStatement,
2364    ) -> Result<PragmaOutput> {
2365        match &stmt.value {
2366            None => Ok(PragmaOutput::Int(state.mvcc_max_chain_length as i64)),
2367            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2368                let value = parse_integer_expr(expr)?;
2369                if value < 2 {
2370                    return Err(FrankenError::OutOfRange {
2371                        what: "fsqlite.mvcc_max_chain_length".into(),
2372                        value: format!("{value} (minimum 2)"),
2373                    });
2374                }
2375                #[allow(clippy::cast_sign_loss)]
2376                {
2377                    state.mvcc_max_chain_length = value as usize;
2378                }
2379                // Note: value is stored in pragma_state and will be read by
2380                // the MVCC layer when creating concurrent execution contexts.
2381                // The MvccCoordinator's own max_chain_length is set at
2382                // construction; this PRAGMA value takes effect for new
2383                // concurrent transactions opened on this connection.
2384                Ok(PragmaOutput::Int(value))
2385            }
2386        }
2387    }
2388
2389    fn apply_mvcc_writer_lease_secs(
2390        state: &mut ConnectionPragmaState,
2391        stmt: &PragmaStatement,
2392    ) -> Result<PragmaOutput> {
2393        match &stmt.value {
2394            None => Ok(PragmaOutput::Int(state.mvcc_writer_lease_secs as i64)),
2395            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2396                let value = parse_integer_expr(expr)?;
2397                if value < 1 {
2398                    return Err(FrankenError::OutOfRange {
2399                        what: "fsqlite.mvcc_writer_lease_secs".into(),
2400                        value: format!("{value} (minimum 1)"),
2401                    });
2402                }
2403                #[allow(clippy::cast_sign_loss)]
2404                {
2405                    state.mvcc_writer_lease_secs = value as u64;
2406                }
2407                // Note: same propagation model as mvcc_max_chain_length above.
2408                Ok(PragmaOutput::Int(value))
2409            }
2410        }
2411    }
2412
2413    fn apply_raptorq_repair_symbols(
2414        mgr: &mut TransactionManager,
2415        stmt: &PragmaStatement,
2416        wal_fec_sidecar_path: Option<&Path>,
2417    ) -> Result<PragmaOutput> {
2418        match &stmt.value {
2419            None => {
2420                if let Some(sidecar) = wal_fec_sidecar_path {
2421                    let persisted = read_wal_fec_raptorq_repair_symbols(sidecar)?;
2422                    mgr.set_raptorq_repair_symbols(persisted);
2423                    debug!(
2424                        sidecar = %sidecar.display(),
2425                        raptorq_repair_symbols = persisted,
2426                        "loaded raptorq_repair_symbols from wal-fec sidecar"
2427                    );
2428                }
2429                Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2430            }
2431            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2432                let requested = parse_raptorq_repair_symbols(expr)?;
2433                mgr.set_raptorq_repair_symbols(requested);
2434
2435                if let Some(sidecar) = wal_fec_sidecar_path {
2436                    persist_wal_fec_raptorq_repair_symbols(sidecar, requested)?;
2437                    info!(
2438                        sidecar = %sidecar.display(),
2439                        raptorq_repair_symbols = requested,
2440                        "persisted raptorq_repair_symbols to wal-fec sidecar"
2441                    );
2442                }
2443
2444                Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2445            }
2446        }
2447    }
2448
2449    fn parse_raptorq_repair_symbols(expr: &Expr) -> Result<u8> {
2450        let raw = parse_integer_expr(expr)?;
2451        if raw < 0 {
2452            warn!(
2453                value = raw,
2454                "rejecting negative raptorq_repair_symbols value"
2455            );
2456            return Err(FrankenError::OutOfRange {
2457                what: "raptorq_repair_symbols".to_owned(),
2458                value: raw.to_string(),
2459            });
2460        }
2461
2462        let max = i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS);
2463        if raw > max {
2464            warn!(
2465                value = raw,
2466                max = MAX_RAPTORQ_REPAIR_SYMBOLS,
2467                "rejecting out-of-range raptorq_repair_symbols value"
2468            );
2469            return Err(FrankenError::OutOfRange {
2470                what: "raptorq_repair_symbols".to_owned(),
2471                value: raw.to_string(),
2472            });
2473        }
2474
2475        u8::try_from(raw).map_err(|_| {
2476            error!(
2477                value = raw,
2478                "failed to convert validated raptorq_repair_symbols to u8"
2479            );
2480            FrankenError::OutOfRange {
2481                what: "raptorq_repair_symbols".to_owned(),
2482                value: raw.to_string(),
2483            }
2484        })
2485    }
2486
2487    fn parse_integer_expr(expr: &Expr) -> Result<i64> {
2488        match expr {
2489            Expr::Literal(Literal::Integer(n), _) => Ok(*n),
2490            Expr::UnaryOp {
2491                op: UnaryOp::Negate,
2492                expr,
2493                ..
2494            } => Ok(-parse_integer_expr(expr)?),
2495            Expr::UnaryOp {
2496                op: UnaryOp::Plus,
2497                expr,
2498                ..
2499            } => parse_integer_expr(expr),
2500            Expr::Column(col, _) => {
2501                col.column
2502                    .parse::<i64>()
2503                    .map_err(|_| FrankenError::TypeMismatch {
2504                        expected: "integer (0..255)".to_owned(),
2505                        actual: col.column.to_string(),
2506                    })
2507            }
2508            other => Err(FrankenError::TypeMismatch {
2509                expected: "integer (0..255)".to_owned(),
2510                actual: format!("{other:?}"),
2511            }),
2512        }
2513    }
2514
2515    fn parse_bool(expr: &Expr) -> Result<bool> {
2516        let (raw, parsed) = match expr {
2517            Expr::Literal(Literal::Integer(n), _) => (format!("{n}"), parse_int_bool(*n)),
2518            Expr::Literal(Literal::String(s), _) => (s.clone(), parse_str_bool(s)),
2519            Expr::Literal(Literal::True, _) => ("TRUE".to_owned(), Some(true)),
2520            Expr::Literal(Literal::False, _) => ("FALSE".to_owned(), Some(false)),
2521            Expr::Column(col, _) => (col.column.to_string(), parse_str_bool(&col.column)),
2522            other => {
2523                return Err(FrankenError::TypeMismatch {
2524                    expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2525                    actual: format!("{other:?}"),
2526                });
2527            }
2528        };
2529
2530        parsed.ok_or_else(|| FrankenError::TypeMismatch {
2531            expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2532            actual: raw,
2533        })
2534    }
2535
2536    fn parse_int_bool(n: i64) -> Option<bool> {
2537        match n {
2538            0 => Some(false),
2539            1 => Some(true),
2540            _ => None,
2541        }
2542    }
2543
2544    fn parse_str_bool(s: &str) -> Option<bool> {
2545        if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("true") {
2546            Some(true)
2547        } else if s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("false") {
2548            Some(false)
2549        } else if s == "1" {
2550            Some(true)
2551        } else if s == "0" {
2552            Some(false)
2553        } else {
2554            None
2555        }
2556    }
2557}
2558
2559// ── Tests ───────────────────────────────────────────────────────────────────
2560
2561#[cfg(test)]
2562mod tests {
2563    use super::*;
2564
2565    fn test_failure() -> bool {
2566        false
2567    }
2568
2569    // ── test_vdbe_op_struct_size ─────────────────────────────────────────
2570    #[test]
2571    fn test_vdbe_op_struct_size() {
2572        // Verify VdbeOp fields are accessible and correctly typed.
2573        let op = VdbeOp {
2574            opcode: Opcode::Integer,
2575            p1: 42,
2576            p2: 1,
2577            p3: 0,
2578            p4: P4::None,
2579            p5: 0,
2580        };
2581        assert_eq!(op.opcode, Opcode::Integer);
2582        assert_eq!(op.p1, 42_i32);
2583        assert_eq!(op.p2, 1_i32);
2584        assert_eq!(op.p3, 0_i32);
2585        assert_eq!(op.p4, P4::None);
2586        assert_eq!(op.p5, 0_u16);
2587    }
2588
2589    #[test]
2590    fn test_schema_evaluation_context_encoding_and_nested_restoration() {
2591        const CONSTANT_ARGUMENT_MASK: i32 = 0x15;
2592
2593        let mut builder = ProgramBuilder::new();
2594        let output = builder.alloc_reg();
2595        builder.with_schema_evaluation_context(SchemaEvaluationContext::Index, |builder| {
2596            builder.emit_op(
2597                Opcode::PureFunc,
2598                CONSTANT_ARGUMENT_MASK,
2599                0,
2600                output,
2601                P4::FuncName("INDEX_FN".to_owned()),
2602                0,
2603            );
2604            builder.with_schema_evaluation_context(
2605                SchemaEvaluationContext::CheckConstraint,
2606                |builder| {
2607                    builder.emit_op(
2608                        Opcode::Function,
2609                        CONSTANT_ARGUMENT_MASK,
2610                        0,
2611                        output,
2612                        P4::FuncName("CHECK_FN".to_owned()),
2613                        0,
2614                    );
2615                },
2616            );
2617            builder.emit_op(
2618                Opcode::PureFunc,
2619                CONSTANT_ARGUMENT_MASK,
2620                0,
2621                output,
2622                P4::FuncName("INDEX_AGAIN_FN".to_owned()),
2623                0,
2624            );
2625        });
2626        builder.with_schema_evaluation_context(
2627            SchemaEvaluationContext::GeneratedColumn,
2628            |builder| {
2629                builder.emit_op(
2630                    Opcode::PureFunc,
2631                    CONSTANT_ARGUMENT_MASK,
2632                    0,
2633                    output,
2634                    P4::FuncName("GENERATED_FN".to_owned()),
2635                    0,
2636                );
2637            },
2638        );
2639        builder.emit_op(
2640            Opcode::PureFunc,
2641            CONSTANT_ARGUMENT_MASK,
2642            0,
2643            output,
2644            P4::FuncName("ORDINARY_FN".to_owned()),
2645            0,
2646        );
2647        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2648
2649        let program = builder
2650            .finish()
2651            .expect("schema-context program should build");
2652        let function_ops: Vec<_> = program
2653            .ops()
2654            .iter()
2655            .filter(|op| matches!(op.opcode, Opcode::Function | Opcode::PureFunc))
2656            .collect();
2657        let contexts: Vec<_> = function_ops
2658            .iter()
2659            .map(|op| SchemaEvaluationContext::from_function_p1(op.p1))
2660            .collect();
2661
2662        assert_eq!(
2663            contexts,
2664            [
2665                Some(SchemaEvaluationContext::Index),
2666                Some(SchemaEvaluationContext::CheckConstraint),
2667                Some(SchemaEvaluationContext::Index),
2668                Some(SchemaEvaluationContext::GeneratedColumn),
2669                None,
2670            ]
2671        );
2672        assert!(
2673            function_ops
2674                .iter()
2675                .all(|op| { op.p1 & !FUNCTION_SCHEMA_CONTEXT_MASK == CONSTANT_ARGUMENT_MASK })
2676        );
2677    }
2678
2679    // ── test_p4_variant_all_types ───────────────────────────────────────
2680    #[test]
2681    fn test_p4_variant_all_types() {
2682        // Each P4 variant can be constructed and pattern-matched.
2683        let variants: Vec<P4> = vec![
2684            P4::None,
2685            P4::Int(42),
2686            P4::Int64(i64::MAX),
2687            P4::Real(1.234_567_89),
2688            P4::Str("hello".to_owned()),
2689            P4::Blob(vec![0xDE, 0xAD]),
2690            P4::Collation("BINARY".to_owned()),
2691            P4::FuncName("count".to_owned()),
2692            P4::Table("users".to_owned()),
2693            P4::Affinity("ddd".to_owned()),
2694            P4::PrecomputedHeader(fsqlite_types::record::PrecomputedRecordHeader::new(&[
2695                fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
2696                fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
2697            ])),
2698        ];
2699        assert_eq!(variants.len(), 11);
2700
2701        // Verify each variant matches itself.
2702        assert!(matches!(variants[0], P4::None));
2703        assert!(matches!(variants[1], P4::Int(42)));
2704        assert!(matches!(variants[2], P4::Int64(i64::MAX)));
2705        assert!(matches!(variants[3], P4::Real(_)));
2706        assert!(matches!(variants[4], P4::Str(_)));
2707        assert!(matches!(variants[5], P4::Blob(_)));
2708        assert!(matches!(variants[6], P4::Collation(_)));
2709        assert!(matches!(variants[7], P4::FuncName(ref s) if s == "count"));
2710        assert!(matches!(variants[8], P4::Table(ref s) if s == "users"));
2711        assert!(matches!(variants[9], P4::Affinity(ref s) if s == "ddd"));
2712        assert!(matches!(
2713            variants[10],
2714            P4::PrecomputedHeader(ref header) if header.template == vec![3, 0, 0]
2715        ));
2716    }
2717
2718    // ── test_label_emit_and_resolve ─────────────────────────────────────
2719    #[test]
2720    fn test_label_emit_and_resolve() {
2721        let mut b = ProgramBuilder::new();
2722
2723        // Emit two distinct labels.
2724        let label_a = b.emit_label();
2725        let label_b = b.emit_label();
2726        assert_ne!(label_a, label_b);
2727
2728        // Emit a jump to label_a (forward reference).
2729        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label_a, P4::None, 0);
2730        assert_eq!(b.op_at(jump_addr).unwrap().p2, -1); // unresolved placeholder
2731
2732        // Emit some instructions.
2733        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
2734        b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
2735
2736        // Resolve label_a to the current address (2 instructions after the jump).
2737        b.resolve_label(label_a);
2738
2739        // The jump's p2 should now be patched to address 3.
2740        assert_eq!(b.op_at(jump_addr).unwrap().p2, 3);
2741
2742        // Emit another jump to label_b.
2743        let jump2 = b.emit_jump_to_label(Opcode::If, 1, 0, label_b, P4::None, 0);
2744        b.resolve_label(label_b);
2745        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2746        assert_eq!(b.op_at(jump2).unwrap().p2, 4);
2747
2748        // Finish should succeed (all labels resolved).
2749        let prog = b.finish().unwrap();
2750        assert_eq!(prog.len(), 5);
2751    }
2752
2753    // ── test_unresolved_label_error ─────────────────────────────────────
2754    #[test]
2755    fn test_unresolved_label_error() {
2756        let mut b = ProgramBuilder::new();
2757        let label = b.emit_label();
2758        b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
2759
2760        // Don't resolve the label — finish should fail.
2761        let result = b.finish();
2762        assert!(result.is_err());
2763    }
2764
2765    // ── test_register_alloc_sequential ──────────────────────────────────
2766    #[test]
2767    fn test_register_alloc_sequential() {
2768        let mut alloc = RegisterAllocator::new();
2769
2770        // Sequential single allocations start at 1.
2771        assert_eq!(alloc.alloc_reg(), 1);
2772        assert_eq!(alloc.alloc_reg(), 2);
2773        assert_eq!(alloc.alloc_reg(), 3);
2774
2775        // Block allocation returns first register of contiguous block.
2776        let block_start = alloc.alloc_regs(3);
2777        assert_eq!(block_start, 4);
2778        // Next single alloc continues after the block.
2779        assert_eq!(alloc.alloc_reg(), 7);
2780
2781        assert_eq!(alloc.count(), 7);
2782    }
2783
2784    // ── test_register_temp_pool_reuse ───────────────────────────────────
2785    #[test]
2786    fn test_register_temp_pool_reuse() {
2787        let mut alloc = RegisterAllocator::new();
2788
2789        let r1 = alloc.alloc_reg(); // 1
2790        let t1 = alloc.alloc_temp(); // 2 (new allocation)
2791        let t2 = alloc.alloc_temp(); // 3 (new allocation)
2792        assert_eq!(r1, 1);
2793        assert_eq!(t1, 2);
2794        assert_eq!(t2, 3);
2795
2796        // Return temps to pool.
2797        alloc.free_temp(t1);
2798        alloc.free_temp(t2);
2799
2800        // Next temp allocations reuse from pool (LIFO order).
2801        let t3 = alloc.alloc_temp();
2802        let t4 = alloc.alloc_temp();
2803        assert_eq!(t3, t2); // 3 (last freed)
2804        assert_eq!(t4, t1); // 2
2805
2806        // High water mark unchanged (no new registers needed).
2807        assert_eq!(alloc.count(), 3);
2808    }
2809
2810    // ── test_coroutine_init_yield_end ───────────────────────────────────
2811    #[test]
2812    fn test_coroutine_init_yield_end() {
2813        // InitCoroutine: set yield register to body PC.
2814        let yield_reg = 1;
2815        let body_pc = 10;
2816        let mut co = CoroutineState::new(yield_reg, body_pc);
2817        assert_eq!(co.yield_reg, yield_reg);
2818        assert_eq!(co.saved_pc, body_pc);
2819        assert!(!co.exhausted);
2820
2821        // Yield: bidirectional PC swap.
2822        // Caller is at PC=5, coroutine body is at PC=10.
2823        let resume = co.yield_swap(5);
2824        assert_eq!(resume, 10); // jump to body
2825        assert_eq!(co.saved_pc, 5); // caller's PC saved
2826
2827        // Body yields back: caller at 5, body at 15.
2828        let resume2 = co.yield_swap(15);
2829        assert_eq!(resume2, 5); // back to caller
2830        assert_eq!(co.saved_pc, 15);
2831
2832        // EndCoroutine: marks exhaustion, returns to caller.
2833        let final_pc = co.end();
2834        assert_eq!(final_pc, 15); // returns saved_pc
2835        assert!(co.exhausted);
2836    }
2837
2838    // ── test_coroutine_multi_row_production ─────────────────────────────
2839    #[test]
2840    fn test_coroutine_multi_row_production() {
2841        // Simulate a CTE body producing 5 rows via Yield loop.
2842        let mut co = CoroutineState::new(1, 10); // body starts at PC=10
2843        let mut rows_consumed = 0;
2844        let caller_start_pc = 5;
2845
2846        // Caller yields to body.
2847        let mut next_pc = co.yield_swap(caller_start_pc);
2848        assert_eq!(next_pc, 10); // first entry into body
2849
2850        // Body produces rows.
2851        for row in 1..=5 {
2852            // Body "produces" a row, then yields back to caller.
2853            let body_pc = 10 + row; // body advances its PC
2854            next_pc = co.yield_swap(body_pc);
2855            // Caller resumes at its saved PC.
2856            assert_eq!(next_pc, caller_start_pc);
2857            rows_consumed += 1;
2858
2859            if row < 5 {
2860                // Caller yields back to body to get next row.
2861                next_pc = co.yield_swap(caller_start_pc);
2862                assert_eq!(next_pc, body_pc); // resume body
2863            }
2864        }
2865
2866        assert_eq!(rows_consumed, 5);
2867
2868        // Body signals exhaustion.
2869        let final_pc = co.end();
2870        assert!(co.exhausted);
2871        assert!(final_pc > 0); // valid return PC
2872    }
2873
2874    #[test]
2875    fn test_program_builder_infers_register_count_from_manual_opcode_registers() {
2876        let mut builder = ProgramBuilder::new();
2877        builder.emit_op(Opcode::Integer, 11, 3, 0, P4::None, 0);
2878        builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
2879        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2880
2881        let program = builder.finish().expect("program should build");
2882        assert_eq!(
2883            program.register_count(),
2884            3,
2885            "bytecode that writes raw registers must still allocate a large enough register file",
2886        );
2887    }
2888
2889    #[test]
2890    fn test_program_builder_infers_register_count_for_non_contiguous_comparison_operands() {
2891        let mut builder = ProgramBuilder::new();
2892        builder.emit_op(Opcode::Eq, 2, 0, 7, P4::None, 0);
2893        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2894
2895        let program = builder.finish().expect("program should build");
2896        assert_eq!(
2897            program.register_count(),
2898            7,
2899            "comparison opcodes must account for both read registers even when they are not contiguous",
2900        );
2901    }
2902
2903    #[test]
2904    fn test_program_builder_infers_register_count_for_store_p2_comparisons() {
2905        let mut builder = ProgramBuilder::new();
2906        builder.emit_op(Opcode::Eq, 2, 9, 7, P4::None, 0x20);
2907        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2908
2909        let program = builder.finish().expect("program should build");
2910        assert_eq!(
2911            program.register_count(),
2912            9,
2913            "SQLITE_STOREP2 comparisons must reserve the destination register in the pre-sized register file",
2914        );
2915    }
2916
2917    #[test]
2918    fn test_program_builder_tracks_sorter_preflight_and_runtime_bound_registers() {
2919        let mut builder = ProgramBuilder::new();
2920        builder.emit_op(
2921            Opcode::SorterOpen,
2922            0,
2923            1,
2924            8,
2925            P4::None,
2926            SORTER_OPEN_TOP_N_REGISTER,
2927        );
2928        builder.emit_op(
2929            Opcode::SorterCompare,
2930            0,
2931            2,
2932            7,
2933            P4::None,
2934            fsqlite_types::opcode::SORTER_COMPARE_TOP_N_PREFLIGHT,
2935        );
2936        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2937
2938        let program = builder.finish().expect("program should build");
2939        assert_eq!(
2940            program.register_count(),
2941            8,
2942            "runtime SorterOpen.p3 and SorterCompare.p3 must contribute to register sizing"
2943        );
2944    }
2945
2946    #[test]
2947    fn test_sorter_compare_register_spans_distinguish_preflight_consumption() {
2948        let ordinary = VdbeOp {
2949            opcode: Opcode::SorterCompare,
2950            p1: 0,
2951            p2: 1,
2952            p3: 7,
2953            p4: P4::None,
2954            p5: 0,
2955        };
2956        let ordinary_spans = opcode_register_spans(&ordinary);
2957        let preflight = VdbeOp {
2958            p5: SORTER_COMPARE_TOP_N_PREFLIGHT,
2959            ..ordinary
2960        };
2961        let preflight_spans = opcode_register_spans(&preflight);
2962
2963        assert_eq!(
2964            ordinary_spans,
2965            OpcodeRegisterSpans {
2966                read_start: 7,
2967                read_len: 1,
2968                write_start: -1,
2969                write_len: 0,
2970            },
2971            "ordinary SorterCompare must leave P3 read-only"
2972        );
2973        assert_eq!(
2974            preflight_spans,
2975            OpcodeRegisterSpans {
2976                read_start: 7,
2977                read_len: 1,
2978                write_start: 7,
2979                write_len: 1,
2980            },
2981            "top-N preflight must model P3 as consumed"
2982        );
2983    }
2984
2985    #[test]
2986    fn test_program_builder_does_not_treat_immediate_sorter_bound_as_register() {
2987        let mut builder = ProgramBuilder::new();
2988        builder.emit_op(Opcode::SorterOpen, 0, 1, 500, P4::None, 0);
2989        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2990
2991        let program = builder.finish().expect("program should build");
2992        assert_eq!(
2993            program.register_count(),
2994            0,
2995            "legacy immediate SorterOpen.p3 is not a register operand"
2996        );
2997    }
2998
2999    #[test]
3000    fn test_program_builder_rejects_out_of_bounds_goto_target() {
3001        let mut builder = ProgramBuilder::new();
3002        builder.emit_op(Opcode::Goto, 0, 99, 0, P4::None, 0);
3003        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3004
3005        let err = builder.finish().expect_err("invalid jump target must fail");
3006        match err {
3007            FrankenError::Internal(message) => {
3008                assert!(message.contains("Goto"));
3009                assert!(message.contains("p2"));
3010            }
3011            other => assert!(
3012                matches!(other, FrankenError::Internal(_)),
3013                "expected internal verifier error, got {other:?}"
3014            ),
3015        }
3016    }
3017
3018    #[test]
3019    fn test_program_builder_rejects_out_of_bounds_jump_branch_target() {
3020        let mut builder = ProgramBuilder::new();
3021        builder.emit_op(Opcode::Jump, 0, 1, 42, P4::None, 0);
3022        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3023
3024        let err = builder
3025            .finish()
3026            .expect_err("invalid branch target must fail");
3027        match err {
3028            FrankenError::Internal(message) => {
3029                assert!(message.contains("Jump"));
3030                assert!(message.contains("p3"));
3031            }
3032            other => assert!(
3033                matches!(other, FrankenError::Internal(_)),
3034                "expected internal verifier error, got {other:?}"
3035            ),
3036        }
3037    }
3038
3039    // ── test_all_opcode_dispatch_coverage ────────────────────────────────
3040    #[test]
3041    fn test_all_opcode_dispatch_coverage() {
3042        // Every assigned Opcode enum byte has a valid name and can be
3043        // constructed from its byte value. This ensures no gaps in the enum.
3044        for byte in 1..Opcode::COUNT as u8 {
3045            let opcode = Opcode::from_byte(byte);
3046            assert!(
3047                opcode.is_some(),
3048                "Opcode::from_byte({byte}) returned None — gap in opcode enum"
3049            );
3050            let opcode = opcode.unwrap();
3051            let name = opcode.name();
3052            assert!(!name.is_empty(), "opcode {byte} has empty name");
3053        }
3054        assert_eq!(Opcode::from_byte(Opcode::COUNT as u8), None);
3055    }
3056
3057    // ── test_p5_flags_u16_range ─────────────────────────────────────────
3058    #[test]
3059    fn test_p5_flags_u16_range() {
3060        // Confirm p5 is u16 and accepts values above 0xFF.
3061        let op = VdbeOp {
3062            opcode: Opcode::Eq,
3063            p1: 1,
3064            p2: 5,
3065            p3: 2,
3066            p4: P4::None,
3067            p5: 0x1FF, // 511, exceeds u8 range
3068        };
3069        assert_eq!(op.p5, 0x1FF);
3070        assert!(op.p5 > 255);
3071
3072        let op2 = VdbeOp {
3073            opcode: Opcode::Noop,
3074            p1: 0,
3075            p2: 0,
3076            p3: 0,
3077            p4: P4::None,
3078            p5: u16::MAX,
3079        };
3080        assert_eq!(op2.p5, 65535);
3081    }
3082
3083    // ── test_program_builder_basic ──────────────────────────────────────
3084    #[test]
3085    fn test_program_builder_basic() {
3086        let mut b = ProgramBuilder::new();
3087
3088        // Build: Init -> Integer 42 into r1 -> ResultRow r1,1 -> Halt
3089        let end_label = b.emit_label();
3090        b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
3091        let r1 = b.alloc_reg();
3092        assert_eq!(r1, 1);
3093        b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
3094        b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
3095        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3096        b.resolve_label(end_label);
3097
3098        let prog = b.finish().unwrap();
3099        assert_eq!(prog.len(), 4);
3100        assert_eq!(prog.register_count(), 1);
3101        assert_eq!(prog.max_bind_parameter_index().unwrap(), 0);
3102
3103        // The Init instruction's p2 should point to address 4 (after Halt).
3104        assert_eq!(prog.get(0).unwrap().opcode, Opcode::Init);
3105        assert_eq!(prog.get(0).unwrap().p2, 4);
3106    }
3107
3108    #[test]
3109    fn test_program_precomputes_max_bind_parameter_index() {
3110        let mut b = ProgramBuilder::new();
3111        b.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
3112        b.emit_op(Opcode::Variable, 4, 2, 0, P4::None, 0);
3113        b.emit_op(Opcode::Variable, 2, 3, 0, P4::None, 0);
3114        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3115        let prog = b.finish().unwrap();
3116        assert_eq!(prog.max_bind_parameter_index(), Ok(4));
3117    }
3118
3119    #[test]
3120    fn test_program_tracks_invalid_bind_parameter_index() {
3121        let mut b = ProgramBuilder::new();
3122        b.emit_op(Opcode::Variable, 0, 1, 0, P4::None, 0);
3123        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3124        let prog = b.finish().unwrap();
3125        assert_eq!(prog.max_bind_parameter_index(), Err(0));
3126    }
3127
3128    #[test]
3129    fn test_program_storage_only_hot_path_does_not_require_attached_memdb() {
3130        let mut b = ProgramBuilder::new();
3131        let end = b.emit_label();
3132        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3133        b.emit_op(Opcode::OpenWrite, 0, 256, 0, P4::Int(1), 0);
3134        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
3135        b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
3136        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
3137        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
3138        b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
3139        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
3140        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3141        b.resolve_label(end);
3142
3143        // ubs:ignore - VDBE ProgramBuilder finalization in a unit test; no token or randomness.
3144        let prog = match b.finish() {
3145            Ok(prog) => prog,
3146            Err(err) => {
3147                assert!(test_failure(), "program should build: {err}");
3148                return;
3149            }
3150        };
3151        assert!(
3152            !prog.requires_attached_memdb(),
3153            "storage-only table hot paths should not force a MemDatabase handoff"
3154        );
3155    }
3156
3157    #[test]
3158    fn test_program_with_ephemeral_cursor_requires_attached_memdb() {
3159        let mut b = ProgramBuilder::new();
3160        let end = b.emit_label();
3161        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3162        b.emit_op(Opcode::OpenEphemeral, 0, 1, 0, P4::None, 0);
3163        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3164        b.resolve_label(end);
3165
3166        let prog = b.finish().expect("program should build");
3167        assert!(
3168            prog.requires_attached_memdb(),
3169            "ephemeral table programs still depend on the attached MemDatabase"
3170        );
3171    }
3172
3173    #[test]
3174    fn test_program_with_sorter_cursor_does_not_require_attached_memdb() -> Result<()> {
3175        let mut b = ProgramBuilder::new();
3176        let end = b.emit_label();
3177        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3178        b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::Str("+".to_owned()), 0);
3179        b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
3180        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3181        b.resolve_label(end);
3182
3183        let prog = b.finish()?;
3184        assert!(
3185            !prog.requires_attached_memdb(),
3186            "sorter-backed temp/exchange state is owned by VDBE and should not force a MemDatabase handoff"
3187        );
3188        Ok(())
3189    }
3190
3191    #[test]
3192    fn test_program_builder_accumulates_table_index_meta_by_table_cursor() {
3193        use fsqlite_types::opcode::IndexCursorMeta;
3194
3195        let mut b = ProgramBuilder::new();
3196        b.register_table_indexes(
3197            3,
3198            vec![IndexCursorMeta {
3199                cursor_id: 4,
3200                column_indices: vec![0, 2],
3201            }],
3202        );
3203        b.register_table_indexes(
3204            3,
3205            vec![IndexCursorMeta {
3206                cursor_id: 5,
3207                column_indices: vec![1],
3208            }],
3209        );
3210
3211        let prog = b.finish().expect("program should build");
3212        let metas = prog
3213            .table_index_meta()
3214            .get(&3)
3215            .expect("table cursor metadata should be present");
3216        assert_eq!(metas.len(), 2);
3217        assert_eq!(metas[0].cursor_id, 4);
3218        assert_eq!(metas[0].column_indices, vec![0, 2]);
3219        assert_eq!(metas[1].cursor_id, 5);
3220        assert_eq!(metas[1].column_indices, vec![1]);
3221    }
3222
3223    // ── test_disassemble ────────────────────────────────────────────────
3224    #[test]
3225    fn test_disassemble() {
3226        let mut b = ProgramBuilder::new();
3227        b.emit_op(Opcode::Init, 0, 2, 0, P4::None, 0);
3228        b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
3229        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3230        let prog = b.finish().unwrap();
3231
3232        let asm = prog.disassemble();
3233        assert!(asm.contains("Init"));
3234        assert!(asm.contains("Integer"));
3235        assert!(asm.contains("Halt"));
3236        assert!(asm.contains("42")); // p1 of Integer
3237    }
3238
3239    // ── test_key_info ───────────────────────────────────────────────────
3240    #[test]
3241    fn test_key_info() {
3242        let ki = KeyInfo {
3243            num_fields: 3,
3244            collations: vec![
3245                "BINARY".to_owned(),
3246                "NOCASE".to_owned(),
3247                "BINARY".to_owned(),
3248            ],
3249            sort_orders: vec![SortOrder::Asc, SortOrder::Desc, SortOrder::Asc],
3250        };
3251        assert_eq!(ki.num_fields, 3);
3252        assert_eq!(ki.collations.len(), 3);
3253        assert_eq!(ki.sort_orders[1], SortOrder::Desc);
3254    }
3255
3256    // ── test_label_already_resolved ─────────────────────────────────────
3257    #[test]
3258    fn test_label_already_resolved() {
3259        // If a label is resolved before a jump references it, the jump
3260        // should be patched immediately.
3261        let mut b = ProgramBuilder::new();
3262        let label = b.emit_label();
3263        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3264        b.resolve_label(label); // resolved to address 1
3265        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3266
3267        // Now emit a jump referencing the already-resolved label.
3268        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3269        // p2 should already be patched to 1.
3270        assert_eq!(b.op_at(jump_addr).unwrap().p2, 1);
3271
3272        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3273
3274        let prog = b.finish().unwrap();
3275        assert_eq!(prog.len(), 4);
3276    }
3277
3278    // ── test_builder_register_via_builder ────────────────────────────────
3279    #[test]
3280    fn test_builder_register_via_builder() {
3281        let mut b = ProgramBuilder::new();
3282        let r1 = b.alloc_reg();
3283        let r2 = b.alloc_reg();
3284        let block = b.alloc_regs(4);
3285        assert_eq!(r1, 1);
3286        assert_eq!(r2, 2);
3287        assert_eq!(block, 3);
3288        assert_eq!(b.register_count(), 6);
3289
3290        // Temp allocation.
3291        let t1 = b.alloc_temp();
3292        assert_eq!(t1, 7);
3293        b.free_temp(t1);
3294        let t2 = b.alloc_temp();
3295        assert_eq!(t2, t1); // reused
3296    }
3297
3298    // ── test_resolve_label_to_specific_address ──────────────────────────
3299    #[test]
3300    fn test_resolve_label_to_specific_address() {
3301        let mut b = ProgramBuilder::new();
3302        let label = b.emit_label();
3303        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3304        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3305        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3306
3307        // Resolve to a specific address (not current).
3308        b.resolve_label_to(label, 42);
3309        assert_eq!(b.op_at(jump_addr).unwrap().p2, 42);
3310    }
3311
3312    // ── test_empty_program_finishes ─────────────────────────────────────
3313    #[test]
3314    fn test_empty_program_finishes() {
3315        let b = ProgramBuilder::new();
3316        let prog = b.finish().unwrap();
3317        assert!(prog.is_empty());
3318        assert_eq!(prog.register_count(), 0);
3319    }
3320
3321    // ── test_unreferenced_unresolved_label_ok ───────────────────────────
3322    #[test]
3323    fn test_unreferenced_unresolved_label_ok() {
3324        // A label that was created but never referenced or resolved should
3325        // not cause an error (it's unused, not a dangling reference).
3326        let mut b = ProgramBuilder::new();
3327        let _label = b.emit_label();
3328        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3329        let prog = b.finish().unwrap();
3330        assert_eq!(prog.len(), 1);
3331    }
3332
3333    // ── PRAGMA handling (bd-iwu.5) ───────────────────────────────────────
3334
3335    #[cfg(not(target_arch = "wasm32"))]
3336    use std::fs;
3337
3338    use fsqlite_ast::Statement;
3339    use fsqlite_error::FrankenError;
3340    use fsqlite_mvcc::{BeginKind, MvccError, TransactionManager};
3341    use fsqlite_parser::Parser;
3342    use fsqlite_types::{CommitSeq, ObjectId, Oti, PageData, PageNumber, PageSize};
3343    use fsqlite_wal::{
3344        DEFAULT_RAPTORQ_REPAIR_SYMBOLS, WalFecGroupMeta, WalFecGroupMetaInit, WalFecGroupRecord,
3345        WalFecRecoveryOutcome, WalFrameCandidate, WalSalts, append_wal_fec_group,
3346        build_source_page_hashes, generate_wal_fec_repair_symbols,
3347        recover_wal_fec_group_with_decoder, scan_wal_fec,
3348    };
3349    #[cfg(not(target_arch = "wasm32"))]
3350    use tempfile::tempdir;
3351
3352    fn parse_pragma(sql: &str) -> std::result::Result<fsqlite_ast::PragmaStatement, String> {
3353        let mut p = Parser::from_sql(sql);
3354        let stmt = p.parse_statement().expect("parse statement");
3355        match stmt {
3356            Statement::Pragma(p) => Ok(p),
3357            other => Err(format!("expected PRAGMA, got: {other:?}")),
3358        }
3359    }
3360
3361    fn test_page(first_byte: u8) -> PageData {
3362        let mut page = PageData::zeroed(PageSize::DEFAULT);
3363        page.as_bytes_mut()[0] = first_byte;
3364        page
3365    }
3366
3367    fn make_source_pages(seed: u8, k_source: u32) -> Vec<Vec<u8>> {
3368        let page_len = usize::try_from(PageSize::DEFAULT.get()).expect("page size fits usize");
3369        (0..k_source)
3370            .map(|idx| {
3371                let idx_u8 = u8::try_from(idx).expect("test k_source fits u8");
3372                let mut page = vec![seed.wrapping_add(idx_u8); page_len];
3373                page[0] = idx_u8;
3374                page
3375            })
3376            .collect()
3377    }
3378
3379    fn make_wal_fec_group(
3380        start_frame_no: u32,
3381        r_repair: u8,
3382        seed: u8,
3383    ) -> (WalFecGroupRecord, Vec<Vec<u8>>) {
3384        let k_source = 5_u32;
3385        let source_pages = make_source_pages(seed, k_source);
3386        let page_size = PageSize::DEFAULT.get();
3387        let source_hashes = build_source_page_hashes(&source_pages);
3388        let page_numbers = (0..k_source).map(|i| 10 + i).collect::<Vec<_>>();
3389        let oti = Oti {
3390            f: u64::from(k_source) * u64::from(page_size),
3391            al: 1,
3392            t: page_size,
3393            z: 1,
3394            n: 1,
3395        };
3396        let meta = WalFecGroupMeta::from_init(WalFecGroupMetaInit {
3397            wal_salt1: 0xA11C_E001,
3398            wal_salt2: 0xA11C_E002,
3399            start_frame_no,
3400            end_frame_no: start_frame_no + (k_source - 1),
3401            db_size_pages: 256,
3402            page_size,
3403            k_source,
3404            r_repair: u32::from(r_repair),
3405            oti,
3406            object_id: ObjectId::from_bytes([seed; 16]),
3407            page_numbers,
3408            source_page_xxh3_128: source_hashes,
3409        })
3410        .expect("meta");
3411        let repair_symbols =
3412            generate_wal_fec_repair_symbols(&meta, &source_pages).expect("symbols");
3413        (
3414            WalFecGroupRecord::new(meta, repair_symbols).expect("group"),
3415            source_pages,
3416        )
3417    }
3418
3419    #[test]
3420    fn test_pragma_serializable_query_returns_current_setting() {
3421        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3422
3423        let stmt = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3424        let out = pragma::apply(&mut mgr, &stmt).unwrap();
3425        assert_eq!(out, pragma::PragmaOutput::Bool(true));
3426    }
3427
3428    #[test]
3429    fn test_connection_pragma_differential_views_default_query_returns_false() {
3430        let mut state = pragma::ConnectionPragmaState::default();
3431
3432        let stmt = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3433        let out = pragma::apply_connection_pragma(&mut state, &stmt).expect("query pragma");
3434        assert_eq!(out, pragma::PragmaOutput::Bool(false));
3435    }
3436
3437    #[test]
3438    fn test_connection_pragma_differential_views_set_and_query_across_aliases() {
3439        let mut state = pragma::ConnectionPragmaState::default();
3440
3441        let set_on = parse_pragma("PRAGMA fsqlite.differential_views = ON").expect("parse pragma");
3442        assert_eq!(
3443            pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3444            pragma::PragmaOutput::Bool(true)
3445        );
3446        assert!(state.differential_views.is_enabled());
3447
3448        let query = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3449        assert_eq!(
3450            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3451            pragma::PragmaOutput::Bool(true)
3452        );
3453    }
3454
3455    #[test]
3456    fn test_connection_pragma_differential_views_rejects_non_boolean_values() {
3457        let mut state = pragma::ConnectionPragmaState::default();
3458
3459        let stmt = parse_pragma("PRAGMA fsqlite_differential_views = 2").expect("parse pragma");
3460        assert!(matches!(
3461            pragma::apply_connection_pragma(&mut state, &stmt),
3462            Err(FrankenError::TypeMismatch { .. })
3463        ));
3464    }
3465
3466    #[test]
3467    fn test_connection_pragma_query_only_set_and_query() {
3468        let mut state = pragma::ConnectionPragmaState::default();
3469
3470        let query = parse_pragma("PRAGMA query_only").expect("parse pragma");
3471        assert_eq!(
3472            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3473            pragma::PragmaOutput::Int(0)
3474        );
3475
3476        let set_on = parse_pragma("PRAGMA query_only = ON").expect("parse pragma");
3477        assert_eq!(
3478            pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3479            pragma::PragmaOutput::Int(1)
3480        );
3481        assert!(state.query_only);
3482
3483        assert_eq!(
3484            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3485            pragma::PragmaOutput::Int(1)
3486        );
3487    }
3488
3489    #[test]
3490    fn test_connection_pragma_query_only_rejects_non_boolean_values() {
3491        let mut state = pragma::ConnectionPragmaState::default();
3492
3493        let stmt = parse_pragma("PRAGMA query_only = 2").expect("parse pragma");
3494        assert!(matches!(
3495            pragma::apply_connection_pragma(&mut state, &stmt),
3496            Err(FrankenError::TypeMismatch { .. })
3497        ));
3498    }
3499
3500    fn apply_sql(state: &mut pragma::ConnectionPragmaState, sql: &str) -> pragma::PragmaOutput {
3501        let stmt = parse_pragma(sql).expect("parse pragma");
3502        pragma::apply_connection_pragma(state, &stmt).expect("apply pragma")
3503    }
3504
3505    #[test]
3506    fn test_connection_pragma_boolean_readbacks() {
3507        // GH #282/#278/#262/#281/#283: set/readback surfaces for boolean pragmas.
3508        use pragma::PragmaOutput::Bool;
3509        let mut state = pragma::ConnectionPragmaState::default();
3510
3511        // trusted_schema and automatic_index default ON; the rest default OFF.
3512        assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(true));
3513        assert_eq!(apply_sql(&mut state, "PRAGMA automatic_index"), Bool(true));
3514        assert_eq!(
3515            apply_sql(&mut state, "PRAGMA read_uncommitted"),
3516            Bool(false)
3517        );
3518        assert_eq!(apply_sql(&mut state, "PRAGMA cell_size_check"), Bool(false));
3519        assert_eq!(
3520            apply_sql(&mut state, "PRAGMA checkpoint_fullfsync"),
3521            Bool(false)
3522        );
3523
3524        // Set then read back each toggle.
3525        assert_eq!(
3526            apply_sql(&mut state, "PRAGMA trusted_schema = OFF"),
3527            Bool(false)
3528        );
3529        assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(false));
3530        assert_eq!(
3531            apply_sql(&mut state, "PRAGMA read_uncommitted = ON"),
3532            Bool(true)
3533        );
3534        assert_eq!(
3535            apply_sql(&mut state, "PRAGMA cell_size_check = 1"),
3536            Bool(true)
3537        );
3538        assert_eq!(
3539            apply_sql(&mut state, "PRAGMA checkpoint_fullfsync = TRUE"),
3540            Bool(true)
3541        );
3542        assert_eq!(
3543            apply_sql(&mut state, "PRAGMA automatic_index = 0"),
3544            Bool(false)
3545        );
3546    }
3547
3548    #[test]
3549    fn test_connection_pragma_locking_mode_readback() {
3550        // GH #273: locking_mode echoes normal/exclusive (lowercased).
3551        use pragma::PragmaOutput::Text;
3552        let mut state = pragma::ConnectionPragmaState::default();
3553        assert_eq!(
3554            apply_sql(&mut state, "PRAGMA locking_mode"),
3555            Text("normal".to_owned())
3556        );
3557        assert_eq!(
3558            apply_sql(&mut state, "PRAGMA locking_mode = EXCLUSIVE"),
3559            Text("exclusive".to_owned())
3560        );
3561        assert_eq!(
3562            apply_sql(&mut state, "PRAGMA locking_mode"),
3563            Text("exclusive".to_owned())
3564        );
3565        // An unrecognized value is ignored; the current mode is echoed unchanged.
3566        assert_eq!(
3567            apply_sql(&mut state, "PRAGMA locking_mode = bogus"),
3568            Text("exclusive".to_owned())
3569        );
3570    }
3571
3572    #[test]
3573    fn test_connection_pragma_secure_delete_tristate() {
3574        // GH #277: secure_delete is a tri-state integer (0=OFF, 1=ON, 2=FAST).
3575        use pragma::PragmaOutput::Int;
3576        let mut state = pragma::ConnectionPragmaState::default();
3577        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete"), Int(0));
3578        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = ON"), Int(1));
3579        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = FAST"), Int(2));
3580        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = OFF"), Int(0));
3581        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = 2"), Int(2));
3582    }
3583
3584    #[test]
3585    fn test_connection_pragma_threads_readback() {
3586        // GH #279: threads reports the stored limit; a set clamps to 8; a
3587        // negative argument leaves the limit unchanged.
3588        use pragma::PragmaOutput::Int;
3589        let mut state = pragma::ConnectionPragmaState::default();
3590        assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(0));
3591        assert_eq!(apply_sql(&mut state, "PRAGMA threads = 4"), Int(4));
3592        assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(4));
3593        // Clamps to the stock maximum of 8.
3594        assert_eq!(apply_sql(&mut state, "PRAGMA threads = 100"), Int(8));
3595        // A negative argument leaves the current limit unchanged.
3596        assert_eq!(apply_sql(&mut state, "PRAGMA threads = -1"), Int(8));
3597    }
3598
3599    #[test]
3600    fn test_pragma_serializable_set_and_query() {
3601        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3602
3603        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3604        assert_eq!(
3605            pragma::apply(&mut mgr, &set_off).unwrap(),
3606            pragma::PragmaOutput::Bool(false)
3607        );
3608
3609        let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3610        assert_eq!(
3611            pragma::apply(&mut mgr, &query).unwrap(),
3612            pragma::PragmaOutput::Bool(false)
3613        );
3614    }
3615
3616    #[test]
3617    fn test_pragma_scope_per_connection_via_handler() {
3618        let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3619        let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3620
3621        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3622        let _ = pragma::apply(&mut conn_a, &set_off).unwrap();
3623
3624        let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3625        assert_eq!(
3626            pragma::apply(&mut conn_a, &query).unwrap(),
3627            pragma::PragmaOutput::Bool(false)
3628        );
3629        assert_eq!(
3630            pragma::apply(&mut conn_b, &query).unwrap(),
3631            pragma::PragmaOutput::Bool(true)
3632        );
3633    }
3634
3635    #[test]
3636    fn test_pragma_not_retroactive_to_active_txn_via_handler() {
3637        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3638
3639        let mut txn = mgr.begin(BeginKind::Concurrent).unwrap();
3640        mgr.write_page(&mut txn, PageNumber::new(1).unwrap(), test_page(0x01))
3641            .unwrap();
3642        txn.has_in_rw = true;
3643        txn.has_out_rw = true;
3644        assert!(txn.has_dangerous_structure());
3645
3646        // Flip OFF mid-txn; this must not affect the already-begun transaction.
3647        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3648        let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3649
3650        assert_eq!(
3651            mgr.commit(&mut txn).unwrap_err(),
3652            MvccError::BusySnapshot,
3653            "PRAGMA change must not be retroactive to an active txn"
3654        );
3655    }
3656
3657    #[test]
3658    fn test_e2e_serializable_pragma_switch_changes_behavior() {
3659        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3660
3661        // Run workload with serializable=ON: must abort on dangerous structure.
3662        let set_on = parse_pragma("PRAGMA fsqlite.serializable = ON").expect("parse pragma");
3663        let _ = pragma::apply(&mut mgr, &set_on).unwrap();
3664
3665        let mut txn_on = mgr.begin(BeginKind::Concurrent).unwrap();
3666        mgr.write_page(&mut txn_on, PageNumber::new(1).unwrap(), test_page(0x10))
3667            .unwrap();
3668        txn_on.has_in_rw = true;
3669        txn_on.has_out_rw = true;
3670        assert_eq!(
3671            mgr.commit(&mut txn_on).unwrap_err(),
3672            MvccError::BusySnapshot,
3673            "serializable=ON must enforce SSI (abort)"
3674        );
3675
3676        // Run the same workload with serializable=OFF: must commit (plain SI).
3677        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3678        let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3679
3680        let mut txn_off = mgr.begin(BeginKind::Concurrent).unwrap();
3681        mgr.write_page(&mut txn_off, PageNumber::new(2).unwrap(), test_page(0x20))
3682            .unwrap();
3683        txn_off.has_in_rw = true;
3684        txn_off.has_out_rw = true;
3685
3686        let seq = mgr.commit(&mut txn_off).unwrap();
3687        assert!(
3688            seq > CommitSeq::ZERO,
3689            "serializable=OFF must allow write skew"
3690        );
3691    }
3692
3693    #[test]
3694    fn test_pragma_raptorq_repair_symbols_default_query() {
3695        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3696        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3697        assert_eq!(
3698            pragma::apply(&mut mgr, &query).expect("query pragma"),
3699            pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3700        );
3701    }
3702
3703    #[cfg(not(target_arch = "wasm32"))]
3704    #[test]
3705    fn test_bd_1hi_12_unit_compliance_gate() {
3706        let dir = tempdir().expect("tempdir");
3707        let sidecar = dir.path().join("unit.wal-fec");
3708        let db_path = dir.path().join("unit.db");
3709        fs::write(&db_path, vec![0_u8; 100]).expect("seed db header");
3710
3711        let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3712        let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3713
3714        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3715        assert_eq!(
3716            pragma::apply_with_sidecar(&mut conn_a, &query, Some(&sidecar)).expect("query default"),
3717            pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3718        );
3719
3720        let set_max = parse_pragma("PRAGMA raptorq_repair_symbols = 255").expect("parse set max");
3721        assert_eq!(
3722            pragma::apply_with_sidecar(&mut conn_a, &set_max, Some(&sidecar)).expect("set max"),
3723            pragma::PragmaOutput::Int(255)
3724        );
3725
3726        let set_too_high =
3727            parse_pragma("PRAGMA raptorq_repair_symbols = 256").expect("parse set too high");
3728        assert!(matches!(
3729            pragma::apply_with_sidecar(&mut conn_a, &set_too_high, Some(&sidecar)),
3730            Err(FrankenError::OutOfRange { .. })
3731        ));
3732
3733        let set_negative =
3734            parse_pragma("PRAGMA raptorq_repair_symbols = -1").expect("parse set negative");
3735        assert!(matches!(
3736            pragma::apply_with_sidecar(&mut conn_a, &set_negative, Some(&sidecar)),
3737            Err(FrankenError::OutOfRange { .. })
3738        ));
3739
3740        let set_non_integer =
3741            parse_pragma("PRAGMA raptorq_repair_symbols = ON").expect("parse set non-integer");
3742        assert!(matches!(
3743            pragma::apply_with_sidecar(&mut conn_a, &set_non_integer, Some(&sidecar)),
3744            Err(FrankenError::TypeMismatch { .. })
3745        ));
3746
3747        let query_new_conn = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3748        assert_eq!(
3749            pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3750                .expect("query persisted value"),
3751            pragma::PragmaOutput::Int(255)
3752        );
3753
3754        let set_shared = parse_pragma("PRAGMA raptorq_repair_symbols = 7").expect("parse shared");
3755        let _ = pragma::apply_with_sidecar(&mut conn_a, &set_shared, Some(&sidecar))
3756            .expect("persist shared setting");
3757        assert_eq!(
3758            pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3759                .expect("cross-connection visibility"),
3760            pragma::PragmaOutput::Int(7)
3761        );
3762
3763        let db_bytes = fs::read(&db_path).expect("read db header");
3764        assert!(
3765            db_bytes[72..92].iter().all(|&byte| byte == 0),
3766            "sqlite header reserved bytes must remain untouched"
3767        );
3768    }
3769
3770    #[cfg(not(target_arch = "wasm32"))]
3771    #[test]
3772    fn prop_bd_1hi_12_structure_compliance() {
3773        let dir = tempdir().expect("tempdir");
3774        let sidecar = dir.path().join("property.wal-fec");
3775        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3776        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3777
3778        for value in 0_u16..=255_u16 {
3779            let sql = format!("PRAGMA raptorq_repair_symbols = {value}");
3780            let set_stmt = parse_pragma(&sql).expect("parse set statement");
3781            assert_eq!(
3782                pragma::apply_with_sidecar(&mut mgr, &set_stmt, Some(&sidecar)).expect("set value"),
3783                pragma::PragmaOutput::Int(i64::from(value))
3784            );
3785            assert_eq!(
3786                pragma::apply_with_sidecar(&mut mgr, &query, Some(&sidecar)).expect("query value"),
3787                pragma::PragmaOutput::Int(i64::from(value))
3788            );
3789        }
3790    }
3791
3792    #[cfg(not(target_arch = "wasm32"))]
3793    #[test]
3794    #[allow(clippy::too_many_lines)]
3795    fn test_e2e_bd_1hi_12_compliance() {
3796        let dir = tempdir().expect("tempdir");
3797        let sidecar = dir.path().join("e2e.wal-fec");
3798        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3799
3800        let set_zero = parse_pragma("PRAGMA raptorq_repair_symbols = 0").expect("parse set 0");
3801        let _ = pragma::apply_with_sidecar(&mut mgr, &set_zero, Some(&sidecar)).expect("set 0");
3802        if mgr.raptorq_repair_symbols() > 0 {
3803            let (group, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x10);
3804            append_wal_fec_group(&sidecar, &group).expect("append group");
3805        }
3806        let after_zero = scan_wal_fec(&sidecar).expect("scan after zero");
3807        assert!(
3808            after_zero.groups.is_empty(),
3809            "N=0 must produce no .wal-fec groups for new commits"
3810        );
3811
3812        let set_one = parse_pragma("PRAGMA raptorq_repair_symbols = 1").expect("parse set 1");
3813        let _ = pragma::apply_with_sidecar(&mut mgr, &set_one, Some(&sidecar)).expect("set 1");
3814        let (group_r1, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x11);
3815        append_wal_fec_group(&sidecar, &group_r1).expect("append r=1 group");
3816
3817        let set_two = parse_pragma("PRAGMA raptorq_repair_symbols = 2").expect("parse set 2");
3818        let _ = pragma::apply_with_sidecar(&mut mgr, &set_two, Some(&sidecar)).expect("set 2");
3819        let (group_r2, _) = make_wal_fec_group(6, mgr.raptorq_repair_symbols(), 0x22);
3820        append_wal_fec_group(&sidecar, &group_r2).expect("append r=2 group");
3821
3822        let set_four = parse_pragma("PRAGMA raptorq_repair_symbols = 4").expect("parse set 4");
3823        let _ = pragma::apply_with_sidecar(&mut mgr, &set_four, Some(&sidecar)).expect("set 4");
3824        let (group_r4, source_pages_r4) =
3825            make_wal_fec_group(11, mgr.raptorq_repair_symbols(), 0x33);
3826        append_wal_fec_group(&sidecar, &group_r4).expect("append r=4 group");
3827
3828        let scan = scan_wal_fec(&sidecar).expect("scan sidecar");
3829        assert_eq!(scan.groups.len(), 3);
3830        assert_eq!(scan.groups[0].repair_symbols.len(), 1);
3831        assert_eq!(scan.groups[1].repair_symbols.len(), 2);
3832        assert_eq!(scan.groups[2].repair_symbols.len(), 4);
3833        assert_eq!(scan.groups[1].meta.r_repair, 2);
3834        assert_eq!(scan.groups[2].meta.r_repair, 4);
3835
3836        let group_id = group_r4.meta.group_id();
3837        let wal_salts = WalSalts {
3838            salt1: group_r4.meta.wal_salt1,
3839            salt2: group_r4.meta.wal_salt2,
3840        };
3841        let k_source = usize::try_from(group_r4.meta.k_source).expect("k fits usize");
3842
3843        let mut corrupt_three_frames = Vec::new();
3844        for (idx, page) in source_pages_r4.iter().enumerate() {
3845            let mut payload = page.clone();
3846            if idx < 3 {
3847                payload[0] ^= 0xFF;
3848            }
3849            corrupt_three_frames.push(WalFrameCandidate {
3850                frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3851                page_data: payload,
3852            });
3853        }
3854        let expected_pages = source_pages_r4.clone();
3855        let recovered = recover_wal_fec_group_with_decoder(
3856            &sidecar,
3857            group_id,
3858            wal_salts,
3859            group_r4.meta.start_frame_no,
3860            &corrupt_three_frames,
3861            move |meta: &WalFecGroupMeta, symbols| {
3862                if symbols.len() < usize::try_from(meta.k_source).expect("k fits usize") {
3863                    return Err(FrankenError::WalCorrupt {
3864                        detail: "insufficient symbols".to_owned(),
3865                    });
3866                }
3867                Ok(expected_pages.clone())
3868            },
3869        )
3870        .expect("recover with <=R corruption");
3871        assert!(
3872            matches!(recovered, WalFecRecoveryOutcome::Recovered(_)),
3873            "expected recovered outcome"
3874        );
3875        let WalFecRecoveryOutcome::Recovered(group) = recovered else {
3876            unreachable!("asserted recovered outcome above");
3877        };
3878        assert_eq!(group.recovered_pages.len(), k_source);
3879
3880        let mut corrupt_five_frames = Vec::new();
3881        for (idx, page) in source_pages_r4.iter().enumerate() {
3882            let mut payload = page.clone();
3883            payload[0] ^= 0x55;
3884            corrupt_five_frames.push(WalFrameCandidate {
3885                frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3886                page_data: payload,
3887            });
3888        }
3889        let truncated = recover_wal_fec_group_with_decoder(
3890            &sidecar,
3891            group_id,
3892            wal_salts,
3893            group_r4.meta.start_frame_no,
3894            &corrupt_five_frames,
3895            |_meta: &WalFecGroupMeta, _symbols| {
3896                Err(FrankenError::WalCorrupt {
3897                    detail: "decoder should not be able to recover".to_owned(),
3898                })
3899            },
3900        )
3901        .expect("recover with >R corruption");
3902        assert!(matches!(
3903            truncated,
3904            WalFecRecoveryOutcome::TruncateBeforeGroup { .. }
3905        ));
3906    }
3907}