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