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        /// Foreign key enforcement toggle (`PRAGMA foreign_keys`).
1563        pub foreign_keys: bool,
1564        /// Recursive trigger toggle (`PRAGMA recursive_triggers`).
1565        pub recursive_triggers: bool,
1566        /// Query-only toggle (`PRAGMA query_only`).
1567        pub query_only: bool,
1568        /// Connection-level SSI toggle (`PRAGMA fsqlite.serializable`).
1569        pub serializable: bool,
1570        /// Differential-view streaming toggle (`PRAGMA fsqlite_differential_views`).
1571        pub differential_views: DifferentialViewsSetting,
1572        /// WAL-FEC repair symbol budget (`PRAGMA raptorq_repair_symbols`).
1573        pub raptorq_repair_symbols: u8,
1574        /// MVCC maximum committed versions per page chain before eager GC.
1575        /// `PRAGMA fsqlite.mvcc_max_chain_length`.
1576        pub mvcc_max_chain_length: usize,
1577        /// MVCC serialized writer lease duration in seconds.
1578        /// `PRAGMA fsqlite.mvcc_writer_lease_secs`.
1579        pub mvcc_writer_lease_secs: u64,
1580        /// `PRAGMA writable_schema` toggle — allows direct DML on sqlite_master.
1581        pub writable_schema: bool,
1582        /// `PRAGMA case_sensitive_like` toggle. When `false` (the default) LIKE
1583        /// folds ASCII case; when `true` LIKE is byte-exact (case-sensitive).
1584        pub case_sensitive_like: bool,
1585        /// `PRAGMA trusted_schema` toggle. Default ON (1), matching the C library
1586        /// default (`SQLITE_TRUSTED_SCHEMA` unset). Set/readback surface only.
1587        pub trusted_schema: bool,
1588        /// `PRAGMA read_uncommitted` toggle (default OFF). Set/readback surface.
1589        pub read_uncommitted: bool,
1590        /// `PRAGMA cell_size_check` toggle (default OFF). Set/readback surface.
1591        pub cell_size_check: bool,
1592        /// `PRAGMA checkpoint_fullfsync` toggle (default OFF). Set/readback surface.
1593        pub checkpoint_fullfsync: bool,
1594        /// `PRAGMA automatic_index` toggle (default ON). Set/readback surface.
1595        pub automatic_index: bool,
1596        /// `PRAGMA locking_mode` (`normal` or `exclusive`; default `normal`).
1597        /// Set/readback surface only.
1598        pub locking_mode: String,
1599        /// `PRAGMA secure_delete` tri-state: 0 = OFF, 1 = ON, 2 = FAST (default 0).
1600        /// Set/readback surface only.
1601        pub secure_delete: i64,
1602        /// `PRAGMA threads` auxiliary worker-thread limit (default 0). Advisory:
1603        /// FrankenSQLite does not spawn SQLite-style sort helper threads, so this
1604        /// is a stored limit clamped to the stock maximum (8).
1605        pub threads: i64,
1606    }
1607
1608    impl Default for ConnectionPragmaState {
1609        fn default() -> Self {
1610            Self {
1611                journal_mode: "wal".to_owned(),
1612                synchronous: "NORMAL".to_owned(),
1613                cache_size: -2000,
1614                page_size: 4096,
1615                busy_timeout_ms: 5000,
1616                temp_store: 0,
1617                mmap_size: 0,
1618                auto_vacuum: 0,
1619                wal_autocheckpoint: 1000,
1620                user_version: 0,
1621                application_id: 0,
1622                foreign_keys: false,
1623                recursive_triggers: false,
1624                query_only: false,
1625                serializable: true,
1626                differential_views: DifferentialViewsSetting::Off,
1627                raptorq_repair_symbols: DEFAULT_RAPTORQ_REPAIR_SYMBOLS,
1628                mvcc_max_chain_length: 64,
1629                mvcc_writer_lease_secs: 30,
1630                writable_schema: false,
1631                case_sensitive_like: false,
1632                trusted_schema: true,
1633                read_uncommitted: false,
1634                cell_size_check: false,
1635                checkpoint_fullfsync: false,
1636                automatic_index: true,
1637                locking_mode: "normal".to_owned(),
1638                secure_delete: 0,
1639                threads: 0,
1640            }
1641        }
1642    }
1643
1644    /// Apply a PRAGMA statement to the provided connection-scoped state.
1645    ///
1646    /// Currently supports:
1647    /// - `PRAGMA fsqlite.serializable`
1648    /// - `PRAGMA fsqlite.serializable = ON|OFF|TRUE|FALSE|1|0`
1649    /// - `PRAGMA raptorq_repair_symbols`
1650    /// - `PRAGMA raptorq_repair_symbols = N` (N in [0, 255])
1651    ///
1652    /// Unknown pragmas return [`PragmaOutput::Unsupported`].
1653    pub fn apply(mgr: &mut TransactionManager, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1654        apply_with_sidecar(mgr, stmt, None)
1655    }
1656
1657    /// Apply a PRAGMA statement with optional `.wal-fec` sidecar persistence.
1658    pub fn apply_with_sidecar(
1659        mgr: &mut TransactionManager,
1660        stmt: &PragmaStatement,
1661        wal_fec_sidecar_path: Option<&Path>,
1662    ) -> Result<PragmaOutput> {
1663        if is_fsqlite_serializable(&stmt.name) {
1664            return apply_serializable(mgr, stmt);
1665        }
1666        if is_raptorq_repair_symbols(&stmt.name) {
1667            return apply_raptorq_repair_symbols(mgr, stmt, wal_fec_sidecar_path);
1668        }
1669        Ok(PragmaOutput::Unsupported)
1670    }
1671
1672    /// Apply a PRAGMA to connection-level settings.
1673    ///
1674    /// Handles common connection-scoped PRAGMAs used by the harness and
1675    /// compatibility paths. Returns `Unsupported` for pragmas not handled at
1676    /// this layer, allowing the caller to chain with [`apply`].
1677    pub fn apply_connection_pragma(
1678        state: &mut ConnectionPragmaState,
1679        stmt: &PragmaStatement,
1680    ) -> Result<PragmaOutput> {
1681        let name = &stmt.name.name;
1682        if is_fsqlite_serializable(&stmt.name) {
1683            return apply_serializable_connection(state, stmt);
1684        }
1685        if is_fsqlite_differential_views(&stmt.name) {
1686            return apply_differential_views_connection(state, stmt);
1687        }
1688        if is_raptorq_repair_symbols(&stmt.name) {
1689            return apply_raptorq_repair_symbols_connection(state, stmt);
1690        }
1691        if name.eq_ignore_ascii_case("journal_mode") {
1692            return apply_journal_mode(state, stmt);
1693        }
1694        if name.eq_ignore_ascii_case("synchronous") {
1695            return apply_synchronous(state, stmt);
1696        }
1697        if name.eq_ignore_ascii_case("cache_size") {
1698            return apply_cache_size(state, stmt);
1699        }
1700        if name.eq_ignore_ascii_case("page_size") {
1701            return apply_page_size(state, stmt);
1702        }
1703        if name.eq_ignore_ascii_case("busy_timeout") {
1704            return apply_busy_timeout(state, stmt);
1705        }
1706        if name.eq_ignore_ascii_case("temp_store") {
1707            return apply_temp_store(state, stmt);
1708        }
1709        if name.eq_ignore_ascii_case("mmap_size") {
1710            return apply_mmap_size(state, stmt);
1711        }
1712        if name.eq_ignore_ascii_case("auto_vacuum") {
1713            return apply_auto_vacuum(state, stmt);
1714        }
1715        if name.eq_ignore_ascii_case("wal_autocheckpoint") {
1716            return apply_wal_autocheckpoint(state, stmt);
1717        }
1718        if name.eq_ignore_ascii_case("user_version") {
1719            return apply_user_version(state, stmt);
1720        }
1721        if name.eq_ignore_ascii_case("application_id") {
1722            return apply_application_id(state, stmt);
1723        }
1724        if name.eq_ignore_ascii_case("foreign_keys") {
1725            return apply_foreign_keys(state, stmt);
1726        }
1727        if name.eq_ignore_ascii_case("recursive_triggers") {
1728            return apply_recursive_triggers(state, stmt);
1729        }
1730        if name.eq_ignore_ascii_case("query_only") {
1731            return apply_query_only(state, stmt);
1732        }
1733        if name.eq_ignore_ascii_case("writable_schema") {
1734            return apply_writable_schema(state, stmt);
1735        }
1736        if name.eq_ignore_ascii_case("case_sensitive_like") {
1737            return apply_case_sensitive_like(state, stmt);
1738        }
1739        if name.eq_ignore_ascii_case("trusted_schema") {
1740            return apply_bool_toggle(&mut state.trusted_schema, stmt);
1741        }
1742        if name.eq_ignore_ascii_case("read_uncommitted") {
1743            return apply_bool_toggle(&mut state.read_uncommitted, stmt);
1744        }
1745        if name.eq_ignore_ascii_case("cell_size_check") {
1746            return apply_bool_toggle(&mut state.cell_size_check, stmt);
1747        }
1748        if name.eq_ignore_ascii_case("checkpoint_fullfsync") {
1749            return apply_bool_toggle(&mut state.checkpoint_fullfsync, stmt);
1750        }
1751        if name.eq_ignore_ascii_case("automatic_index") {
1752            return apply_bool_toggle(&mut state.automatic_index, stmt);
1753        }
1754        if name.eq_ignore_ascii_case("locking_mode") {
1755            return apply_locking_mode(state, stmt);
1756        }
1757        if name.eq_ignore_ascii_case("secure_delete") {
1758            return apply_secure_delete(state, stmt);
1759        }
1760        if name.eq_ignore_ascii_case("threads") {
1761            return apply_threads(state, stmt);
1762        }
1763        if is_fsqlite_mvcc_max_chain_length(&stmt.name) {
1764            return apply_mvcc_max_chain_length(state, stmt);
1765        }
1766        if is_fsqlite_mvcc_writer_lease_secs(&stmt.name) {
1767            return apply_mvcc_writer_lease_secs(state, stmt);
1768        }
1769        Ok(PragmaOutput::Unsupported)
1770    }
1771
1772    fn apply_serializable_connection(
1773        state: &mut ConnectionPragmaState,
1774        stmt: &PragmaStatement,
1775    ) -> Result<PragmaOutput> {
1776        match &stmt.value {
1777            None => Ok(PragmaOutput::Bool(state.serializable)),
1778            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1779                let enabled = parse_bool(expr)?;
1780                state.serializable = enabled;
1781                Ok(PragmaOutput::Bool(enabled))
1782            }
1783        }
1784    }
1785
1786    /// Generic boolean-toggle PRAGMA: a bare query echoes the current value and an
1787    /// assignment stores and echoes the new value, matching C SQLite's integer
1788    /// 0/1 readback. Used for the set/readback-only pragmas trusted_schema,
1789    /// read_uncommitted, cell_size_check, checkpoint_fullfsync, and
1790    /// automatic_index. (GH #282, #278, #262, #281, #283)
1791    fn apply_bool_toggle(flag: &mut bool, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1792        match &stmt.value {
1793            None => Ok(PragmaOutput::Bool(*flag)),
1794            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1795                let enabled = parse_bool(expr)?;
1796                *flag = enabled;
1797                Ok(PragmaOutput::Bool(enabled))
1798            }
1799        }
1800    }
1801
1802    /// `PRAGMA locking_mode [= NORMAL|EXCLUSIVE]`. A bare query echoes the current
1803    /// mode; an assignment accepts NORMAL/EXCLUSIVE case-insensitively and echoes
1804    /// the lowercased mode. Any other value is ignored and the current mode is
1805    /// echoed unchanged, matching C SQLite. (GH #273)
1806    fn apply_locking_mode(
1807        state: &mut ConnectionPragmaState,
1808        stmt: &PragmaStatement,
1809    ) -> Result<PragmaOutput> {
1810        match &stmt.value {
1811            None => Ok(PragmaOutput::Text(state.locking_mode.clone())),
1812            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1813                let requested = parse_text_expr(expr)?.to_ascii_lowercase();
1814                if requested == "normal" || requested == "exclusive" {
1815                    state.locking_mode = requested;
1816                }
1817                Ok(PragmaOutput::Text(state.locking_mode.clone()))
1818            }
1819        }
1820    }
1821
1822    /// `PRAGMA secure_delete [= OFF|ON|FAST|0|1|2]`. Tri-state: 0 = OFF, 1 = ON,
1823    /// 2 = FAST. C SQLite reports the integer on readback and echoes the new value
1824    /// on assignment. This is the set/readback surface only; the actual
1825    /// zero-on-delete storage semantics are tracked separately. (GH #277)
1826    fn apply_secure_delete(
1827        state: &mut ConnectionPragmaState,
1828        stmt: &PragmaStatement,
1829    ) -> Result<PragmaOutput> {
1830        match &stmt.value {
1831            None => Ok(PragmaOutput::Int(state.secure_delete)),
1832            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1833                let value = parse_secure_delete_value(expr)?;
1834                state.secure_delete = value;
1835                Ok(PragmaOutput::Int(value))
1836            }
1837        }
1838    }
1839
1840    fn parse_secure_delete_value(expr: &Expr) -> Result<i64> {
1841        if let Expr::Literal(Literal::Integer(n), _) = expr {
1842            return match *n {
1843                0..=2 => Ok(*n),
1844                _ => Err(FrankenError::OutOfRange {
1845                    what: "secure_delete".to_owned(),
1846                    value: n.to_string(),
1847                }),
1848            };
1849        }
1850        if let Ok(text) = parse_text_expr(expr)
1851            && text.eq_ignore_ascii_case("fast")
1852        {
1853            return Ok(2);
1854        }
1855        // OFF/ON/TRUE/FALSE map to 0/1.
1856        Ok(i64::from(parse_bool(expr)?))
1857    }
1858
1859    /// `PRAGMA threads [= N]`. A bare query reports the current limit; an
1860    /// assignment with a non-negative N clamps to the stock maximum (8), stores
1861    /// it, and echoes the effective value. A negative argument leaves the limit
1862    /// unchanged and just reports it, matching C SQLite. (GH #279)
1863    fn apply_threads(
1864        state: &mut ConnectionPragmaState,
1865        stmt: &PragmaStatement,
1866    ) -> Result<PragmaOutput> {
1867        /// Stock `SQLITE_MAX_WORKER_THREADS`.
1868        const MAX_WORKER_THREADS: i64 = 8;
1869        match &stmt.value {
1870            None => Ok(PragmaOutput::Int(state.threads)),
1871            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1872                let n = parse_integer_expr(expr)?;
1873                if n >= 0 {
1874                    state.threads = n.min(MAX_WORKER_THREADS);
1875                }
1876                Ok(PragmaOutput::Int(state.threads))
1877            }
1878        }
1879    }
1880
1881    /// `PRAGMA case_sensitive_like = ON|OFF`. SQLite treats this as write-only,
1882    /// but mirroring the other boolean toggles we also echo the current value on
1883    /// the no-argument query form. When ON, LIKE becomes byte-exact; the actual
1884    /// matching behavior is honored by the LIKE evaluation paths that read this
1885    /// flag (via the connection's pragma state).
1886    fn apply_case_sensitive_like(
1887        state: &mut ConnectionPragmaState,
1888        stmt: &PragmaStatement,
1889    ) -> Result<PragmaOutput> {
1890        match &stmt.value {
1891            None => Ok(PragmaOutput::Bool(state.case_sensitive_like)),
1892            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1893                let enabled = parse_bool(expr)?;
1894                state.case_sensitive_like = enabled;
1895                Ok(PragmaOutput::Bool(enabled))
1896            }
1897        }
1898    }
1899
1900    fn apply_raptorq_repair_symbols_connection(
1901        state: &mut ConnectionPragmaState,
1902        stmt: &PragmaStatement,
1903    ) -> Result<PragmaOutput> {
1904        match &stmt.value {
1905            None => Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols))),
1906            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1907                let value = parse_integer_expr(expr)?;
1908                if !(0..=i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS)).contains(&value) {
1909                    return Err(FrankenError::OutOfRange {
1910                        what: "raptorq_repair_symbols".to_owned(),
1911                        value: value.to_string(),
1912                    });
1913                }
1914                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1915                {
1916                    state.raptorq_repair_symbols = value as u8;
1917                }
1918                Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols)))
1919            }
1920        }
1921    }
1922
1923    fn apply_differential_views_connection(
1924        state: &mut ConnectionPragmaState,
1925        stmt: &PragmaStatement,
1926    ) -> Result<PragmaOutput> {
1927        match &stmt.value {
1928            None => Ok(PragmaOutput::Bool(state.differential_views.is_enabled())),
1929            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1930                let enabled = parse_bool(expr)?;
1931                state.differential_views = DifferentialViewsSetting::from_enabled(enabled);
1932                Ok(PragmaOutput::Bool(enabled))
1933            }
1934        }
1935    }
1936
1937    fn apply_journal_mode(
1938        state: &mut ConnectionPragmaState,
1939        stmt: &PragmaStatement,
1940    ) -> Result<PragmaOutput> {
1941        match &stmt.value {
1942            None => Ok(PragmaOutput::Text(state.journal_mode.clone())),
1943            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1944                let mode = parse_text_expr(expr)?;
1945                let lower = mode.to_ascii_lowercase();
1946                match lower.as_str() {
1947                    "delete" | "truncate" | "persist" | "memory" | "wal" | "off" => {
1948                        state.journal_mode.clone_from(&lower);
1949                        Ok(PragmaOutput::Text(lower))
1950                    }
1951                    _ => Err(FrankenError::TypeMismatch {
1952                        expected: "delete|truncate|persist|memory|wal|off".to_owned(),
1953                        actual: mode,
1954                    }),
1955                }
1956            }
1957        }
1958    }
1959
1960    fn apply_synchronous(
1961        state: &mut ConnectionPragmaState,
1962        stmt: &PragmaStatement,
1963    ) -> Result<PragmaOutput> {
1964        match &stmt.value {
1965            None => Ok(PragmaOutput::Text(state.synchronous.clone())),
1966            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1967                let val = parse_synchronous_value(expr)?;
1968                state.synchronous.clone_from(&val);
1969                Ok(PragmaOutput::Text(val))
1970            }
1971        }
1972    }
1973
1974    fn parse_synchronous_value(expr: &Expr) -> Result<String> {
1975        // Accept both text names and integer codes (0=OFF, 1=NORMAL, 2=FULL, 3=EXTRA).
1976        if let Expr::Literal(Literal::Integer(n), _) = expr {
1977            match n {
1978                0 => Ok("OFF".to_owned()),
1979                1 => Ok("NORMAL".to_owned()),
1980                2 => Ok("FULL".to_owned()),
1981                3 => Ok("EXTRA".to_owned()),
1982                _ => Err(FrankenError::OutOfRange {
1983                    what: "synchronous".to_owned(),
1984                    value: n.to_string(),
1985                }),
1986            }
1987        } else {
1988            let text = parse_text_expr(expr)?;
1989            let upper = text.to_ascii_uppercase();
1990            match upper.as_str() {
1991                "OFF" | "NORMAL" | "FULL" | "EXTRA" => Ok(upper),
1992                _ => Err(FrankenError::TypeMismatch {
1993                    expected: "OFF|NORMAL|FULL|EXTRA|0|1|2|3".to_owned(),
1994                    actual: text,
1995                }),
1996            }
1997        }
1998    }
1999
2000    fn apply_cache_size(
2001        state: &mut ConnectionPragmaState,
2002        stmt: &PragmaStatement,
2003    ) -> Result<PragmaOutput> {
2004        match &stmt.value {
2005            None => Ok(PragmaOutput::Int(state.cache_size)),
2006            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2007                let val = parse_integer_expr(expr)?;
2008                state.cache_size = val;
2009                Ok(PragmaOutput::Int(val))
2010            }
2011        }
2012    }
2013
2014    fn apply_page_size(
2015        state: &mut ConnectionPragmaState,
2016        stmt: &PragmaStatement,
2017    ) -> Result<PragmaOutput> {
2018        match &stmt.value {
2019            None => Ok(PragmaOutput::Int(i64::from(state.page_size))),
2020            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2021                let val = parse_integer_expr(expr)?;
2022                if !(512..=65536).contains(&val) || !is_power_of_two(val) {
2023                    return Err(FrankenError::OutOfRange {
2024                        what: "page_size".to_owned(),
2025                        value: val.to_string(),
2026                    });
2027                }
2028                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2029                {
2030                    state.page_size = val as u32;
2031                }
2032                Ok(PragmaOutput::Int(val))
2033            }
2034        }
2035    }
2036
2037    fn apply_busy_timeout(
2038        state: &mut ConnectionPragmaState,
2039        stmt: &PragmaStatement,
2040    ) -> Result<PragmaOutput> {
2041        match &stmt.value {
2042            None => Ok(PragmaOutput::Int(state.busy_timeout_ms)),
2043            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2044                let val = parse_integer_expr(expr)?;
2045                state.busy_timeout_ms = val.max(0);
2046                Ok(PragmaOutput::Int(state.busy_timeout_ms))
2047            }
2048        }
2049    }
2050
2051    fn apply_temp_store(
2052        state: &mut ConnectionPragmaState,
2053        stmt: &PragmaStatement,
2054    ) -> Result<PragmaOutput> {
2055        match &stmt.value {
2056            None => Ok(PragmaOutput::Int(state.temp_store)),
2057            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2058                let val = parse_temp_store_value(expr)?;
2059                state.temp_store = val;
2060                Ok(PragmaOutput::Int(val))
2061            }
2062        }
2063    }
2064
2065    fn apply_mmap_size(
2066        state: &mut ConnectionPragmaState,
2067        stmt: &PragmaStatement,
2068    ) -> Result<PragmaOutput> {
2069        match &stmt.value {
2070            None => Ok(PragmaOutput::Int(state.mmap_size)),
2071            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2072                let val = parse_integer_expr(expr)?;
2073                state.mmap_size = val.max(0);
2074                Ok(PragmaOutput::Int(state.mmap_size))
2075            }
2076        }
2077    }
2078
2079    fn apply_auto_vacuum(
2080        state: &mut ConnectionPragmaState,
2081        stmt: &PragmaStatement,
2082    ) -> Result<PragmaOutput> {
2083        match &stmt.value {
2084            None => Ok(PragmaOutput::Int(state.auto_vacuum)),
2085            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2086                let val = parse_auto_vacuum_value(expr)?;
2087                state.auto_vacuum = val;
2088                Ok(PragmaOutput::Int(val))
2089            }
2090        }
2091    }
2092
2093    fn apply_wal_autocheckpoint(
2094        state: &mut ConnectionPragmaState,
2095        stmt: &PragmaStatement,
2096    ) -> Result<PragmaOutput> {
2097        match &stmt.value {
2098            None => Ok(PragmaOutput::Int(state.wal_autocheckpoint)),
2099            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2100                let val = parse_integer_expr(expr)?;
2101                state.wal_autocheckpoint = val.max(0);
2102                Ok(PragmaOutput::Int(state.wal_autocheckpoint))
2103            }
2104        }
2105    }
2106
2107    fn apply_user_version(
2108        state: &mut ConnectionPragmaState,
2109        stmt: &PragmaStatement,
2110    ) -> Result<PragmaOutput> {
2111        match &stmt.value {
2112            None => Ok(PragmaOutput::Int(state.user_version)),
2113            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2114                let val = parse_integer_expr(expr)?;
2115                state.user_version = val;
2116                Ok(PragmaOutput::Int(val))
2117            }
2118        }
2119    }
2120
2121    fn apply_application_id(
2122        state: &mut ConnectionPragmaState,
2123        stmt: &PragmaStatement,
2124    ) -> Result<PragmaOutput> {
2125        match &stmt.value {
2126            None => Ok(PragmaOutput::Int(state.application_id)),
2127            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2128                let val = parse_integer_expr(expr)?;
2129                state.application_id = val;
2130                Ok(PragmaOutput::Int(val))
2131            }
2132        }
2133    }
2134
2135    fn apply_foreign_keys(
2136        state: &mut ConnectionPragmaState,
2137        stmt: &PragmaStatement,
2138    ) -> Result<PragmaOutput> {
2139        match &stmt.value {
2140            None => Ok(PragmaOutput::Int(i64::from(state.foreign_keys))),
2141            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2142                let enabled = parse_bool(expr)?;
2143                state.foreign_keys = enabled;
2144                Ok(PragmaOutput::Int(i64::from(enabled)))
2145            }
2146        }
2147    }
2148
2149    fn apply_recursive_triggers(
2150        state: &mut ConnectionPragmaState,
2151        stmt: &PragmaStatement,
2152    ) -> Result<PragmaOutput> {
2153        match &stmt.value {
2154            None => Ok(PragmaOutput::Int(i64::from(state.recursive_triggers))),
2155            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2156                let enabled = parse_bool(expr)?;
2157                state.recursive_triggers = enabled;
2158                Ok(PragmaOutput::Int(i64::from(enabled)))
2159            }
2160        }
2161    }
2162
2163    fn apply_query_only(
2164        state: &mut ConnectionPragmaState,
2165        stmt: &PragmaStatement,
2166    ) -> Result<PragmaOutput> {
2167        match &stmt.value {
2168            None => Ok(PragmaOutput::Int(i64::from(state.query_only))),
2169            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2170                let enabled = parse_bool(expr)?;
2171                state.query_only = enabled;
2172                Ok(PragmaOutput::Int(i64::from(enabled)))
2173            }
2174        }
2175    }
2176
2177    fn apply_writable_schema(
2178        state: &mut ConnectionPragmaState,
2179        stmt: &PragmaStatement,
2180    ) -> Result<PragmaOutput> {
2181        match &stmt.value {
2182            None => Ok(PragmaOutput::Int(i64::from(state.writable_schema))),
2183            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2184                let enabled = parse_bool(expr)?;
2185                state.writable_schema = enabled;
2186                Ok(PragmaOutput::Int(i64::from(enabled)))
2187            }
2188        }
2189    }
2190
2191    fn parse_temp_store_value(expr: &Expr) -> Result<i64> {
2192        if let Expr::Literal(Literal::Integer(n), _) = expr {
2193            return match *n {
2194                0..=2 => Ok(*n),
2195                _ => Err(FrankenError::OutOfRange {
2196                    what: "temp_store".to_owned(),
2197                    value: n.to_string(),
2198                }),
2199            };
2200        }
2201
2202        let text = parse_text_expr(expr)?;
2203        match text.to_ascii_lowercase().as_str() {
2204            "default" => Ok(0),
2205            "file" => Ok(1),
2206            "memory" => Ok(2),
2207            _ => Err(FrankenError::TypeMismatch {
2208                expected: "DEFAULT|FILE|MEMORY|0|1|2".to_owned(),
2209                actual: text,
2210            }),
2211        }
2212    }
2213
2214    fn parse_auto_vacuum_value(expr: &Expr) -> Result<i64> {
2215        if let Expr::Literal(Literal::Integer(n), _) = expr {
2216            return match *n {
2217                0..=2 => Ok(*n),
2218                _ => Err(FrankenError::OutOfRange {
2219                    what: "auto_vacuum".to_owned(),
2220                    value: n.to_string(),
2221                }),
2222            };
2223        }
2224
2225        let text = parse_text_expr(expr)?;
2226        match text.to_ascii_lowercase().as_str() {
2227            "none" => Ok(0),
2228            "full" => Ok(1),
2229            "incremental" => Ok(2),
2230            _ => Err(FrankenError::TypeMismatch {
2231                expected: "NONE|FULL|INCREMENTAL|0|1|2".to_owned(),
2232                actual: text,
2233            }),
2234        }
2235    }
2236
2237    fn is_power_of_two(n: i64) -> bool {
2238        n > 0 && (n & (n - 1)) == 0
2239    }
2240
2241    /// Extract a text value from a PRAGMA assignment expression.
2242    fn parse_text_expr(expr: &Expr) -> Result<String> {
2243        match expr {
2244            Expr::Literal(Literal::String(s), _) => Ok(s.clone()),
2245            Expr::Column(col, _) => Ok(col.column.to_string()),
2246            Expr::Literal(Literal::Integer(n), _) => Ok(n.to_string()),
2247            other => Err(FrankenError::TypeMismatch {
2248                expected: "text or identifier".to_owned(),
2249                actual: format!("{other:?}"),
2250            }),
2251        }
2252    }
2253
2254    fn apply_serializable(
2255        mgr: &mut TransactionManager,
2256        stmt: &PragmaStatement,
2257    ) -> Result<PragmaOutput> {
2258        match &stmt.value {
2259            None => Ok(PragmaOutput::Bool(mgr.ssi_enabled())),
2260            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2261                let enabled = parse_bool(expr)?;
2262                mgr.set_ssi_enabled(enabled);
2263                Ok(PragmaOutput::Bool(mgr.ssi_enabled()))
2264            }
2265        }
2266    }
2267
2268    fn is_fsqlite_serializable(name: &QualifiedName) -> bool {
2269        name.schema
2270            .as_deref()
2271            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2272            && name.name.eq_ignore_ascii_case("serializable")
2273    }
2274
2275    fn is_fsqlite_differential_views(name: &QualifiedName) -> bool {
2276        match name.schema.as_deref() {
2277            Some(schema) => {
2278                schema.eq_ignore_ascii_case("fsqlite")
2279                    && name.name.eq_ignore_ascii_case("differential_views")
2280            }
2281            None => name.name.eq_ignore_ascii_case("fsqlite_differential_views"),
2282        }
2283    }
2284
2285    fn is_raptorq_repair_symbols(name: &QualifiedName) -> bool {
2286        let schema_ok = match name.schema.as_deref() {
2287            None => true,
2288            Some(schema) => schema.eq_ignore_ascii_case("fsqlite"),
2289        };
2290        schema_ok && name.name.eq_ignore_ascii_case("raptorq_repair_symbols")
2291    }
2292
2293    fn is_fsqlite_mvcc_max_chain_length(name: &QualifiedName) -> bool {
2294        name.schema
2295            .as_deref()
2296            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2297            && name.name.eq_ignore_ascii_case("mvcc_max_chain_length")
2298    }
2299
2300    fn is_fsqlite_mvcc_writer_lease_secs(name: &QualifiedName) -> bool {
2301        name.schema
2302            .as_deref()
2303            .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2304            && name.name.eq_ignore_ascii_case("mvcc_writer_lease_secs")
2305    }
2306
2307    fn apply_mvcc_max_chain_length(
2308        state: &mut ConnectionPragmaState,
2309        stmt: &PragmaStatement,
2310    ) -> Result<PragmaOutput> {
2311        match &stmt.value {
2312            None => Ok(PragmaOutput::Int(state.mvcc_max_chain_length as i64)),
2313            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2314                let value = parse_integer_expr(expr)?;
2315                if value < 2 {
2316                    return Err(FrankenError::OutOfRange {
2317                        what: "fsqlite.mvcc_max_chain_length".into(),
2318                        value: format!("{value} (minimum 2)"),
2319                    });
2320                }
2321                #[allow(clippy::cast_sign_loss)]
2322                {
2323                    state.mvcc_max_chain_length = value as usize;
2324                }
2325                // Note: value is stored in pragma_state and will be read by
2326                // the MVCC layer when creating concurrent execution contexts.
2327                // The MvccCoordinator's own max_chain_length is set at
2328                // construction; this PRAGMA value takes effect for new
2329                // concurrent transactions opened on this connection.
2330                Ok(PragmaOutput::Int(value))
2331            }
2332        }
2333    }
2334
2335    fn apply_mvcc_writer_lease_secs(
2336        state: &mut ConnectionPragmaState,
2337        stmt: &PragmaStatement,
2338    ) -> Result<PragmaOutput> {
2339        match &stmt.value {
2340            None => Ok(PragmaOutput::Int(state.mvcc_writer_lease_secs as i64)),
2341            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2342                let value = parse_integer_expr(expr)?;
2343                if value < 1 {
2344                    return Err(FrankenError::OutOfRange {
2345                        what: "fsqlite.mvcc_writer_lease_secs".into(),
2346                        value: format!("{value} (minimum 1)"),
2347                    });
2348                }
2349                #[allow(clippy::cast_sign_loss)]
2350                {
2351                    state.mvcc_writer_lease_secs = value as u64;
2352                }
2353                // Note: same propagation model as mvcc_max_chain_length above.
2354                Ok(PragmaOutput::Int(value))
2355            }
2356        }
2357    }
2358
2359    fn apply_raptorq_repair_symbols(
2360        mgr: &mut TransactionManager,
2361        stmt: &PragmaStatement,
2362        wal_fec_sidecar_path: Option<&Path>,
2363    ) -> Result<PragmaOutput> {
2364        match &stmt.value {
2365            None => {
2366                if let Some(sidecar) = wal_fec_sidecar_path {
2367                    let persisted = read_wal_fec_raptorq_repair_symbols(sidecar)?;
2368                    mgr.set_raptorq_repair_symbols(persisted);
2369                    debug!(
2370                        sidecar = %sidecar.display(),
2371                        raptorq_repair_symbols = persisted,
2372                        "loaded raptorq_repair_symbols from wal-fec sidecar"
2373                    );
2374                }
2375                Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2376            }
2377            Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2378                let requested = parse_raptorq_repair_symbols(expr)?;
2379                mgr.set_raptorq_repair_symbols(requested);
2380
2381                if let Some(sidecar) = wal_fec_sidecar_path {
2382                    persist_wal_fec_raptorq_repair_symbols(sidecar, requested)?;
2383                    info!(
2384                        sidecar = %sidecar.display(),
2385                        raptorq_repair_symbols = requested,
2386                        "persisted raptorq_repair_symbols to wal-fec sidecar"
2387                    );
2388                }
2389
2390                Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2391            }
2392        }
2393    }
2394
2395    fn parse_raptorq_repair_symbols(expr: &Expr) -> Result<u8> {
2396        let raw = parse_integer_expr(expr)?;
2397        if raw < 0 {
2398            warn!(
2399                value = raw,
2400                "rejecting negative raptorq_repair_symbols value"
2401            );
2402            return Err(FrankenError::OutOfRange {
2403                what: "raptorq_repair_symbols".to_owned(),
2404                value: raw.to_string(),
2405            });
2406        }
2407
2408        let max = i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS);
2409        if raw > max {
2410            warn!(
2411                value = raw,
2412                max = MAX_RAPTORQ_REPAIR_SYMBOLS,
2413                "rejecting out-of-range raptorq_repair_symbols value"
2414            );
2415            return Err(FrankenError::OutOfRange {
2416                what: "raptorq_repair_symbols".to_owned(),
2417                value: raw.to_string(),
2418            });
2419        }
2420
2421        u8::try_from(raw).map_err(|_| {
2422            error!(
2423                value = raw,
2424                "failed to convert validated raptorq_repair_symbols to u8"
2425            );
2426            FrankenError::OutOfRange {
2427                what: "raptorq_repair_symbols".to_owned(),
2428                value: raw.to_string(),
2429            }
2430        })
2431    }
2432
2433    fn parse_integer_expr(expr: &Expr) -> Result<i64> {
2434        match expr {
2435            Expr::Literal(Literal::Integer(n), _) => Ok(*n),
2436            Expr::UnaryOp {
2437                op: UnaryOp::Negate,
2438                expr,
2439                ..
2440            } => Ok(-parse_integer_expr(expr)?),
2441            Expr::UnaryOp {
2442                op: UnaryOp::Plus,
2443                expr,
2444                ..
2445            } => parse_integer_expr(expr),
2446            Expr::Column(col, _) => {
2447                col.column
2448                    .parse::<i64>()
2449                    .map_err(|_| FrankenError::TypeMismatch {
2450                        expected: "integer (0..255)".to_owned(),
2451                        actual: col.column.to_string(),
2452                    })
2453            }
2454            other => Err(FrankenError::TypeMismatch {
2455                expected: "integer (0..255)".to_owned(),
2456                actual: format!("{other:?}"),
2457            }),
2458        }
2459    }
2460
2461    fn parse_bool(expr: &Expr) -> Result<bool> {
2462        let (raw, parsed) = match expr {
2463            Expr::Literal(Literal::Integer(n), _) => (format!("{n}"), parse_int_bool(*n)),
2464            Expr::Literal(Literal::String(s), _) => (s.clone(), parse_str_bool(s)),
2465            Expr::Literal(Literal::True, _) => ("TRUE".to_owned(), Some(true)),
2466            Expr::Literal(Literal::False, _) => ("FALSE".to_owned(), Some(false)),
2467            Expr::Column(col, _) => (col.column.to_string(), parse_str_bool(&col.column)),
2468            other => {
2469                return Err(FrankenError::TypeMismatch {
2470                    expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2471                    actual: format!("{other:?}"),
2472                });
2473            }
2474        };
2475
2476        parsed.ok_or_else(|| FrankenError::TypeMismatch {
2477            expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2478            actual: raw,
2479        })
2480    }
2481
2482    fn parse_int_bool(n: i64) -> Option<bool> {
2483        match n {
2484            0 => Some(false),
2485            1 => Some(true),
2486            _ => None,
2487        }
2488    }
2489
2490    fn parse_str_bool(s: &str) -> Option<bool> {
2491        if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("true") {
2492            Some(true)
2493        } else if s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("false") {
2494            Some(false)
2495        } else if s == "1" {
2496            Some(true)
2497        } else if s == "0" {
2498            Some(false)
2499        } else {
2500            None
2501        }
2502    }
2503}
2504
2505// ── Tests ───────────────────────────────────────────────────────────────────
2506
2507#[cfg(test)]
2508mod tests {
2509    use super::*;
2510
2511    fn test_failure() -> bool {
2512        false
2513    }
2514
2515    // ── test_vdbe_op_struct_size ─────────────────────────────────────────
2516    #[test]
2517    fn test_vdbe_op_struct_size() {
2518        // Verify VdbeOp fields are accessible and correctly typed.
2519        let op = VdbeOp {
2520            opcode: Opcode::Integer,
2521            p1: 42,
2522            p2: 1,
2523            p3: 0,
2524            p4: P4::None,
2525            p5: 0,
2526        };
2527        assert_eq!(op.opcode, Opcode::Integer);
2528        assert_eq!(op.p1, 42_i32);
2529        assert_eq!(op.p2, 1_i32);
2530        assert_eq!(op.p3, 0_i32);
2531        assert_eq!(op.p4, P4::None);
2532        assert_eq!(op.p5, 0_u16);
2533    }
2534
2535    #[test]
2536    fn test_schema_evaluation_context_encoding_and_nested_restoration() {
2537        const CONSTANT_ARGUMENT_MASK: i32 = 0x15;
2538
2539        let mut builder = ProgramBuilder::new();
2540        let output = builder.alloc_reg();
2541        builder.with_schema_evaluation_context(SchemaEvaluationContext::Index, |builder| {
2542            builder.emit_op(
2543                Opcode::PureFunc,
2544                CONSTANT_ARGUMENT_MASK,
2545                0,
2546                output,
2547                P4::FuncName("INDEX_FN".to_owned()),
2548                0,
2549            );
2550            builder.with_schema_evaluation_context(
2551                SchemaEvaluationContext::CheckConstraint,
2552                |builder| {
2553                    builder.emit_op(
2554                        Opcode::Function,
2555                        CONSTANT_ARGUMENT_MASK,
2556                        0,
2557                        output,
2558                        P4::FuncName("CHECK_FN".to_owned()),
2559                        0,
2560                    );
2561                },
2562            );
2563            builder.emit_op(
2564                Opcode::PureFunc,
2565                CONSTANT_ARGUMENT_MASK,
2566                0,
2567                output,
2568                P4::FuncName("INDEX_AGAIN_FN".to_owned()),
2569                0,
2570            );
2571        });
2572        builder.with_schema_evaluation_context(
2573            SchemaEvaluationContext::GeneratedColumn,
2574            |builder| {
2575                builder.emit_op(
2576                    Opcode::PureFunc,
2577                    CONSTANT_ARGUMENT_MASK,
2578                    0,
2579                    output,
2580                    P4::FuncName("GENERATED_FN".to_owned()),
2581                    0,
2582                );
2583            },
2584        );
2585        builder.emit_op(
2586            Opcode::PureFunc,
2587            CONSTANT_ARGUMENT_MASK,
2588            0,
2589            output,
2590            P4::FuncName("ORDINARY_FN".to_owned()),
2591            0,
2592        );
2593        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2594
2595        let program = builder
2596            .finish()
2597            .expect("schema-context program should build");
2598        let function_ops: Vec<_> = program
2599            .ops()
2600            .iter()
2601            .filter(|op| matches!(op.opcode, Opcode::Function | Opcode::PureFunc))
2602            .collect();
2603        let contexts: Vec<_> = function_ops
2604            .iter()
2605            .map(|op| SchemaEvaluationContext::from_function_p1(op.p1))
2606            .collect();
2607
2608        assert_eq!(
2609            contexts,
2610            [
2611                Some(SchemaEvaluationContext::Index),
2612                Some(SchemaEvaluationContext::CheckConstraint),
2613                Some(SchemaEvaluationContext::Index),
2614                Some(SchemaEvaluationContext::GeneratedColumn),
2615                None,
2616            ]
2617        );
2618        assert!(
2619            function_ops
2620                .iter()
2621                .all(|op| { op.p1 & !FUNCTION_SCHEMA_CONTEXT_MASK == CONSTANT_ARGUMENT_MASK })
2622        );
2623    }
2624
2625    // ── test_p4_variant_all_types ───────────────────────────────────────
2626    #[test]
2627    fn test_p4_variant_all_types() {
2628        // Each P4 variant can be constructed and pattern-matched.
2629        let variants: Vec<P4> = vec![
2630            P4::None,
2631            P4::Int(42),
2632            P4::Int64(i64::MAX),
2633            P4::Real(1.234_567_89),
2634            P4::Str("hello".to_owned()),
2635            P4::Blob(vec![0xDE, 0xAD]),
2636            P4::Collation("BINARY".to_owned()),
2637            P4::FuncName("count".to_owned()),
2638            P4::Table("users".to_owned()),
2639            P4::Affinity("ddd".to_owned()),
2640            P4::PrecomputedHeader(fsqlite_types::record::PrecomputedRecordHeader::new(&[
2641                fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
2642                fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
2643            ])),
2644        ];
2645        assert_eq!(variants.len(), 11);
2646
2647        // Verify each variant matches itself.
2648        assert!(matches!(variants[0], P4::None));
2649        assert!(matches!(variants[1], P4::Int(42)));
2650        assert!(matches!(variants[2], P4::Int64(i64::MAX)));
2651        assert!(matches!(variants[3], P4::Real(_)));
2652        assert!(matches!(variants[4], P4::Str(_)));
2653        assert!(matches!(variants[5], P4::Blob(_)));
2654        assert!(matches!(variants[6], P4::Collation(_)));
2655        assert!(matches!(variants[7], P4::FuncName(ref s) if s == "count"));
2656        assert!(matches!(variants[8], P4::Table(ref s) if s == "users"));
2657        assert!(matches!(variants[9], P4::Affinity(ref s) if s == "ddd"));
2658        assert!(matches!(
2659            variants[10],
2660            P4::PrecomputedHeader(ref header) if header.template == vec![3, 0, 0]
2661        ));
2662    }
2663
2664    // ── test_label_emit_and_resolve ─────────────────────────────────────
2665    #[test]
2666    fn test_label_emit_and_resolve() {
2667        let mut b = ProgramBuilder::new();
2668
2669        // Emit two distinct labels.
2670        let label_a = b.emit_label();
2671        let label_b = b.emit_label();
2672        assert_ne!(label_a, label_b);
2673
2674        // Emit a jump to label_a (forward reference).
2675        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label_a, P4::None, 0);
2676        assert_eq!(b.op_at(jump_addr).unwrap().p2, -1); // unresolved placeholder
2677
2678        // Emit some instructions.
2679        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
2680        b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
2681
2682        // Resolve label_a to the current address (2 instructions after the jump).
2683        b.resolve_label(label_a);
2684
2685        // The jump's p2 should now be patched to address 3.
2686        assert_eq!(b.op_at(jump_addr).unwrap().p2, 3);
2687
2688        // Emit another jump to label_b.
2689        let jump2 = b.emit_jump_to_label(Opcode::If, 1, 0, label_b, P4::None, 0);
2690        b.resolve_label(label_b);
2691        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2692        assert_eq!(b.op_at(jump2).unwrap().p2, 4);
2693
2694        // Finish should succeed (all labels resolved).
2695        let prog = b.finish().unwrap();
2696        assert_eq!(prog.len(), 5);
2697    }
2698
2699    // ── test_unresolved_label_error ─────────────────────────────────────
2700    #[test]
2701    fn test_unresolved_label_error() {
2702        let mut b = ProgramBuilder::new();
2703        let label = b.emit_label();
2704        b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
2705
2706        // Don't resolve the label — finish should fail.
2707        let result = b.finish();
2708        assert!(result.is_err());
2709    }
2710
2711    // ── test_register_alloc_sequential ──────────────────────────────────
2712    #[test]
2713    fn test_register_alloc_sequential() {
2714        let mut alloc = RegisterAllocator::new();
2715
2716        // Sequential single allocations start at 1.
2717        assert_eq!(alloc.alloc_reg(), 1);
2718        assert_eq!(alloc.alloc_reg(), 2);
2719        assert_eq!(alloc.alloc_reg(), 3);
2720
2721        // Block allocation returns first register of contiguous block.
2722        let block_start = alloc.alloc_regs(3);
2723        assert_eq!(block_start, 4);
2724        // Next single alloc continues after the block.
2725        assert_eq!(alloc.alloc_reg(), 7);
2726
2727        assert_eq!(alloc.count(), 7);
2728    }
2729
2730    // ── test_register_temp_pool_reuse ───────────────────────────────────
2731    #[test]
2732    fn test_register_temp_pool_reuse() {
2733        let mut alloc = RegisterAllocator::new();
2734
2735        let r1 = alloc.alloc_reg(); // 1
2736        let t1 = alloc.alloc_temp(); // 2 (new allocation)
2737        let t2 = alloc.alloc_temp(); // 3 (new allocation)
2738        assert_eq!(r1, 1);
2739        assert_eq!(t1, 2);
2740        assert_eq!(t2, 3);
2741
2742        // Return temps to pool.
2743        alloc.free_temp(t1);
2744        alloc.free_temp(t2);
2745
2746        // Next temp allocations reuse from pool (LIFO order).
2747        let t3 = alloc.alloc_temp();
2748        let t4 = alloc.alloc_temp();
2749        assert_eq!(t3, t2); // 3 (last freed)
2750        assert_eq!(t4, t1); // 2
2751
2752        // High water mark unchanged (no new registers needed).
2753        assert_eq!(alloc.count(), 3);
2754    }
2755
2756    // ── test_coroutine_init_yield_end ───────────────────────────────────
2757    #[test]
2758    fn test_coroutine_init_yield_end() {
2759        // InitCoroutine: set yield register to body PC.
2760        let yield_reg = 1;
2761        let body_pc = 10;
2762        let mut co = CoroutineState::new(yield_reg, body_pc);
2763        assert_eq!(co.yield_reg, yield_reg);
2764        assert_eq!(co.saved_pc, body_pc);
2765        assert!(!co.exhausted);
2766
2767        // Yield: bidirectional PC swap.
2768        // Caller is at PC=5, coroutine body is at PC=10.
2769        let resume = co.yield_swap(5);
2770        assert_eq!(resume, 10); // jump to body
2771        assert_eq!(co.saved_pc, 5); // caller's PC saved
2772
2773        // Body yields back: caller at 5, body at 15.
2774        let resume2 = co.yield_swap(15);
2775        assert_eq!(resume2, 5); // back to caller
2776        assert_eq!(co.saved_pc, 15);
2777
2778        // EndCoroutine: marks exhaustion, returns to caller.
2779        let final_pc = co.end();
2780        assert_eq!(final_pc, 15); // returns saved_pc
2781        assert!(co.exhausted);
2782    }
2783
2784    // ── test_coroutine_multi_row_production ─────────────────────────────
2785    #[test]
2786    fn test_coroutine_multi_row_production() {
2787        // Simulate a CTE body producing 5 rows via Yield loop.
2788        let mut co = CoroutineState::new(1, 10); // body starts at PC=10
2789        let mut rows_consumed = 0;
2790        let caller_start_pc = 5;
2791
2792        // Caller yields to body.
2793        let mut next_pc = co.yield_swap(caller_start_pc);
2794        assert_eq!(next_pc, 10); // first entry into body
2795
2796        // Body produces rows.
2797        for row in 1..=5 {
2798            // Body "produces" a row, then yields back to caller.
2799            let body_pc = 10 + row; // body advances its PC
2800            next_pc = co.yield_swap(body_pc);
2801            // Caller resumes at its saved PC.
2802            assert_eq!(next_pc, caller_start_pc);
2803            rows_consumed += 1;
2804
2805            if row < 5 {
2806                // Caller yields back to body to get next row.
2807                next_pc = co.yield_swap(caller_start_pc);
2808                assert_eq!(next_pc, body_pc); // resume body
2809            }
2810        }
2811
2812        assert_eq!(rows_consumed, 5);
2813
2814        // Body signals exhaustion.
2815        let final_pc = co.end();
2816        assert!(co.exhausted);
2817        assert!(final_pc > 0); // valid return PC
2818    }
2819
2820    #[test]
2821    fn test_program_builder_infers_register_count_from_manual_opcode_registers() {
2822        let mut builder = ProgramBuilder::new();
2823        builder.emit_op(Opcode::Integer, 11, 3, 0, P4::None, 0);
2824        builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
2825        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2826
2827        let program = builder.finish().expect("program should build");
2828        assert_eq!(
2829            program.register_count(),
2830            3,
2831            "bytecode that writes raw registers must still allocate a large enough register file",
2832        );
2833    }
2834
2835    #[test]
2836    fn test_program_builder_infers_register_count_for_non_contiguous_comparison_operands() {
2837        let mut builder = ProgramBuilder::new();
2838        builder.emit_op(Opcode::Eq, 2, 0, 7, P4::None, 0);
2839        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2840
2841        let program = builder.finish().expect("program should build");
2842        assert_eq!(
2843            program.register_count(),
2844            7,
2845            "comparison opcodes must account for both read registers even when they are not contiguous",
2846        );
2847    }
2848
2849    #[test]
2850    fn test_program_builder_infers_register_count_for_store_p2_comparisons() {
2851        let mut builder = ProgramBuilder::new();
2852        builder.emit_op(Opcode::Eq, 2, 9, 7, P4::None, 0x20);
2853        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2854
2855        let program = builder.finish().expect("program should build");
2856        assert_eq!(
2857            program.register_count(),
2858            9,
2859            "SQLITE_STOREP2 comparisons must reserve the destination register in the pre-sized register file",
2860        );
2861    }
2862
2863    #[test]
2864    fn test_program_builder_tracks_sorter_preflight_and_runtime_bound_registers() {
2865        let mut builder = ProgramBuilder::new();
2866        builder.emit_op(
2867            Opcode::SorterOpen,
2868            0,
2869            1,
2870            8,
2871            P4::None,
2872            SORTER_OPEN_TOP_N_REGISTER,
2873        );
2874        builder.emit_op(
2875            Opcode::SorterCompare,
2876            0,
2877            2,
2878            7,
2879            P4::None,
2880            fsqlite_types::opcode::SORTER_COMPARE_TOP_N_PREFLIGHT,
2881        );
2882        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2883
2884        let program = builder.finish().expect("program should build");
2885        assert_eq!(
2886            program.register_count(),
2887            8,
2888            "runtime SorterOpen.p3 and SorterCompare.p3 must contribute to register sizing"
2889        );
2890    }
2891
2892    #[test]
2893    fn test_sorter_compare_register_spans_distinguish_preflight_consumption() {
2894        let ordinary = VdbeOp {
2895            opcode: Opcode::SorterCompare,
2896            p1: 0,
2897            p2: 1,
2898            p3: 7,
2899            p4: P4::None,
2900            p5: 0,
2901        };
2902        let ordinary_spans = opcode_register_spans(&ordinary);
2903        let preflight = VdbeOp {
2904            p5: SORTER_COMPARE_TOP_N_PREFLIGHT,
2905            ..ordinary
2906        };
2907        let preflight_spans = opcode_register_spans(&preflight);
2908
2909        assert_eq!(
2910            ordinary_spans,
2911            OpcodeRegisterSpans {
2912                read_start: 7,
2913                read_len: 1,
2914                write_start: -1,
2915                write_len: 0,
2916            },
2917            "ordinary SorterCompare must leave P3 read-only"
2918        );
2919        assert_eq!(
2920            preflight_spans,
2921            OpcodeRegisterSpans {
2922                read_start: 7,
2923                read_len: 1,
2924                write_start: 7,
2925                write_len: 1,
2926            },
2927            "top-N preflight must model P3 as consumed"
2928        );
2929    }
2930
2931    #[test]
2932    fn test_program_builder_does_not_treat_immediate_sorter_bound_as_register() {
2933        let mut builder = ProgramBuilder::new();
2934        builder.emit_op(Opcode::SorterOpen, 0, 1, 500, P4::None, 0);
2935        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2936
2937        let program = builder.finish().expect("program should build");
2938        assert_eq!(
2939            program.register_count(),
2940            0,
2941            "legacy immediate SorterOpen.p3 is not a register operand"
2942        );
2943    }
2944
2945    #[test]
2946    fn test_program_builder_rejects_out_of_bounds_goto_target() {
2947        let mut builder = ProgramBuilder::new();
2948        builder.emit_op(Opcode::Goto, 0, 99, 0, P4::None, 0);
2949        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2950
2951        let err = builder.finish().expect_err("invalid jump target must fail");
2952        match err {
2953            FrankenError::Internal(message) => {
2954                assert!(message.contains("Goto"));
2955                assert!(message.contains("p2"));
2956            }
2957            other => assert!(
2958                matches!(other, FrankenError::Internal(_)),
2959                "expected internal verifier error, got {other:?}"
2960            ),
2961        }
2962    }
2963
2964    #[test]
2965    fn test_program_builder_rejects_out_of_bounds_jump_branch_target() {
2966        let mut builder = ProgramBuilder::new();
2967        builder.emit_op(Opcode::Jump, 0, 1, 42, P4::None, 0);
2968        builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2969
2970        let err = builder
2971            .finish()
2972            .expect_err("invalid branch target must fail");
2973        match err {
2974            FrankenError::Internal(message) => {
2975                assert!(message.contains("Jump"));
2976                assert!(message.contains("p3"));
2977            }
2978            other => assert!(
2979                matches!(other, FrankenError::Internal(_)),
2980                "expected internal verifier error, got {other:?}"
2981            ),
2982        }
2983    }
2984
2985    // ── test_all_opcode_dispatch_coverage ────────────────────────────────
2986    #[test]
2987    fn test_all_opcode_dispatch_coverage() {
2988        // Every assigned Opcode enum byte has a valid name and can be
2989        // constructed from its byte value. This ensures no gaps in the enum.
2990        for byte in 1..Opcode::COUNT as u8 {
2991            let opcode = Opcode::from_byte(byte);
2992            assert!(
2993                opcode.is_some(),
2994                "Opcode::from_byte({byte}) returned None — gap in opcode enum"
2995            );
2996            let opcode = opcode.unwrap();
2997            let name = opcode.name();
2998            assert!(!name.is_empty(), "opcode {byte} has empty name");
2999        }
3000        assert_eq!(Opcode::from_byte(Opcode::COUNT as u8), None);
3001    }
3002
3003    // ── test_p5_flags_u16_range ─────────────────────────────────────────
3004    #[test]
3005    fn test_p5_flags_u16_range() {
3006        // Confirm p5 is u16 and accepts values above 0xFF.
3007        let op = VdbeOp {
3008            opcode: Opcode::Eq,
3009            p1: 1,
3010            p2: 5,
3011            p3: 2,
3012            p4: P4::None,
3013            p5: 0x1FF, // 511, exceeds u8 range
3014        };
3015        assert_eq!(op.p5, 0x1FF);
3016        assert!(op.p5 > 255);
3017
3018        let op2 = VdbeOp {
3019            opcode: Opcode::Noop,
3020            p1: 0,
3021            p2: 0,
3022            p3: 0,
3023            p4: P4::None,
3024            p5: u16::MAX,
3025        };
3026        assert_eq!(op2.p5, 65535);
3027    }
3028
3029    // ── test_program_builder_basic ──────────────────────────────────────
3030    #[test]
3031    fn test_program_builder_basic() {
3032        let mut b = ProgramBuilder::new();
3033
3034        // Build: Init -> Integer 42 into r1 -> ResultRow r1,1 -> Halt
3035        let end_label = b.emit_label();
3036        b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
3037        let r1 = b.alloc_reg();
3038        assert_eq!(r1, 1);
3039        b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
3040        b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
3041        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3042        b.resolve_label(end_label);
3043
3044        let prog = b.finish().unwrap();
3045        assert_eq!(prog.len(), 4);
3046        assert_eq!(prog.register_count(), 1);
3047        assert_eq!(prog.max_bind_parameter_index().unwrap(), 0);
3048
3049        // The Init instruction's p2 should point to address 4 (after Halt).
3050        assert_eq!(prog.get(0).unwrap().opcode, Opcode::Init);
3051        assert_eq!(prog.get(0).unwrap().p2, 4);
3052    }
3053
3054    #[test]
3055    fn test_program_precomputes_max_bind_parameter_index() {
3056        let mut b = ProgramBuilder::new();
3057        b.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
3058        b.emit_op(Opcode::Variable, 4, 2, 0, P4::None, 0);
3059        b.emit_op(Opcode::Variable, 2, 3, 0, P4::None, 0);
3060        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3061        let prog = b.finish().unwrap();
3062        assert_eq!(prog.max_bind_parameter_index(), Ok(4));
3063    }
3064
3065    #[test]
3066    fn test_program_tracks_invalid_bind_parameter_index() {
3067        let mut b = ProgramBuilder::new();
3068        b.emit_op(Opcode::Variable, 0, 1, 0, P4::None, 0);
3069        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3070        let prog = b.finish().unwrap();
3071        assert_eq!(prog.max_bind_parameter_index(), Err(0));
3072    }
3073
3074    #[test]
3075    fn test_program_storage_only_hot_path_does_not_require_attached_memdb() {
3076        let mut b = ProgramBuilder::new();
3077        let end = b.emit_label();
3078        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3079        b.emit_op(Opcode::OpenWrite, 0, 256, 0, P4::Int(1), 0);
3080        b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
3081        b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
3082        b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
3083        b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
3084        b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
3085        b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
3086        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3087        b.resolve_label(end);
3088
3089        // ubs:ignore - VDBE ProgramBuilder finalization in a unit test; no token or randomness.
3090        let prog = match b.finish() {
3091            Ok(prog) => prog,
3092            Err(err) => {
3093                assert!(test_failure(), "program should build: {err}");
3094                return;
3095            }
3096        };
3097        assert!(
3098            !prog.requires_attached_memdb(),
3099            "storage-only table hot paths should not force a MemDatabase handoff"
3100        );
3101    }
3102
3103    #[test]
3104    fn test_program_with_ephemeral_cursor_requires_attached_memdb() {
3105        let mut b = ProgramBuilder::new();
3106        let end = b.emit_label();
3107        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3108        b.emit_op(Opcode::OpenEphemeral, 0, 1, 0, P4::None, 0);
3109        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3110        b.resolve_label(end);
3111
3112        let prog = b.finish().expect("program should build");
3113        assert!(
3114            prog.requires_attached_memdb(),
3115            "ephemeral table programs still depend on the attached MemDatabase"
3116        );
3117    }
3118
3119    #[test]
3120    fn test_program_with_sorter_cursor_does_not_require_attached_memdb() -> Result<()> {
3121        let mut b = ProgramBuilder::new();
3122        let end = b.emit_label();
3123        b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3124        b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::Str("+".to_owned()), 0);
3125        b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
3126        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3127        b.resolve_label(end);
3128
3129        let prog = b.finish()?;
3130        assert!(
3131            !prog.requires_attached_memdb(),
3132            "sorter-backed temp/exchange state is owned by VDBE and should not force a MemDatabase handoff"
3133        );
3134        Ok(())
3135    }
3136
3137    #[test]
3138    fn test_program_builder_accumulates_table_index_meta_by_table_cursor() {
3139        use fsqlite_types::opcode::IndexCursorMeta;
3140
3141        let mut b = ProgramBuilder::new();
3142        b.register_table_indexes(
3143            3,
3144            vec![IndexCursorMeta {
3145                cursor_id: 4,
3146                column_indices: vec![0, 2],
3147            }],
3148        );
3149        b.register_table_indexes(
3150            3,
3151            vec![IndexCursorMeta {
3152                cursor_id: 5,
3153                column_indices: vec![1],
3154            }],
3155        );
3156
3157        let prog = b.finish().expect("program should build");
3158        let metas = prog
3159            .table_index_meta()
3160            .get(&3)
3161            .expect("table cursor metadata should be present");
3162        assert_eq!(metas.len(), 2);
3163        assert_eq!(metas[0].cursor_id, 4);
3164        assert_eq!(metas[0].column_indices, vec![0, 2]);
3165        assert_eq!(metas[1].cursor_id, 5);
3166        assert_eq!(metas[1].column_indices, vec![1]);
3167    }
3168
3169    // ── test_disassemble ────────────────────────────────────────────────
3170    #[test]
3171    fn test_disassemble() {
3172        let mut b = ProgramBuilder::new();
3173        b.emit_op(Opcode::Init, 0, 2, 0, P4::None, 0);
3174        b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
3175        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3176        let prog = b.finish().unwrap();
3177
3178        let asm = prog.disassemble();
3179        assert!(asm.contains("Init"));
3180        assert!(asm.contains("Integer"));
3181        assert!(asm.contains("Halt"));
3182        assert!(asm.contains("42")); // p1 of Integer
3183    }
3184
3185    // ── test_key_info ───────────────────────────────────────────────────
3186    #[test]
3187    fn test_key_info() {
3188        let ki = KeyInfo {
3189            num_fields: 3,
3190            collations: vec![
3191                "BINARY".to_owned(),
3192                "NOCASE".to_owned(),
3193                "BINARY".to_owned(),
3194            ],
3195            sort_orders: vec![SortOrder::Asc, SortOrder::Desc, SortOrder::Asc],
3196        };
3197        assert_eq!(ki.num_fields, 3);
3198        assert_eq!(ki.collations.len(), 3);
3199        assert_eq!(ki.sort_orders[1], SortOrder::Desc);
3200    }
3201
3202    // ── test_label_already_resolved ─────────────────────────────────────
3203    #[test]
3204    fn test_label_already_resolved() {
3205        // If a label is resolved before a jump references it, the jump
3206        // should be patched immediately.
3207        let mut b = ProgramBuilder::new();
3208        let label = b.emit_label();
3209        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3210        b.resolve_label(label); // resolved to address 1
3211        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3212
3213        // Now emit a jump referencing the already-resolved label.
3214        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3215        // p2 should already be patched to 1.
3216        assert_eq!(b.op_at(jump_addr).unwrap().p2, 1);
3217
3218        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3219
3220        let prog = b.finish().unwrap();
3221        assert_eq!(prog.len(), 4);
3222    }
3223
3224    // ── test_builder_register_via_builder ────────────────────────────────
3225    #[test]
3226    fn test_builder_register_via_builder() {
3227        let mut b = ProgramBuilder::new();
3228        let r1 = b.alloc_reg();
3229        let r2 = b.alloc_reg();
3230        let block = b.alloc_regs(4);
3231        assert_eq!(r1, 1);
3232        assert_eq!(r2, 2);
3233        assert_eq!(block, 3);
3234        assert_eq!(b.register_count(), 6);
3235
3236        // Temp allocation.
3237        let t1 = b.alloc_temp();
3238        assert_eq!(t1, 7);
3239        b.free_temp(t1);
3240        let t2 = b.alloc_temp();
3241        assert_eq!(t2, t1); // reused
3242    }
3243
3244    // ── test_resolve_label_to_specific_address ──────────────────────────
3245    #[test]
3246    fn test_resolve_label_to_specific_address() {
3247        let mut b = ProgramBuilder::new();
3248        let label = b.emit_label();
3249        let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3250        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3251        b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3252
3253        // Resolve to a specific address (not current).
3254        b.resolve_label_to(label, 42);
3255        assert_eq!(b.op_at(jump_addr).unwrap().p2, 42);
3256    }
3257
3258    // ── test_empty_program_finishes ─────────────────────────────────────
3259    #[test]
3260    fn test_empty_program_finishes() {
3261        let b = ProgramBuilder::new();
3262        let prog = b.finish().unwrap();
3263        assert!(prog.is_empty());
3264        assert_eq!(prog.register_count(), 0);
3265    }
3266
3267    // ── test_unreferenced_unresolved_label_ok ───────────────────────────
3268    #[test]
3269    fn test_unreferenced_unresolved_label_ok() {
3270        // A label that was created but never referenced or resolved should
3271        // not cause an error (it's unused, not a dangling reference).
3272        let mut b = ProgramBuilder::new();
3273        let _label = b.emit_label();
3274        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3275        let prog = b.finish().unwrap();
3276        assert_eq!(prog.len(), 1);
3277    }
3278
3279    // ── PRAGMA handling (bd-iwu.5) ───────────────────────────────────────
3280
3281    #[cfg(not(target_arch = "wasm32"))]
3282    use std::fs;
3283
3284    use fsqlite_ast::Statement;
3285    use fsqlite_error::FrankenError;
3286    use fsqlite_mvcc::{BeginKind, MvccError, TransactionManager};
3287    use fsqlite_parser::Parser;
3288    use fsqlite_types::{CommitSeq, ObjectId, Oti, PageData, PageNumber, PageSize};
3289    use fsqlite_wal::{
3290        DEFAULT_RAPTORQ_REPAIR_SYMBOLS, WalFecGroupMeta, WalFecGroupMetaInit, WalFecGroupRecord,
3291        WalFecRecoveryOutcome, WalFrameCandidate, WalSalts, append_wal_fec_group,
3292        build_source_page_hashes, generate_wal_fec_repair_symbols,
3293        recover_wal_fec_group_with_decoder, scan_wal_fec,
3294    };
3295    #[cfg(not(target_arch = "wasm32"))]
3296    use tempfile::tempdir;
3297
3298    fn parse_pragma(sql: &str) -> std::result::Result<fsqlite_ast::PragmaStatement, String> {
3299        let mut p = Parser::from_sql(sql);
3300        let stmt = p.parse_statement().expect("parse statement");
3301        match stmt {
3302            Statement::Pragma(p) => Ok(p),
3303            other => Err(format!("expected PRAGMA, got: {other:?}")),
3304        }
3305    }
3306
3307    fn test_page(first_byte: u8) -> PageData {
3308        let mut page = PageData::zeroed(PageSize::DEFAULT);
3309        page.as_bytes_mut()[0] = first_byte;
3310        page
3311    }
3312
3313    fn make_source_pages(seed: u8, k_source: u32) -> Vec<Vec<u8>> {
3314        let page_len = usize::try_from(PageSize::DEFAULT.get()).expect("page size fits usize");
3315        (0..k_source)
3316            .map(|idx| {
3317                let idx_u8 = u8::try_from(idx).expect("test k_source fits u8");
3318                let mut page = vec![seed.wrapping_add(idx_u8); page_len];
3319                page[0] = idx_u8;
3320                page
3321            })
3322            .collect()
3323    }
3324
3325    fn make_wal_fec_group(
3326        start_frame_no: u32,
3327        r_repair: u8,
3328        seed: u8,
3329    ) -> (WalFecGroupRecord, Vec<Vec<u8>>) {
3330        let k_source = 5_u32;
3331        let source_pages = make_source_pages(seed, k_source);
3332        let page_size = PageSize::DEFAULT.get();
3333        let source_hashes = build_source_page_hashes(&source_pages);
3334        let page_numbers = (0..k_source).map(|i| 10 + i).collect::<Vec<_>>();
3335        let oti = Oti {
3336            f: u64::from(k_source) * u64::from(page_size),
3337            al: 1,
3338            t: page_size,
3339            z: 1,
3340            n: 1,
3341        };
3342        let meta = WalFecGroupMeta::from_init(WalFecGroupMetaInit {
3343            wal_salt1: 0xA11C_E001,
3344            wal_salt2: 0xA11C_E002,
3345            start_frame_no,
3346            end_frame_no: start_frame_no + (k_source - 1),
3347            db_size_pages: 256,
3348            page_size,
3349            k_source,
3350            r_repair: u32::from(r_repair),
3351            oti,
3352            object_id: ObjectId::from_bytes([seed; 16]),
3353            page_numbers,
3354            source_page_xxh3_128: source_hashes,
3355        })
3356        .expect("meta");
3357        let repair_symbols =
3358            generate_wal_fec_repair_symbols(&meta, &source_pages).expect("symbols");
3359        (
3360            WalFecGroupRecord::new(meta, repair_symbols).expect("group"),
3361            source_pages,
3362        )
3363    }
3364
3365    #[test]
3366    fn test_pragma_serializable_query_returns_current_setting() {
3367        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3368
3369        let stmt = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3370        let out = pragma::apply(&mut mgr, &stmt).unwrap();
3371        assert_eq!(out, pragma::PragmaOutput::Bool(true));
3372    }
3373
3374    #[test]
3375    fn test_connection_pragma_differential_views_default_query_returns_false() {
3376        let mut state = pragma::ConnectionPragmaState::default();
3377
3378        let stmt = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3379        let out = pragma::apply_connection_pragma(&mut state, &stmt).expect("query pragma");
3380        assert_eq!(out, pragma::PragmaOutput::Bool(false));
3381    }
3382
3383    #[test]
3384    fn test_connection_pragma_differential_views_set_and_query_across_aliases() {
3385        let mut state = pragma::ConnectionPragmaState::default();
3386
3387        let set_on = parse_pragma("PRAGMA fsqlite.differential_views = ON").expect("parse pragma");
3388        assert_eq!(
3389            pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3390            pragma::PragmaOutput::Bool(true)
3391        );
3392        assert!(state.differential_views.is_enabled());
3393
3394        let query = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3395        assert_eq!(
3396            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3397            pragma::PragmaOutput::Bool(true)
3398        );
3399    }
3400
3401    #[test]
3402    fn test_connection_pragma_differential_views_rejects_non_boolean_values() {
3403        let mut state = pragma::ConnectionPragmaState::default();
3404
3405        let stmt = parse_pragma("PRAGMA fsqlite_differential_views = 2").expect("parse pragma");
3406        assert!(matches!(
3407            pragma::apply_connection_pragma(&mut state, &stmt),
3408            Err(FrankenError::TypeMismatch { .. })
3409        ));
3410    }
3411
3412    #[test]
3413    fn test_connection_pragma_query_only_set_and_query() {
3414        let mut state = pragma::ConnectionPragmaState::default();
3415
3416        let query = parse_pragma("PRAGMA query_only").expect("parse pragma");
3417        assert_eq!(
3418            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3419            pragma::PragmaOutput::Int(0)
3420        );
3421
3422        let set_on = parse_pragma("PRAGMA query_only = ON").expect("parse pragma");
3423        assert_eq!(
3424            pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3425            pragma::PragmaOutput::Int(1)
3426        );
3427        assert!(state.query_only);
3428
3429        assert_eq!(
3430            pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3431            pragma::PragmaOutput::Int(1)
3432        );
3433    }
3434
3435    #[test]
3436    fn test_connection_pragma_query_only_rejects_non_boolean_values() {
3437        let mut state = pragma::ConnectionPragmaState::default();
3438
3439        let stmt = parse_pragma("PRAGMA query_only = 2").expect("parse pragma");
3440        assert!(matches!(
3441            pragma::apply_connection_pragma(&mut state, &stmt),
3442            Err(FrankenError::TypeMismatch { .. })
3443        ));
3444    }
3445
3446    fn apply_sql(state: &mut pragma::ConnectionPragmaState, sql: &str) -> pragma::PragmaOutput {
3447        let stmt = parse_pragma(sql).expect("parse pragma");
3448        pragma::apply_connection_pragma(state, &stmt).expect("apply pragma")
3449    }
3450
3451    #[test]
3452    fn test_connection_pragma_boolean_readbacks() {
3453        // GH #282/#278/#262/#281/#283: set/readback surfaces for boolean pragmas.
3454        use pragma::PragmaOutput::Bool;
3455        let mut state = pragma::ConnectionPragmaState::default();
3456
3457        // trusted_schema and automatic_index default ON; the rest default OFF.
3458        assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(true));
3459        assert_eq!(apply_sql(&mut state, "PRAGMA automatic_index"), Bool(true));
3460        assert_eq!(
3461            apply_sql(&mut state, "PRAGMA read_uncommitted"),
3462            Bool(false)
3463        );
3464        assert_eq!(apply_sql(&mut state, "PRAGMA cell_size_check"), Bool(false));
3465        assert_eq!(
3466            apply_sql(&mut state, "PRAGMA checkpoint_fullfsync"),
3467            Bool(false)
3468        );
3469
3470        // Set then read back each toggle.
3471        assert_eq!(
3472            apply_sql(&mut state, "PRAGMA trusted_schema = OFF"),
3473            Bool(false)
3474        );
3475        assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(false));
3476        assert_eq!(
3477            apply_sql(&mut state, "PRAGMA read_uncommitted = ON"),
3478            Bool(true)
3479        );
3480        assert_eq!(
3481            apply_sql(&mut state, "PRAGMA cell_size_check = 1"),
3482            Bool(true)
3483        );
3484        assert_eq!(
3485            apply_sql(&mut state, "PRAGMA checkpoint_fullfsync = TRUE"),
3486            Bool(true)
3487        );
3488        assert_eq!(
3489            apply_sql(&mut state, "PRAGMA automatic_index = 0"),
3490            Bool(false)
3491        );
3492    }
3493
3494    #[test]
3495    fn test_connection_pragma_locking_mode_readback() {
3496        // GH #273: locking_mode echoes normal/exclusive (lowercased).
3497        use pragma::PragmaOutput::Text;
3498        let mut state = pragma::ConnectionPragmaState::default();
3499        assert_eq!(
3500            apply_sql(&mut state, "PRAGMA locking_mode"),
3501            Text("normal".to_owned())
3502        );
3503        assert_eq!(
3504            apply_sql(&mut state, "PRAGMA locking_mode = EXCLUSIVE"),
3505            Text("exclusive".to_owned())
3506        );
3507        assert_eq!(
3508            apply_sql(&mut state, "PRAGMA locking_mode"),
3509            Text("exclusive".to_owned())
3510        );
3511        // An unrecognized value is ignored; the current mode is echoed unchanged.
3512        assert_eq!(
3513            apply_sql(&mut state, "PRAGMA locking_mode = bogus"),
3514            Text("exclusive".to_owned())
3515        );
3516    }
3517
3518    #[test]
3519    fn test_connection_pragma_secure_delete_tristate() {
3520        // GH #277: secure_delete is a tri-state integer (0=OFF, 1=ON, 2=FAST).
3521        use pragma::PragmaOutput::Int;
3522        let mut state = pragma::ConnectionPragmaState::default();
3523        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete"), Int(0));
3524        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = ON"), Int(1));
3525        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = FAST"), Int(2));
3526        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = OFF"), Int(0));
3527        assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = 2"), Int(2));
3528    }
3529
3530    #[test]
3531    fn test_connection_pragma_threads_readback() {
3532        // GH #279: threads reports the stored limit; a set clamps to 8; a
3533        // negative argument leaves the limit unchanged.
3534        use pragma::PragmaOutput::Int;
3535        let mut state = pragma::ConnectionPragmaState::default();
3536        assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(0));
3537        assert_eq!(apply_sql(&mut state, "PRAGMA threads = 4"), Int(4));
3538        assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(4));
3539        // Clamps to the stock maximum of 8.
3540        assert_eq!(apply_sql(&mut state, "PRAGMA threads = 100"), Int(8));
3541        // A negative argument leaves the current limit unchanged.
3542        assert_eq!(apply_sql(&mut state, "PRAGMA threads = -1"), Int(8));
3543    }
3544
3545    #[test]
3546    fn test_pragma_serializable_set_and_query() {
3547        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3548
3549        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3550        assert_eq!(
3551            pragma::apply(&mut mgr, &set_off).unwrap(),
3552            pragma::PragmaOutput::Bool(false)
3553        );
3554
3555        let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3556        assert_eq!(
3557            pragma::apply(&mut mgr, &query).unwrap(),
3558            pragma::PragmaOutput::Bool(false)
3559        );
3560    }
3561
3562    #[test]
3563    fn test_pragma_scope_per_connection_via_handler() {
3564        let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3565        let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3566
3567        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3568        let _ = pragma::apply(&mut conn_a, &set_off).unwrap();
3569
3570        let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3571        assert_eq!(
3572            pragma::apply(&mut conn_a, &query).unwrap(),
3573            pragma::PragmaOutput::Bool(false)
3574        );
3575        assert_eq!(
3576            pragma::apply(&mut conn_b, &query).unwrap(),
3577            pragma::PragmaOutput::Bool(true)
3578        );
3579    }
3580
3581    #[test]
3582    fn test_pragma_not_retroactive_to_active_txn_via_handler() {
3583        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3584
3585        let mut txn = mgr.begin(BeginKind::Concurrent).unwrap();
3586        mgr.write_page(&mut txn, PageNumber::new(1).unwrap(), test_page(0x01))
3587            .unwrap();
3588        txn.has_in_rw = true;
3589        txn.has_out_rw = true;
3590        assert!(txn.has_dangerous_structure());
3591
3592        // Flip OFF mid-txn; this must not affect the already-begun transaction.
3593        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3594        let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3595
3596        assert_eq!(
3597            mgr.commit(&mut txn).unwrap_err(),
3598            MvccError::BusySnapshot,
3599            "PRAGMA change must not be retroactive to an active txn"
3600        );
3601    }
3602
3603    #[test]
3604    fn test_e2e_serializable_pragma_switch_changes_behavior() {
3605        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3606
3607        // Run workload with serializable=ON: must abort on dangerous structure.
3608        let set_on = parse_pragma("PRAGMA fsqlite.serializable = ON").expect("parse pragma");
3609        let _ = pragma::apply(&mut mgr, &set_on).unwrap();
3610
3611        let mut txn_on = mgr.begin(BeginKind::Concurrent).unwrap();
3612        mgr.write_page(&mut txn_on, PageNumber::new(1).unwrap(), test_page(0x10))
3613            .unwrap();
3614        txn_on.has_in_rw = true;
3615        txn_on.has_out_rw = true;
3616        assert_eq!(
3617            mgr.commit(&mut txn_on).unwrap_err(),
3618            MvccError::BusySnapshot,
3619            "serializable=ON must enforce SSI (abort)"
3620        );
3621
3622        // Run the same workload with serializable=OFF: must commit (plain SI).
3623        let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3624        let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3625
3626        let mut txn_off = mgr.begin(BeginKind::Concurrent).unwrap();
3627        mgr.write_page(&mut txn_off, PageNumber::new(2).unwrap(), test_page(0x20))
3628            .unwrap();
3629        txn_off.has_in_rw = true;
3630        txn_off.has_out_rw = true;
3631
3632        let seq = mgr.commit(&mut txn_off).unwrap();
3633        assert!(
3634            seq > CommitSeq::ZERO,
3635            "serializable=OFF must allow write skew"
3636        );
3637    }
3638
3639    #[test]
3640    fn test_pragma_raptorq_repair_symbols_default_query() {
3641        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3642        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3643        assert_eq!(
3644            pragma::apply(&mut mgr, &query).expect("query pragma"),
3645            pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3646        );
3647    }
3648
3649    #[cfg(not(target_arch = "wasm32"))]
3650    #[test]
3651    fn test_bd_1hi_12_unit_compliance_gate() {
3652        let dir = tempdir().expect("tempdir");
3653        let sidecar = dir.path().join("unit.wal-fec");
3654        let db_path = dir.path().join("unit.db");
3655        fs::write(&db_path, vec![0_u8; 100]).expect("seed db header");
3656
3657        let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3658        let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3659
3660        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3661        assert_eq!(
3662            pragma::apply_with_sidecar(&mut conn_a, &query, Some(&sidecar)).expect("query default"),
3663            pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3664        );
3665
3666        let set_max = parse_pragma("PRAGMA raptorq_repair_symbols = 255").expect("parse set max");
3667        assert_eq!(
3668            pragma::apply_with_sidecar(&mut conn_a, &set_max, Some(&sidecar)).expect("set max"),
3669            pragma::PragmaOutput::Int(255)
3670        );
3671
3672        let set_too_high =
3673            parse_pragma("PRAGMA raptorq_repair_symbols = 256").expect("parse set too high");
3674        assert!(matches!(
3675            pragma::apply_with_sidecar(&mut conn_a, &set_too_high, Some(&sidecar)),
3676            Err(FrankenError::OutOfRange { .. })
3677        ));
3678
3679        let set_negative =
3680            parse_pragma("PRAGMA raptorq_repair_symbols = -1").expect("parse set negative");
3681        assert!(matches!(
3682            pragma::apply_with_sidecar(&mut conn_a, &set_negative, Some(&sidecar)),
3683            Err(FrankenError::OutOfRange { .. })
3684        ));
3685
3686        let set_non_integer =
3687            parse_pragma("PRAGMA raptorq_repair_symbols = ON").expect("parse set non-integer");
3688        assert!(matches!(
3689            pragma::apply_with_sidecar(&mut conn_a, &set_non_integer, Some(&sidecar)),
3690            Err(FrankenError::TypeMismatch { .. })
3691        ));
3692
3693        let query_new_conn = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3694        assert_eq!(
3695            pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3696                .expect("query persisted value"),
3697            pragma::PragmaOutput::Int(255)
3698        );
3699
3700        let set_shared = parse_pragma("PRAGMA raptorq_repair_symbols = 7").expect("parse shared");
3701        let _ = pragma::apply_with_sidecar(&mut conn_a, &set_shared, Some(&sidecar))
3702            .expect("persist shared setting");
3703        assert_eq!(
3704            pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3705                .expect("cross-connection visibility"),
3706            pragma::PragmaOutput::Int(7)
3707        );
3708
3709        let db_bytes = fs::read(&db_path).expect("read db header");
3710        assert!(
3711            db_bytes[72..92].iter().all(|&byte| byte == 0),
3712            "sqlite header reserved bytes must remain untouched"
3713        );
3714    }
3715
3716    #[cfg(not(target_arch = "wasm32"))]
3717    #[test]
3718    fn prop_bd_1hi_12_structure_compliance() {
3719        let dir = tempdir().expect("tempdir");
3720        let sidecar = dir.path().join("property.wal-fec");
3721        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3722        let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3723
3724        for value in 0_u16..=255_u16 {
3725            let sql = format!("PRAGMA raptorq_repair_symbols = {value}");
3726            let set_stmt = parse_pragma(&sql).expect("parse set statement");
3727            assert_eq!(
3728                pragma::apply_with_sidecar(&mut mgr, &set_stmt, Some(&sidecar)).expect("set value"),
3729                pragma::PragmaOutput::Int(i64::from(value))
3730            );
3731            assert_eq!(
3732                pragma::apply_with_sidecar(&mut mgr, &query, Some(&sidecar)).expect("query value"),
3733                pragma::PragmaOutput::Int(i64::from(value))
3734            );
3735        }
3736    }
3737
3738    #[cfg(not(target_arch = "wasm32"))]
3739    #[test]
3740    #[allow(clippy::too_many_lines)]
3741    fn test_e2e_bd_1hi_12_compliance() {
3742        let dir = tempdir().expect("tempdir");
3743        let sidecar = dir.path().join("e2e.wal-fec");
3744        let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3745
3746        let set_zero = parse_pragma("PRAGMA raptorq_repair_symbols = 0").expect("parse set 0");
3747        let _ = pragma::apply_with_sidecar(&mut mgr, &set_zero, Some(&sidecar)).expect("set 0");
3748        if mgr.raptorq_repair_symbols() > 0 {
3749            let (group, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x10);
3750            append_wal_fec_group(&sidecar, &group).expect("append group");
3751        }
3752        let after_zero = scan_wal_fec(&sidecar).expect("scan after zero");
3753        assert!(
3754            after_zero.groups.is_empty(),
3755            "N=0 must produce no .wal-fec groups for new commits"
3756        );
3757
3758        let set_one = parse_pragma("PRAGMA raptorq_repair_symbols = 1").expect("parse set 1");
3759        let _ = pragma::apply_with_sidecar(&mut mgr, &set_one, Some(&sidecar)).expect("set 1");
3760        let (group_r1, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x11);
3761        append_wal_fec_group(&sidecar, &group_r1).expect("append r=1 group");
3762
3763        let set_two = parse_pragma("PRAGMA raptorq_repair_symbols = 2").expect("parse set 2");
3764        let _ = pragma::apply_with_sidecar(&mut mgr, &set_two, Some(&sidecar)).expect("set 2");
3765        let (group_r2, _) = make_wal_fec_group(6, mgr.raptorq_repair_symbols(), 0x22);
3766        append_wal_fec_group(&sidecar, &group_r2).expect("append r=2 group");
3767
3768        let set_four = parse_pragma("PRAGMA raptorq_repair_symbols = 4").expect("parse set 4");
3769        let _ = pragma::apply_with_sidecar(&mut mgr, &set_four, Some(&sidecar)).expect("set 4");
3770        let (group_r4, source_pages_r4) =
3771            make_wal_fec_group(11, mgr.raptorq_repair_symbols(), 0x33);
3772        append_wal_fec_group(&sidecar, &group_r4).expect("append r=4 group");
3773
3774        let scan = scan_wal_fec(&sidecar).expect("scan sidecar");
3775        assert_eq!(scan.groups.len(), 3);
3776        assert_eq!(scan.groups[0].repair_symbols.len(), 1);
3777        assert_eq!(scan.groups[1].repair_symbols.len(), 2);
3778        assert_eq!(scan.groups[2].repair_symbols.len(), 4);
3779        assert_eq!(scan.groups[1].meta.r_repair, 2);
3780        assert_eq!(scan.groups[2].meta.r_repair, 4);
3781
3782        let group_id = group_r4.meta.group_id();
3783        let wal_salts = WalSalts {
3784            salt1: group_r4.meta.wal_salt1,
3785            salt2: group_r4.meta.wal_salt2,
3786        };
3787        let k_source = usize::try_from(group_r4.meta.k_source).expect("k fits usize");
3788
3789        let mut corrupt_three_frames = Vec::new();
3790        for (idx, page) in source_pages_r4.iter().enumerate() {
3791            let mut payload = page.clone();
3792            if idx < 3 {
3793                payload[0] ^= 0xFF;
3794            }
3795            corrupt_three_frames.push(WalFrameCandidate {
3796                frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3797                page_data: payload,
3798            });
3799        }
3800        let expected_pages = source_pages_r4.clone();
3801        let recovered = recover_wal_fec_group_with_decoder(
3802            &sidecar,
3803            group_id,
3804            wal_salts,
3805            group_r4.meta.start_frame_no,
3806            &corrupt_three_frames,
3807            move |meta: &WalFecGroupMeta, symbols| {
3808                if symbols.len() < usize::try_from(meta.k_source).expect("k fits usize") {
3809                    return Err(FrankenError::WalCorrupt {
3810                        detail: "insufficient symbols".to_owned(),
3811                    });
3812                }
3813                Ok(expected_pages.clone())
3814            },
3815        )
3816        .expect("recover with <=R corruption");
3817        assert!(
3818            matches!(recovered, WalFecRecoveryOutcome::Recovered(_)),
3819            "expected recovered outcome"
3820        );
3821        let WalFecRecoveryOutcome::Recovered(group) = recovered else {
3822            unreachable!("asserted recovered outcome above");
3823        };
3824        assert_eq!(group.recovered_pages.len(), k_source);
3825
3826        let mut corrupt_five_frames = Vec::new();
3827        for (idx, page) in source_pages_r4.iter().enumerate() {
3828            let mut payload = page.clone();
3829            payload[0] ^= 0x55;
3830            corrupt_five_frames.push(WalFrameCandidate {
3831                frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3832                page_data: payload,
3833            });
3834        }
3835        let truncated = recover_wal_fec_group_with_decoder(
3836            &sidecar,
3837            group_id,
3838            wal_salts,
3839            group_r4.meta.start_frame_no,
3840            &corrupt_five_frames,
3841            |_meta: &WalFecGroupMeta, _symbols| {
3842                Err(FrankenError::WalCorrupt {
3843                    detail: "decoder should not be able to recover".to_owned(),
3844                })
3845            },
3846        )
3847        .expect("recover with >R corruption");
3848        assert!(matches!(
3849            truncated,
3850            WalFecRecoveryOutcome::TruncateBeforeGroup { .. }
3851        ));
3852    }
3853}