1#![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
42pub const FUNCTION_SCHEMA_CONTEXT_MASK: i32 = 0x6000_0000;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[repr(i32)]
56pub enum SchemaEvaluationContext {
57 Index = 0x2000_0000,
59 GeneratedColumn = 0x4000_0000,
61 CheckConstraint = 0x6000_0000,
63}
64
65impl SchemaEvaluationContext {
66 #[must_use]
68 pub const fn function_p1_bits(self) -> i32 {
69 self as i32
70 }
71
72 #[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#[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 Opcode::FusedLiteralResultRow => {
303 let (start, len) = register_range(op.p2, 1);
304 (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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
395pub struct Label(usize);
396
397#[derive(Debug)]
399enum LabelState {
400 Unresolved(Vec<usize>),
403 Resolved(i32),
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub enum SortOrder {
412 Asc,
414 Desc,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct KeyInfo {
426 pub num_fields: u16,
428 pub collations: Vec<String>,
430 pub sort_orders: Vec<SortOrder>,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct CoroutineState {
443 pub yield_reg: i32,
445 pub saved_pc: i32,
447 pub exhausted: bool,
449}
450
451impl CoroutineState {
452 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 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 pub fn end(&mut self) -> i32 {
476 self.exhausted = true;
477 self.saved_pc
478 }
479}
480
481#[derive(Debug)]
490pub struct RegisterAllocator {
491 next_reg: i32,
493 temp_pool: Vec<i32>,
495}
496
497impl RegisterAllocator {
498 pub fn new() -> Self {
500 Self {
501 next_reg: 1,
502 temp_pool: Vec::new(),
503 }
504 }
505
506 pub fn alloc_reg(&mut self) -> i32 {
508 let reg = self.next_reg;
509 self.next_reg += 1;
510 reg
511 }
512
513 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 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 pub fn free_temp(&mut self, reg: i32) {
533 self.temp_pool.push(reg);
534 }
535
536 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#[derive(Debug)]
557pub struct ProgramBuilder {
558 ops: smallvec::SmallVec<[VdbeOp; 64]>,
560 labels: Vec<LabelState>,
562 regs: RegisterAllocator,
564 next_anon_placeholder: u32,
566 next_aux_cursor: i32,
568 table_index_meta: HashMap<i32, Vec<fsqlite_types::opcode::IndexCursorMeta>>,
570 schema_evaluation_context: Option<SchemaEvaluationContext>,
572}
573
574impl ProgramBuilder {
575 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 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 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 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 pub fn set_next_anon_placeholder(&mut self, val: u32) {
625 self.next_anon_placeholder = val;
626 }
627
628 pub fn current_anon_placeholder(&self) -> u32 {
630 self.next_anon_placeholder
631 }
632
633 pub fn emit(&mut self, op: VdbeOp) -> usize {
637 let addr = self.ops.len();
638 self.ops.push(op);
639 addr
640 }
641
642 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 pub fn current_addr(&self) -> usize {
664 self.ops.len()
665 }
666
667 pub fn op_at(&self, addr: usize) -> Option<&VdbeOp> {
669 self.ops.get(addr)
670 }
671
672 pub fn op_at_mut(&mut self, addr: usize) -> Option<&mut VdbeOp> {
674 self.ops.get_mut(addr)
675 }
676
677 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 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, 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 self.ops[addr].p2 = *target;
713 }
714 }
715
716 addr
717 }
718
719 pub fn resolve_label(&mut self, label: Label) {
723 let Ok(target) = i32::try_from(self.ops.len()) else {
724 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 return;
736 }
737 };
738
739 for op_idx in refs {
740 self.ops[op_idx].p2 = target;
741 }
742 }
743
744 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 pub fn alloc_reg(&mut self) -> i32 {
762 self.regs.alloc_reg()
763 }
764
765 pub fn alloc_regs(&mut self, n: i32) -> i32 {
767 self.regs.alloc_regs(n)
768 }
769
770 pub fn alloc_temp(&mut self) -> i32 {
772 self.regs.alloc_temp()
773 }
774
775 pub fn free_temp(&mut self, reg: i32) {
777 self.regs.free_temp(reg);
778 }
779
780 pub fn register_count(&self) -> i32 {
782 self.regs.count()
783 }
784
785 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 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 pub fn finish(self) -> Result<VdbeProgram> {
883 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 let mut ops = self.ops;
908 peephole_fuse_append_insert(&mut ops);
909 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
941fn 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 {
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 }
1009
1010fn compute_requires_attached_memdb(ops: &[VdbeOp]) -> bool {
1017 compute_attached_memdb_requirement_reason(ops).is_some()
1018}
1019
1020fn 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
1111pub(crate) type TableIndexMetaMap = HashMap<i32, Box<[fsqlite_types::opcode::IndexCursorMeta]>>;
1114
1115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1117pub enum StorageRootRole {
1118 Table,
1119 Index,
1120 Unknown,
1121}
1122
1123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125pub enum StorageRootAccess {
1126 Read,
1127 Write,
1128}
1129
1130#[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#[derive(Debug, Clone, PartialEq)]
1163pub struct VdbeProgram {
1164 ops: smallvec::SmallVec<[VdbeOp; 64]>,
1166 register_count: i32,
1168 bind_parameter_requirement: std::result::Result<usize, i32>,
1173 table_index_meta: Arc<TableIndexMetaMap>,
1175 has_insert: bool,
1178 requires_attached_memdb: bool,
1181 requires_version_store: bool,
1183}
1184
1185impl VdbeProgram {
1186 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 pub fn ops(&self) -> &[VdbeOp] {
1340 &self.ops
1341 }
1342
1343 pub fn len(&self) -> usize {
1345 self.ops.len()
1346 }
1347
1348 pub fn is_empty(&self) -> bool {
1350 self.ops.is_empty()
1351 }
1352
1353 pub fn register_count(&self) -> i32 {
1355 self.register_count
1356 }
1357
1358 pub fn max_bind_parameter_index(&self) -> std::result::Result<usize, i32> {
1364 self.bind_parameter_requirement
1365 }
1366
1367 pub fn get(&self, pc: usize) -> Option<&VdbeOp> {
1369 self.ops.get(pc)
1370 }
1371
1372 pub fn table_index_meta(&self) -> &TableIndexMetaMap {
1374 self.table_index_meta.as_ref()
1375 }
1376
1377 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 pub fn has_insert_ops(&self) -> bool {
1396 self.has_insert
1397 }
1398
1399 pub fn requires_attached_memdb(&self) -> bool {
1402 self.requires_attached_memdb
1403 }
1404
1405 pub fn attached_memdb_requirement_reason(&self) -> Option<&'static str> {
1408 compute_attached_memdb_requirement_reason(&self.ops)
1409 }
1410
1411 pub fn requires_version_store(&self) -> bool {
1413 self.requires_version_store
1414 }
1415
1416 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
1481pub mod pragma {
1489 use std::path::Path;
1490
1491 use fsqlite_ast::{Expr, Literal, PragmaStatement, PragmaValue, QualifiedName, UnaryOp};
1492 use fsqlite_error::{FrankenError, Result};
1493 use fsqlite_mvcc::TransactionManager;
1494 use fsqlite_wal::{
1495 DEFAULT_RAPTORQ_REPAIR_SYMBOLS, MAX_RAPTORQ_REPAIR_SYMBOLS,
1496 persist_wal_fec_raptorq_repair_symbols, read_wal_fec_raptorq_repair_symbols,
1497 };
1498 use tracing::{debug, error, info, warn};
1499
1500 #[derive(Debug, Clone, PartialEq, Eq)]
1502 pub enum PragmaOutput {
1503 Unsupported,
1505 Bool(bool),
1507 Int(i64),
1509 Text(String),
1511 }
1512
1513 #[derive(Debug, Clone, Copy)]
1520 pub enum DifferentialViewsSetting {
1521 Off,
1522 On,
1523 }
1524
1525 impl DifferentialViewsSetting {
1526 #[must_use]
1527 pub const fn is_enabled(&self) -> bool {
1528 matches!(self, Self::On)
1529 }
1530
1531 #[must_use]
1532 pub const fn from_enabled(enabled: bool) -> Self {
1533 if enabled { Self::On } else { Self::Off }
1534 }
1535 }
1536
1537 #[derive(Debug, Clone)]
1538 #[allow(clippy::struct_excessive_bools)]
1539 pub struct ConnectionPragmaState {
1540 pub journal_mode: String,
1542 pub synchronous: String,
1544 pub cache_size: i64,
1546 pub page_size: u32,
1548 pub busy_timeout_ms: i64,
1550 pub temp_store: i64,
1552 pub mmap_size: i64,
1554 pub auto_vacuum: i64,
1556 pub wal_autocheckpoint: i64,
1558 pub user_version: i64,
1560 pub application_id: i64,
1562 pub default_cache_size: i64,
1567 pub foreign_keys: bool,
1569 pub recursive_triggers: bool,
1571 pub query_only: bool,
1573 pub serializable: bool,
1575 pub differential_views: DifferentialViewsSetting,
1577 pub raptorq_repair_symbols: u8,
1579 pub mvcc_max_chain_length: usize,
1582 pub mvcc_writer_lease_secs: u64,
1585 pub writable_schema: bool,
1587 pub case_sensitive_like: bool,
1590 pub trusted_schema: bool,
1593 pub read_uncommitted: bool,
1595 pub cell_size_check: bool,
1597 pub checkpoint_fullfsync: bool,
1599 pub automatic_index: bool,
1601 pub locking_mode: String,
1604 pub secure_delete: i64,
1607 pub threads: i64,
1611 }
1612
1613 impl Default for ConnectionPragmaState {
1614 fn default() -> Self {
1615 Self {
1616 journal_mode: "wal".to_owned(),
1617 synchronous: "NORMAL".to_owned(),
1618 cache_size: -2000,
1619 page_size: 4096,
1620 busy_timeout_ms: 5000,
1621 temp_store: 0,
1622 mmap_size: 0,
1623 auto_vacuum: 0,
1624 wal_autocheckpoint: 1000,
1625 user_version: 0,
1626 application_id: 0,
1627 default_cache_size: 0,
1628 foreign_keys: false,
1629 recursive_triggers: false,
1630 query_only: false,
1631 serializable: true,
1632 differential_views: DifferentialViewsSetting::Off,
1633 raptorq_repair_symbols: DEFAULT_RAPTORQ_REPAIR_SYMBOLS,
1634 mvcc_max_chain_length: 64,
1635 mvcc_writer_lease_secs: 30,
1636 writable_schema: false,
1637 case_sensitive_like: false,
1638 trusted_schema: true,
1639 read_uncommitted: false,
1640 cell_size_check: false,
1641 checkpoint_fullfsync: false,
1642 automatic_index: true,
1643 locking_mode: "normal".to_owned(),
1644 secure_delete: 0,
1645 threads: 0,
1646 }
1647 }
1648 }
1649
1650 pub fn apply(mgr: &mut TransactionManager, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1660 apply_with_sidecar(mgr, stmt, None)
1661 }
1662
1663 pub fn apply_with_sidecar(
1665 mgr: &mut TransactionManager,
1666 stmt: &PragmaStatement,
1667 wal_fec_sidecar_path: Option<&Path>,
1668 ) -> Result<PragmaOutput> {
1669 if is_fsqlite_serializable(&stmt.name) {
1670 return apply_serializable(mgr, stmt);
1671 }
1672 if is_raptorq_repair_symbols(&stmt.name) {
1673 return apply_raptorq_repair_symbols(mgr, stmt, wal_fec_sidecar_path);
1674 }
1675 Ok(PragmaOutput::Unsupported)
1676 }
1677
1678 pub fn apply_connection_pragma(
1684 state: &mut ConnectionPragmaState,
1685 stmt: &PragmaStatement,
1686 ) -> Result<PragmaOutput> {
1687 let name = &stmt.name.name;
1688 if is_fsqlite_serializable(&stmt.name) {
1689 return apply_serializable_connection(state, stmt);
1690 }
1691 if is_fsqlite_differential_views(&stmt.name) {
1692 return apply_differential_views_connection(state, stmt);
1693 }
1694 if is_raptorq_repair_symbols(&stmt.name) {
1695 return apply_raptorq_repair_symbols_connection(state, stmt);
1696 }
1697 if name.eq_ignore_ascii_case("journal_mode") {
1698 return apply_journal_mode(state, stmt);
1699 }
1700 if name.eq_ignore_ascii_case("synchronous") {
1701 return apply_synchronous(state, stmt);
1702 }
1703 if name.eq_ignore_ascii_case("cache_size") {
1704 return apply_cache_size(state, stmt);
1705 }
1706 if name.eq_ignore_ascii_case("page_size") {
1707 return apply_page_size(state, stmt);
1708 }
1709 if name.eq_ignore_ascii_case("busy_timeout") {
1710 return apply_busy_timeout(state, stmt);
1711 }
1712 if name.eq_ignore_ascii_case("temp_store") {
1713 return apply_temp_store(state, stmt);
1714 }
1715 if name.eq_ignore_ascii_case("mmap_size") {
1716 return apply_mmap_size(state, stmt);
1717 }
1718 if name.eq_ignore_ascii_case("auto_vacuum") {
1719 return apply_auto_vacuum(state, stmt);
1720 }
1721 if name.eq_ignore_ascii_case("wal_autocheckpoint") {
1722 return apply_wal_autocheckpoint(state, stmt);
1723 }
1724 if name.eq_ignore_ascii_case("user_version") {
1725 return apply_user_version(state, stmt);
1726 }
1727 if name.eq_ignore_ascii_case("application_id") {
1728 return apply_application_id(state, stmt);
1729 }
1730 if name.eq_ignore_ascii_case("default_cache_size") {
1731 return apply_default_cache_size(state, stmt);
1732 }
1733 if name.eq_ignore_ascii_case("foreign_keys") {
1734 return apply_foreign_keys(state, stmt);
1735 }
1736 if name.eq_ignore_ascii_case("recursive_triggers") {
1737 return apply_recursive_triggers(state, stmt);
1738 }
1739 if name.eq_ignore_ascii_case("query_only") {
1740 return apply_query_only(state, stmt);
1741 }
1742 if name.eq_ignore_ascii_case("writable_schema") {
1743 return apply_writable_schema(state, stmt);
1744 }
1745 if name.eq_ignore_ascii_case("case_sensitive_like") {
1746 return apply_case_sensitive_like(state, stmt);
1747 }
1748 if name.eq_ignore_ascii_case("trusted_schema") {
1749 return apply_bool_toggle(&mut state.trusted_schema, stmt);
1750 }
1751 if name.eq_ignore_ascii_case("read_uncommitted") {
1752 return apply_bool_toggle(&mut state.read_uncommitted, stmt);
1753 }
1754 if name.eq_ignore_ascii_case("cell_size_check") {
1755 return apply_bool_toggle(&mut state.cell_size_check, stmt);
1756 }
1757 if name.eq_ignore_ascii_case("checkpoint_fullfsync") {
1758 return apply_bool_toggle(&mut state.checkpoint_fullfsync, stmt);
1759 }
1760 if name.eq_ignore_ascii_case("automatic_index") {
1761 return apply_bool_toggle(&mut state.automatic_index, stmt);
1762 }
1763 if name.eq_ignore_ascii_case("locking_mode") {
1764 return apply_locking_mode(state, stmt);
1765 }
1766 if name.eq_ignore_ascii_case("secure_delete") {
1767 return apply_secure_delete(state, stmt);
1768 }
1769 if name.eq_ignore_ascii_case("threads") {
1770 return apply_threads(state, stmt);
1771 }
1772 if is_fsqlite_mvcc_max_chain_length(&stmt.name) {
1773 return apply_mvcc_max_chain_length(state, stmt);
1774 }
1775 if is_fsqlite_mvcc_writer_lease_secs(&stmt.name) {
1776 return apply_mvcc_writer_lease_secs(state, stmt);
1777 }
1778 Ok(PragmaOutput::Unsupported)
1779 }
1780
1781 fn apply_serializable_connection(
1782 state: &mut ConnectionPragmaState,
1783 stmt: &PragmaStatement,
1784 ) -> Result<PragmaOutput> {
1785 match &stmt.value {
1786 None => Ok(PragmaOutput::Bool(state.serializable)),
1787 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1788 let enabled = parse_bool(expr)?;
1789 state.serializable = enabled;
1790 Ok(PragmaOutput::Bool(enabled))
1791 }
1792 }
1793 }
1794
1795 fn apply_bool_toggle(flag: &mut bool, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1801 match &stmt.value {
1802 None => Ok(PragmaOutput::Bool(*flag)),
1803 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1804 let enabled = parse_bool(expr)?;
1805 *flag = enabled;
1806 Ok(PragmaOutput::Bool(enabled))
1807 }
1808 }
1809 }
1810
1811 fn apply_locking_mode(
1816 state: &mut ConnectionPragmaState,
1817 stmt: &PragmaStatement,
1818 ) -> Result<PragmaOutput> {
1819 match &stmt.value {
1820 None => Ok(PragmaOutput::Text(state.locking_mode.clone())),
1821 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1822 let requested = parse_text_expr(expr)?.to_ascii_lowercase();
1823 if requested == "normal" || requested == "exclusive" {
1824 state.locking_mode = requested;
1825 }
1826 Ok(PragmaOutput::Text(state.locking_mode.clone()))
1827 }
1828 }
1829 }
1830
1831 fn apply_secure_delete(
1836 state: &mut ConnectionPragmaState,
1837 stmt: &PragmaStatement,
1838 ) -> Result<PragmaOutput> {
1839 match &stmt.value {
1840 None => Ok(PragmaOutput::Int(state.secure_delete)),
1841 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1842 let value = parse_secure_delete_value(expr)?;
1843 state.secure_delete = value;
1844 Ok(PragmaOutput::Int(value))
1845 }
1846 }
1847 }
1848
1849 fn parse_secure_delete_value(expr: &Expr) -> Result<i64> {
1850 if let Expr::Literal(Literal::Integer(n), _) = expr {
1851 return match *n {
1852 0..=2 => Ok(*n),
1853 _ => Err(FrankenError::OutOfRange {
1854 what: "secure_delete".to_owned(),
1855 value: n.to_string(),
1856 }),
1857 };
1858 }
1859 if let Ok(text) = parse_text_expr(expr)
1860 && text.eq_ignore_ascii_case("fast")
1861 {
1862 return Ok(2);
1863 }
1864 Ok(i64::from(parse_bool(expr)?))
1866 }
1867
1868 fn apply_threads(
1873 state: &mut ConnectionPragmaState,
1874 stmt: &PragmaStatement,
1875 ) -> Result<PragmaOutput> {
1876 const MAX_WORKER_THREADS: i64 = 8;
1878 match &stmt.value {
1879 None => Ok(PragmaOutput::Int(state.threads)),
1880 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1881 let n = parse_integer_expr(expr)?;
1882 if n >= 0 {
1883 state.threads = n.min(MAX_WORKER_THREADS);
1884 }
1885 Ok(PragmaOutput::Int(state.threads))
1886 }
1887 }
1888 }
1889
1890 fn apply_case_sensitive_like(
1896 state: &mut ConnectionPragmaState,
1897 stmt: &PragmaStatement,
1898 ) -> Result<PragmaOutput> {
1899 match &stmt.value {
1900 None => Ok(PragmaOutput::Bool(state.case_sensitive_like)),
1901 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1902 let enabled = parse_bool(expr)?;
1903 state.case_sensitive_like = enabled;
1904 Ok(PragmaOutput::Bool(enabled))
1905 }
1906 }
1907 }
1908
1909 fn apply_raptorq_repair_symbols_connection(
1910 state: &mut ConnectionPragmaState,
1911 stmt: &PragmaStatement,
1912 ) -> Result<PragmaOutput> {
1913 match &stmt.value {
1914 None => Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols))),
1915 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1916 let value = parse_integer_expr(expr)?;
1917 if !(0..=i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS)).contains(&value) {
1918 return Err(FrankenError::OutOfRange {
1919 what: "raptorq_repair_symbols".to_owned(),
1920 value: value.to_string(),
1921 });
1922 }
1923 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1924 {
1925 state.raptorq_repair_symbols = value as u8;
1926 }
1927 Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols)))
1928 }
1929 }
1930 }
1931
1932 fn apply_differential_views_connection(
1933 state: &mut ConnectionPragmaState,
1934 stmt: &PragmaStatement,
1935 ) -> Result<PragmaOutput> {
1936 match &stmt.value {
1937 None => Ok(PragmaOutput::Bool(state.differential_views.is_enabled())),
1938 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1939 let enabled = parse_bool(expr)?;
1940 state.differential_views = DifferentialViewsSetting::from_enabled(enabled);
1941 Ok(PragmaOutput::Bool(enabled))
1942 }
1943 }
1944 }
1945
1946 fn apply_journal_mode(
1947 state: &mut ConnectionPragmaState,
1948 stmt: &PragmaStatement,
1949 ) -> Result<PragmaOutput> {
1950 match &stmt.value {
1951 None => Ok(PragmaOutput::Text(state.journal_mode.clone())),
1952 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1953 let mode = parse_text_expr(expr)?;
1954 let lower = mode.to_ascii_lowercase();
1955 match lower.as_str() {
1956 "delete" | "truncate" | "persist" | "memory" | "wal" | "off" => {
1957 state.journal_mode.clone_from(&lower);
1958 Ok(PragmaOutput::Text(lower))
1959 }
1960 _ => Err(FrankenError::TypeMismatch {
1961 expected: "delete|truncate|persist|memory|wal|off".to_owned(),
1962 actual: mode,
1963 }),
1964 }
1965 }
1966 }
1967 }
1968
1969 fn apply_synchronous(
1970 state: &mut ConnectionPragmaState,
1971 stmt: &PragmaStatement,
1972 ) -> Result<PragmaOutput> {
1973 match &stmt.value {
1974 None => Ok(PragmaOutput::Text(state.synchronous.clone())),
1975 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1976 let val = parse_synchronous_value(expr)?;
1977 state.synchronous.clone_from(&val);
1978 Ok(PragmaOutput::Text(val))
1979 }
1980 }
1981 }
1982
1983 fn parse_synchronous_value(expr: &Expr) -> Result<String> {
1984 if let Expr::Literal(Literal::Integer(n), _) = expr {
1986 match n {
1987 0 => Ok("OFF".to_owned()),
1988 1 => Ok("NORMAL".to_owned()),
1989 2 => Ok("FULL".to_owned()),
1990 3 => Ok("EXTRA".to_owned()),
1991 _ => Err(FrankenError::OutOfRange {
1992 what: "synchronous".to_owned(),
1993 value: n.to_string(),
1994 }),
1995 }
1996 } else {
1997 let text = parse_text_expr(expr)?;
1998 let upper = text.to_ascii_uppercase();
1999 match upper.as_str() {
2000 "OFF" | "NORMAL" | "FULL" | "EXTRA" => Ok(upper),
2001 _ => Err(FrankenError::TypeMismatch {
2002 expected: "OFF|NORMAL|FULL|EXTRA|0|1|2|3".to_owned(),
2003 actual: text,
2004 }),
2005 }
2006 }
2007 }
2008
2009 fn apply_cache_size(
2010 state: &mut ConnectionPragmaState,
2011 stmt: &PragmaStatement,
2012 ) -> Result<PragmaOutput> {
2013 match &stmt.value {
2014 None => Ok(PragmaOutput::Int(state.cache_size)),
2015 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2016 let val = parse_integer_expr(expr)?;
2017 state.cache_size = val;
2018 Ok(PragmaOutput::Int(val))
2019 }
2020 }
2021 }
2022
2023 fn apply_page_size(
2024 state: &mut ConnectionPragmaState,
2025 stmt: &PragmaStatement,
2026 ) -> Result<PragmaOutput> {
2027 match &stmt.value {
2028 None => Ok(PragmaOutput::Int(i64::from(state.page_size))),
2029 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2030 let val = parse_integer_expr(expr)?;
2031 if !(512..=65536).contains(&val) || !is_power_of_two(val) {
2032 return Err(FrankenError::OutOfRange {
2033 what: "page_size".to_owned(),
2034 value: val.to_string(),
2035 });
2036 }
2037 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2038 {
2039 state.page_size = val as u32;
2040 }
2041 Ok(PragmaOutput::Int(val))
2042 }
2043 }
2044 }
2045
2046 fn apply_busy_timeout(
2047 state: &mut ConnectionPragmaState,
2048 stmt: &PragmaStatement,
2049 ) -> Result<PragmaOutput> {
2050 match &stmt.value {
2051 None => Ok(PragmaOutput::Int(state.busy_timeout_ms)),
2052 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2053 let val = parse_integer_expr(expr)?;
2054 state.busy_timeout_ms = val.max(0);
2055 Ok(PragmaOutput::Int(state.busy_timeout_ms))
2056 }
2057 }
2058 }
2059
2060 fn apply_temp_store(
2061 state: &mut ConnectionPragmaState,
2062 stmt: &PragmaStatement,
2063 ) -> Result<PragmaOutput> {
2064 match &stmt.value {
2065 None => Ok(PragmaOutput::Int(state.temp_store)),
2066 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2067 let val = parse_temp_store_value(expr)?;
2068 state.temp_store = val;
2069 Ok(PragmaOutput::Int(val))
2070 }
2071 }
2072 }
2073
2074 fn apply_mmap_size(
2075 state: &mut ConnectionPragmaState,
2076 stmt: &PragmaStatement,
2077 ) -> Result<PragmaOutput> {
2078 match &stmt.value {
2079 None => Ok(PragmaOutput::Int(state.mmap_size)),
2080 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2081 let val = parse_integer_expr(expr)?;
2082 state.mmap_size = val.max(0);
2083 Ok(PragmaOutput::Int(state.mmap_size))
2084 }
2085 }
2086 }
2087
2088 fn apply_auto_vacuum(
2089 state: &mut ConnectionPragmaState,
2090 stmt: &PragmaStatement,
2091 ) -> Result<PragmaOutput> {
2092 match &stmt.value {
2093 None => Ok(PragmaOutput::Int(state.auto_vacuum)),
2094 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2095 let val = parse_auto_vacuum_value(expr)?;
2096 state.auto_vacuum = val;
2097 Ok(PragmaOutput::Int(val))
2098 }
2099 }
2100 }
2101
2102 fn apply_wal_autocheckpoint(
2103 state: &mut ConnectionPragmaState,
2104 stmt: &PragmaStatement,
2105 ) -> Result<PragmaOutput> {
2106 match &stmt.value {
2107 None => Ok(PragmaOutput::Int(state.wal_autocheckpoint)),
2108 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2109 let val = parse_integer_expr(expr)?;
2110 state.wal_autocheckpoint = val.max(0);
2111 Ok(PragmaOutput::Int(state.wal_autocheckpoint))
2112 }
2113 }
2114 }
2115
2116 fn apply_user_version(
2117 state: &mut ConnectionPragmaState,
2118 stmt: &PragmaStatement,
2119 ) -> Result<PragmaOutput> {
2120 match &stmt.value {
2121 None => Ok(PragmaOutput::Int(state.user_version)),
2122 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2123 let val = parse_integer_expr(expr)?;
2124 state.user_version = val;
2125 Ok(PragmaOutput::Int(val))
2126 }
2127 }
2128 }
2129
2130 fn apply_application_id(
2131 state: &mut ConnectionPragmaState,
2132 stmt: &PragmaStatement,
2133 ) -> Result<PragmaOutput> {
2134 match &stmt.value {
2135 None => Ok(PragmaOutput::Int(state.application_id)),
2136 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2137 let val = parse_integer_expr(expr)?;
2138 state.application_id = val;
2139 Ok(PragmaOutput::Int(val))
2140 }
2141 }
2142 }
2143
2144 fn apply_default_cache_size(
2152 state: &mut ConnectionPragmaState,
2153 stmt: &PragmaStatement,
2154 ) -> Result<PragmaOutput> {
2155 let report = |stored: i64| if stored == 0 { -2000 } else { stored };
2157 match &stmt.value {
2158 None => Ok(PragmaOutput::Int(report(state.default_cache_size))),
2159 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2160 let stored = parse_integer_expr(expr)?.saturating_abs();
2161 state.default_cache_size = stored;
2162 state.cache_size = stored;
2163 Ok(PragmaOutput::Int(report(stored)))
2164 }
2165 }
2166 }
2167
2168 fn apply_foreign_keys(
2169 state: &mut ConnectionPragmaState,
2170 stmt: &PragmaStatement,
2171 ) -> Result<PragmaOutput> {
2172 match &stmt.value {
2173 None => Ok(PragmaOutput::Int(i64::from(state.foreign_keys))),
2174 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2175 let enabled = parse_bool(expr)?;
2176 state.foreign_keys = enabled;
2177 Ok(PragmaOutput::Int(i64::from(enabled)))
2178 }
2179 }
2180 }
2181
2182 fn apply_recursive_triggers(
2183 state: &mut ConnectionPragmaState,
2184 stmt: &PragmaStatement,
2185 ) -> Result<PragmaOutput> {
2186 match &stmt.value {
2187 None => Ok(PragmaOutput::Int(i64::from(state.recursive_triggers))),
2188 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2189 let enabled = parse_bool(expr)?;
2190 state.recursive_triggers = enabled;
2191 Ok(PragmaOutput::Int(i64::from(enabled)))
2192 }
2193 }
2194 }
2195
2196 fn apply_query_only(
2197 state: &mut ConnectionPragmaState,
2198 stmt: &PragmaStatement,
2199 ) -> Result<PragmaOutput> {
2200 match &stmt.value {
2201 None => Ok(PragmaOutput::Int(i64::from(state.query_only))),
2202 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2203 let enabled = parse_bool(expr)?;
2204 state.query_only = enabled;
2205 Ok(PragmaOutput::Int(i64::from(enabled)))
2206 }
2207 }
2208 }
2209
2210 fn apply_writable_schema(
2211 state: &mut ConnectionPragmaState,
2212 stmt: &PragmaStatement,
2213 ) -> Result<PragmaOutput> {
2214 match &stmt.value {
2215 None => Ok(PragmaOutput::Int(i64::from(state.writable_schema))),
2216 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2217 if expr_is_reset(expr) {
2225 state.writable_schema = false;
2226 return Ok(PragmaOutput::Int(0));
2227 }
2228 let enabled = parse_bool(expr)?;
2229 state.writable_schema = enabled;
2230 Ok(PragmaOutput::Int(i64::from(enabled)))
2231 }
2232 }
2233 }
2234
2235 fn expr_is_reset(expr: &Expr) -> bool {
2238 match expr {
2239 Expr::Column(col, _) => col.column.eq_ignore_ascii_case("reset"),
2240 Expr::Literal(Literal::String(s), _) => s.eq_ignore_ascii_case("reset"),
2241 _ => false,
2242 }
2243 }
2244
2245 fn parse_temp_store_value(expr: &Expr) -> Result<i64> {
2246 if let Expr::Literal(Literal::Integer(n), _) = expr {
2247 return match *n {
2248 0..=2 => Ok(*n),
2249 _ => Err(FrankenError::OutOfRange {
2250 what: "temp_store".to_owned(),
2251 value: n.to_string(),
2252 }),
2253 };
2254 }
2255
2256 let text = parse_text_expr(expr)?;
2257 match text.to_ascii_lowercase().as_str() {
2258 "default" => Ok(0),
2259 "file" => Ok(1),
2260 "memory" => Ok(2),
2261 _ => Err(FrankenError::TypeMismatch {
2262 expected: "DEFAULT|FILE|MEMORY|0|1|2".to_owned(),
2263 actual: text,
2264 }),
2265 }
2266 }
2267
2268 fn parse_auto_vacuum_value(expr: &Expr) -> Result<i64> {
2269 if let Expr::Literal(Literal::Integer(n), _) = expr {
2270 return match *n {
2271 0..=2 => Ok(*n),
2272 _ => Err(FrankenError::OutOfRange {
2273 what: "auto_vacuum".to_owned(),
2274 value: n.to_string(),
2275 }),
2276 };
2277 }
2278
2279 let text = parse_text_expr(expr)?;
2280 match text.to_ascii_lowercase().as_str() {
2281 "none" => Ok(0),
2282 "full" => Ok(1),
2283 "incremental" => Ok(2),
2284 _ => Err(FrankenError::TypeMismatch {
2285 expected: "NONE|FULL|INCREMENTAL|0|1|2".to_owned(),
2286 actual: text,
2287 }),
2288 }
2289 }
2290
2291 fn is_power_of_two(n: i64) -> bool {
2292 n > 0 && (n & (n - 1)) == 0
2293 }
2294
2295 fn parse_text_expr(expr: &Expr) -> Result<String> {
2297 match expr {
2298 Expr::Literal(Literal::String(s), _) => Ok(s.clone()),
2299 Expr::Column(col, _) => Ok(col.column.to_string()),
2300 Expr::Literal(Literal::Integer(n), _) => Ok(n.to_string()),
2301 other => Err(FrankenError::TypeMismatch {
2302 expected: "text or identifier".to_owned(),
2303 actual: format!("{other:?}"),
2304 }),
2305 }
2306 }
2307
2308 fn apply_serializable(
2309 mgr: &mut TransactionManager,
2310 stmt: &PragmaStatement,
2311 ) -> Result<PragmaOutput> {
2312 match &stmt.value {
2313 None => Ok(PragmaOutput::Bool(mgr.ssi_enabled())),
2314 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2315 let enabled = parse_bool(expr)?;
2316 mgr.set_ssi_enabled(enabled);
2317 Ok(PragmaOutput::Bool(mgr.ssi_enabled()))
2318 }
2319 }
2320 }
2321
2322 fn is_fsqlite_serializable(name: &QualifiedName) -> bool {
2323 name.schema
2324 .as_deref()
2325 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2326 && name.name.eq_ignore_ascii_case("serializable")
2327 }
2328
2329 fn is_fsqlite_differential_views(name: &QualifiedName) -> bool {
2330 match name.schema.as_deref() {
2331 Some(schema) => {
2332 schema.eq_ignore_ascii_case("fsqlite")
2333 && name.name.eq_ignore_ascii_case("differential_views")
2334 }
2335 None => name.name.eq_ignore_ascii_case("fsqlite_differential_views"),
2336 }
2337 }
2338
2339 fn is_raptorq_repair_symbols(name: &QualifiedName) -> bool {
2340 let schema_ok = match name.schema.as_deref() {
2341 None => true,
2342 Some(schema) => schema.eq_ignore_ascii_case("fsqlite"),
2343 };
2344 schema_ok && name.name.eq_ignore_ascii_case("raptorq_repair_symbols")
2345 }
2346
2347 fn is_fsqlite_mvcc_max_chain_length(name: &QualifiedName) -> bool {
2348 name.schema
2349 .as_deref()
2350 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2351 && name.name.eq_ignore_ascii_case("mvcc_max_chain_length")
2352 }
2353
2354 fn is_fsqlite_mvcc_writer_lease_secs(name: &QualifiedName) -> bool {
2355 name.schema
2356 .as_deref()
2357 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2358 && name.name.eq_ignore_ascii_case("mvcc_writer_lease_secs")
2359 }
2360
2361 fn apply_mvcc_max_chain_length(
2362 state: &mut ConnectionPragmaState,
2363 stmt: &PragmaStatement,
2364 ) -> Result<PragmaOutput> {
2365 match &stmt.value {
2366 None => Ok(PragmaOutput::Int(state.mvcc_max_chain_length as i64)),
2367 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2368 let value = parse_integer_expr(expr)?;
2369 if value < 2 {
2370 return Err(FrankenError::OutOfRange {
2371 what: "fsqlite.mvcc_max_chain_length".into(),
2372 value: format!("{value} (minimum 2)"),
2373 });
2374 }
2375 #[allow(clippy::cast_sign_loss)]
2376 {
2377 state.mvcc_max_chain_length = value as usize;
2378 }
2379 Ok(PragmaOutput::Int(value))
2385 }
2386 }
2387 }
2388
2389 fn apply_mvcc_writer_lease_secs(
2390 state: &mut ConnectionPragmaState,
2391 stmt: &PragmaStatement,
2392 ) -> Result<PragmaOutput> {
2393 match &stmt.value {
2394 None => Ok(PragmaOutput::Int(state.mvcc_writer_lease_secs as i64)),
2395 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2396 let value = parse_integer_expr(expr)?;
2397 if value < 1 {
2398 return Err(FrankenError::OutOfRange {
2399 what: "fsqlite.mvcc_writer_lease_secs".into(),
2400 value: format!("{value} (minimum 1)"),
2401 });
2402 }
2403 #[allow(clippy::cast_sign_loss)]
2404 {
2405 state.mvcc_writer_lease_secs = value as u64;
2406 }
2407 Ok(PragmaOutput::Int(value))
2409 }
2410 }
2411 }
2412
2413 fn apply_raptorq_repair_symbols(
2414 mgr: &mut TransactionManager,
2415 stmt: &PragmaStatement,
2416 wal_fec_sidecar_path: Option<&Path>,
2417 ) -> Result<PragmaOutput> {
2418 match &stmt.value {
2419 None => {
2420 if let Some(sidecar) = wal_fec_sidecar_path {
2421 let persisted = read_wal_fec_raptorq_repair_symbols(sidecar)?;
2422 mgr.set_raptorq_repair_symbols(persisted);
2423 debug!(
2424 sidecar = %sidecar.display(),
2425 raptorq_repair_symbols = persisted,
2426 "loaded raptorq_repair_symbols from wal-fec sidecar"
2427 );
2428 }
2429 Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2430 }
2431 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2432 let requested = parse_raptorq_repair_symbols(expr)?;
2433 mgr.set_raptorq_repair_symbols(requested);
2434
2435 if let Some(sidecar) = wal_fec_sidecar_path {
2436 persist_wal_fec_raptorq_repair_symbols(sidecar, requested)?;
2437 info!(
2438 sidecar = %sidecar.display(),
2439 raptorq_repair_symbols = requested,
2440 "persisted raptorq_repair_symbols to wal-fec sidecar"
2441 );
2442 }
2443
2444 Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2445 }
2446 }
2447 }
2448
2449 fn parse_raptorq_repair_symbols(expr: &Expr) -> Result<u8> {
2450 let raw = parse_integer_expr(expr)?;
2451 if raw < 0 {
2452 warn!(
2453 value = raw,
2454 "rejecting negative raptorq_repair_symbols value"
2455 );
2456 return Err(FrankenError::OutOfRange {
2457 what: "raptorq_repair_symbols".to_owned(),
2458 value: raw.to_string(),
2459 });
2460 }
2461
2462 let max = i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS);
2463 if raw > max {
2464 warn!(
2465 value = raw,
2466 max = MAX_RAPTORQ_REPAIR_SYMBOLS,
2467 "rejecting out-of-range raptorq_repair_symbols value"
2468 );
2469 return Err(FrankenError::OutOfRange {
2470 what: "raptorq_repair_symbols".to_owned(),
2471 value: raw.to_string(),
2472 });
2473 }
2474
2475 u8::try_from(raw).map_err(|_| {
2476 error!(
2477 value = raw,
2478 "failed to convert validated raptorq_repair_symbols to u8"
2479 );
2480 FrankenError::OutOfRange {
2481 what: "raptorq_repair_symbols".to_owned(),
2482 value: raw.to_string(),
2483 }
2484 })
2485 }
2486
2487 fn parse_integer_expr(expr: &Expr) -> Result<i64> {
2488 match expr {
2489 Expr::Literal(Literal::Integer(n), _) => Ok(*n),
2490 Expr::UnaryOp {
2491 op: UnaryOp::Negate,
2492 expr,
2493 ..
2494 } => Ok(-parse_integer_expr(expr)?),
2495 Expr::UnaryOp {
2496 op: UnaryOp::Plus,
2497 expr,
2498 ..
2499 } => parse_integer_expr(expr),
2500 Expr::Column(col, _) => {
2501 col.column
2502 .parse::<i64>()
2503 .map_err(|_| FrankenError::TypeMismatch {
2504 expected: "integer (0..255)".to_owned(),
2505 actual: col.column.to_string(),
2506 })
2507 }
2508 other => Err(FrankenError::TypeMismatch {
2509 expected: "integer (0..255)".to_owned(),
2510 actual: format!("{other:?}"),
2511 }),
2512 }
2513 }
2514
2515 fn parse_bool(expr: &Expr) -> Result<bool> {
2516 let (raw, parsed) = match expr {
2517 Expr::Literal(Literal::Integer(n), _) => (format!("{n}"), parse_int_bool(*n)),
2518 Expr::Literal(Literal::String(s), _) => (s.clone(), parse_str_bool(s)),
2519 Expr::Literal(Literal::True, _) => ("TRUE".to_owned(), Some(true)),
2520 Expr::Literal(Literal::False, _) => ("FALSE".to_owned(), Some(false)),
2521 Expr::Column(col, _) => (col.column.to_string(), parse_str_bool(&col.column)),
2522 other => {
2523 return Err(FrankenError::TypeMismatch {
2524 expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2525 actual: format!("{other:?}"),
2526 });
2527 }
2528 };
2529
2530 parsed.ok_or_else(|| FrankenError::TypeMismatch {
2531 expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2532 actual: raw,
2533 })
2534 }
2535
2536 fn parse_int_bool(n: i64) -> Option<bool> {
2537 match n {
2538 0 => Some(false),
2539 1 => Some(true),
2540 _ => None,
2541 }
2542 }
2543
2544 fn parse_str_bool(s: &str) -> Option<bool> {
2545 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("true") {
2546 Some(true)
2547 } else if s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("false") {
2548 Some(false)
2549 } else if s == "1" {
2550 Some(true)
2551 } else if s == "0" {
2552 Some(false)
2553 } else {
2554 None
2555 }
2556 }
2557}
2558
2559#[cfg(test)]
2562mod tests {
2563 use super::*;
2564
2565 fn test_failure() -> bool {
2566 false
2567 }
2568
2569 #[test]
2571 fn test_vdbe_op_struct_size() {
2572 let op = VdbeOp {
2574 opcode: Opcode::Integer,
2575 p1: 42,
2576 p2: 1,
2577 p3: 0,
2578 p4: P4::None,
2579 p5: 0,
2580 };
2581 assert_eq!(op.opcode, Opcode::Integer);
2582 assert_eq!(op.p1, 42_i32);
2583 assert_eq!(op.p2, 1_i32);
2584 assert_eq!(op.p3, 0_i32);
2585 assert_eq!(op.p4, P4::None);
2586 assert_eq!(op.p5, 0_u16);
2587 }
2588
2589 #[test]
2590 fn test_schema_evaluation_context_encoding_and_nested_restoration() {
2591 const CONSTANT_ARGUMENT_MASK: i32 = 0x15;
2592
2593 let mut builder = ProgramBuilder::new();
2594 let output = builder.alloc_reg();
2595 builder.with_schema_evaluation_context(SchemaEvaluationContext::Index, |builder| {
2596 builder.emit_op(
2597 Opcode::PureFunc,
2598 CONSTANT_ARGUMENT_MASK,
2599 0,
2600 output,
2601 P4::FuncName("INDEX_FN".to_owned()),
2602 0,
2603 );
2604 builder.with_schema_evaluation_context(
2605 SchemaEvaluationContext::CheckConstraint,
2606 |builder| {
2607 builder.emit_op(
2608 Opcode::Function,
2609 CONSTANT_ARGUMENT_MASK,
2610 0,
2611 output,
2612 P4::FuncName("CHECK_FN".to_owned()),
2613 0,
2614 );
2615 },
2616 );
2617 builder.emit_op(
2618 Opcode::PureFunc,
2619 CONSTANT_ARGUMENT_MASK,
2620 0,
2621 output,
2622 P4::FuncName("INDEX_AGAIN_FN".to_owned()),
2623 0,
2624 );
2625 });
2626 builder.with_schema_evaluation_context(
2627 SchemaEvaluationContext::GeneratedColumn,
2628 |builder| {
2629 builder.emit_op(
2630 Opcode::PureFunc,
2631 CONSTANT_ARGUMENT_MASK,
2632 0,
2633 output,
2634 P4::FuncName("GENERATED_FN".to_owned()),
2635 0,
2636 );
2637 },
2638 );
2639 builder.emit_op(
2640 Opcode::PureFunc,
2641 CONSTANT_ARGUMENT_MASK,
2642 0,
2643 output,
2644 P4::FuncName("ORDINARY_FN".to_owned()),
2645 0,
2646 );
2647 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2648
2649 let program = builder
2650 .finish()
2651 .expect("schema-context program should build");
2652 let function_ops: Vec<_> = program
2653 .ops()
2654 .iter()
2655 .filter(|op| matches!(op.opcode, Opcode::Function | Opcode::PureFunc))
2656 .collect();
2657 let contexts: Vec<_> = function_ops
2658 .iter()
2659 .map(|op| SchemaEvaluationContext::from_function_p1(op.p1))
2660 .collect();
2661
2662 assert_eq!(
2663 contexts,
2664 [
2665 Some(SchemaEvaluationContext::Index),
2666 Some(SchemaEvaluationContext::CheckConstraint),
2667 Some(SchemaEvaluationContext::Index),
2668 Some(SchemaEvaluationContext::GeneratedColumn),
2669 None,
2670 ]
2671 );
2672 assert!(
2673 function_ops
2674 .iter()
2675 .all(|op| { op.p1 & !FUNCTION_SCHEMA_CONTEXT_MASK == CONSTANT_ARGUMENT_MASK })
2676 );
2677 }
2678
2679 #[test]
2681 fn test_p4_variant_all_types() {
2682 let variants: Vec<P4> = vec![
2684 P4::None,
2685 P4::Int(42),
2686 P4::Int64(i64::MAX),
2687 P4::Real(1.234_567_89),
2688 P4::Str("hello".to_owned()),
2689 P4::Blob(vec![0xDE, 0xAD]),
2690 P4::Collation("BINARY".to_owned()),
2691 P4::FuncName("count".to_owned()),
2692 P4::Table("users".to_owned()),
2693 P4::Affinity("ddd".to_owned()),
2694 P4::PrecomputedHeader(fsqlite_types::record::PrecomputedRecordHeader::new(&[
2695 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
2696 fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
2697 ])),
2698 ];
2699 assert_eq!(variants.len(), 11);
2700
2701 assert!(matches!(variants[0], P4::None));
2703 assert!(matches!(variants[1], P4::Int(42)));
2704 assert!(matches!(variants[2], P4::Int64(i64::MAX)));
2705 assert!(matches!(variants[3], P4::Real(_)));
2706 assert!(matches!(variants[4], P4::Str(_)));
2707 assert!(matches!(variants[5], P4::Blob(_)));
2708 assert!(matches!(variants[6], P4::Collation(_)));
2709 assert!(matches!(variants[7], P4::FuncName(ref s) if s == "count"));
2710 assert!(matches!(variants[8], P4::Table(ref s) if s == "users"));
2711 assert!(matches!(variants[9], P4::Affinity(ref s) if s == "ddd"));
2712 assert!(matches!(
2713 variants[10],
2714 P4::PrecomputedHeader(ref header) if header.template == vec![3, 0, 0]
2715 ));
2716 }
2717
2718 #[test]
2720 fn test_label_emit_and_resolve() {
2721 let mut b = ProgramBuilder::new();
2722
2723 let label_a = b.emit_label();
2725 let label_b = b.emit_label();
2726 assert_ne!(label_a, label_b);
2727
2728 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label_a, P4::None, 0);
2730 assert_eq!(b.op_at(jump_addr).unwrap().p2, -1); b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
2734 b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
2735
2736 b.resolve_label(label_a);
2738
2739 assert_eq!(b.op_at(jump_addr).unwrap().p2, 3);
2741
2742 let jump2 = b.emit_jump_to_label(Opcode::If, 1, 0, label_b, P4::None, 0);
2744 b.resolve_label(label_b);
2745 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2746 assert_eq!(b.op_at(jump2).unwrap().p2, 4);
2747
2748 let prog = b.finish().unwrap();
2750 assert_eq!(prog.len(), 5);
2751 }
2752
2753 #[test]
2755 fn test_unresolved_label_error() {
2756 let mut b = ProgramBuilder::new();
2757 let label = b.emit_label();
2758 b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
2759
2760 let result = b.finish();
2762 assert!(result.is_err());
2763 }
2764
2765 #[test]
2767 fn test_register_alloc_sequential() {
2768 let mut alloc = RegisterAllocator::new();
2769
2770 assert_eq!(alloc.alloc_reg(), 1);
2772 assert_eq!(alloc.alloc_reg(), 2);
2773 assert_eq!(alloc.alloc_reg(), 3);
2774
2775 let block_start = alloc.alloc_regs(3);
2777 assert_eq!(block_start, 4);
2778 assert_eq!(alloc.alloc_reg(), 7);
2780
2781 assert_eq!(alloc.count(), 7);
2782 }
2783
2784 #[test]
2786 fn test_register_temp_pool_reuse() {
2787 let mut alloc = RegisterAllocator::new();
2788
2789 let r1 = alloc.alloc_reg(); let t1 = alloc.alloc_temp(); let t2 = alloc.alloc_temp(); assert_eq!(r1, 1);
2793 assert_eq!(t1, 2);
2794 assert_eq!(t2, 3);
2795
2796 alloc.free_temp(t1);
2798 alloc.free_temp(t2);
2799
2800 let t3 = alloc.alloc_temp();
2802 let t4 = alloc.alloc_temp();
2803 assert_eq!(t3, t2); assert_eq!(t4, t1); assert_eq!(alloc.count(), 3);
2808 }
2809
2810 #[test]
2812 fn test_coroutine_init_yield_end() {
2813 let yield_reg = 1;
2815 let body_pc = 10;
2816 let mut co = CoroutineState::new(yield_reg, body_pc);
2817 assert_eq!(co.yield_reg, yield_reg);
2818 assert_eq!(co.saved_pc, body_pc);
2819 assert!(!co.exhausted);
2820
2821 let resume = co.yield_swap(5);
2824 assert_eq!(resume, 10); assert_eq!(co.saved_pc, 5); let resume2 = co.yield_swap(15);
2829 assert_eq!(resume2, 5); assert_eq!(co.saved_pc, 15);
2831
2832 let final_pc = co.end();
2834 assert_eq!(final_pc, 15); assert!(co.exhausted);
2836 }
2837
2838 #[test]
2840 fn test_coroutine_multi_row_production() {
2841 let mut co = CoroutineState::new(1, 10); let mut rows_consumed = 0;
2844 let caller_start_pc = 5;
2845
2846 let mut next_pc = co.yield_swap(caller_start_pc);
2848 assert_eq!(next_pc, 10); for row in 1..=5 {
2852 let body_pc = 10 + row; next_pc = co.yield_swap(body_pc);
2855 assert_eq!(next_pc, caller_start_pc);
2857 rows_consumed += 1;
2858
2859 if row < 5 {
2860 next_pc = co.yield_swap(caller_start_pc);
2862 assert_eq!(next_pc, body_pc); }
2864 }
2865
2866 assert_eq!(rows_consumed, 5);
2867
2868 let final_pc = co.end();
2870 assert!(co.exhausted);
2871 assert!(final_pc > 0); }
2873
2874 #[test]
2875 fn test_program_builder_infers_register_count_from_manual_opcode_registers() {
2876 let mut builder = ProgramBuilder::new();
2877 builder.emit_op(Opcode::Integer, 11, 3, 0, P4::None, 0);
2878 builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
2879 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2880
2881 let program = builder.finish().expect("program should build");
2882 assert_eq!(
2883 program.register_count(),
2884 3,
2885 "bytecode that writes raw registers must still allocate a large enough register file",
2886 );
2887 }
2888
2889 #[test]
2890 fn test_program_builder_infers_register_count_for_non_contiguous_comparison_operands() {
2891 let mut builder = ProgramBuilder::new();
2892 builder.emit_op(Opcode::Eq, 2, 0, 7, P4::None, 0);
2893 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2894
2895 let program = builder.finish().expect("program should build");
2896 assert_eq!(
2897 program.register_count(),
2898 7,
2899 "comparison opcodes must account for both read registers even when they are not contiguous",
2900 );
2901 }
2902
2903 #[test]
2904 fn test_program_builder_infers_register_count_for_store_p2_comparisons() {
2905 let mut builder = ProgramBuilder::new();
2906 builder.emit_op(Opcode::Eq, 2, 9, 7, P4::None, 0x20);
2907 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2908
2909 let program = builder.finish().expect("program should build");
2910 assert_eq!(
2911 program.register_count(),
2912 9,
2913 "SQLITE_STOREP2 comparisons must reserve the destination register in the pre-sized register file",
2914 );
2915 }
2916
2917 #[test]
2918 fn test_program_builder_tracks_sorter_preflight_and_runtime_bound_registers() {
2919 let mut builder = ProgramBuilder::new();
2920 builder.emit_op(
2921 Opcode::SorterOpen,
2922 0,
2923 1,
2924 8,
2925 P4::None,
2926 SORTER_OPEN_TOP_N_REGISTER,
2927 );
2928 builder.emit_op(
2929 Opcode::SorterCompare,
2930 0,
2931 2,
2932 7,
2933 P4::None,
2934 fsqlite_types::opcode::SORTER_COMPARE_TOP_N_PREFLIGHT,
2935 );
2936 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2937
2938 let program = builder.finish().expect("program should build");
2939 assert_eq!(
2940 program.register_count(),
2941 8,
2942 "runtime SorterOpen.p3 and SorterCompare.p3 must contribute to register sizing"
2943 );
2944 }
2945
2946 #[test]
2947 fn test_sorter_compare_register_spans_distinguish_preflight_consumption() {
2948 let ordinary = VdbeOp {
2949 opcode: Opcode::SorterCompare,
2950 p1: 0,
2951 p2: 1,
2952 p3: 7,
2953 p4: P4::None,
2954 p5: 0,
2955 };
2956 let ordinary_spans = opcode_register_spans(&ordinary);
2957 let preflight = VdbeOp {
2958 p5: SORTER_COMPARE_TOP_N_PREFLIGHT,
2959 ..ordinary
2960 };
2961 let preflight_spans = opcode_register_spans(&preflight);
2962
2963 assert_eq!(
2964 ordinary_spans,
2965 OpcodeRegisterSpans {
2966 read_start: 7,
2967 read_len: 1,
2968 write_start: -1,
2969 write_len: 0,
2970 },
2971 "ordinary SorterCompare must leave P3 read-only"
2972 );
2973 assert_eq!(
2974 preflight_spans,
2975 OpcodeRegisterSpans {
2976 read_start: 7,
2977 read_len: 1,
2978 write_start: 7,
2979 write_len: 1,
2980 },
2981 "top-N preflight must model P3 as consumed"
2982 );
2983 }
2984
2985 #[test]
2986 fn test_program_builder_does_not_treat_immediate_sorter_bound_as_register() {
2987 let mut builder = ProgramBuilder::new();
2988 builder.emit_op(Opcode::SorterOpen, 0, 1, 500, P4::None, 0);
2989 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2990
2991 let program = builder.finish().expect("program should build");
2992 assert_eq!(
2993 program.register_count(),
2994 0,
2995 "legacy immediate SorterOpen.p3 is not a register operand"
2996 );
2997 }
2998
2999 #[test]
3000 fn test_program_builder_rejects_out_of_bounds_goto_target() {
3001 let mut builder = ProgramBuilder::new();
3002 builder.emit_op(Opcode::Goto, 0, 99, 0, P4::None, 0);
3003 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3004
3005 let err = builder.finish().expect_err("invalid jump target must fail");
3006 match err {
3007 FrankenError::Internal(message) => {
3008 assert!(message.contains("Goto"));
3009 assert!(message.contains("p2"));
3010 }
3011 other => assert!(
3012 matches!(other, FrankenError::Internal(_)),
3013 "expected internal verifier error, got {other:?}"
3014 ),
3015 }
3016 }
3017
3018 #[test]
3019 fn test_program_builder_rejects_out_of_bounds_jump_branch_target() {
3020 let mut builder = ProgramBuilder::new();
3021 builder.emit_op(Opcode::Jump, 0, 1, 42, P4::None, 0);
3022 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3023
3024 let err = builder
3025 .finish()
3026 .expect_err("invalid branch target must fail");
3027 match err {
3028 FrankenError::Internal(message) => {
3029 assert!(message.contains("Jump"));
3030 assert!(message.contains("p3"));
3031 }
3032 other => assert!(
3033 matches!(other, FrankenError::Internal(_)),
3034 "expected internal verifier error, got {other:?}"
3035 ),
3036 }
3037 }
3038
3039 #[test]
3041 fn test_all_opcode_dispatch_coverage() {
3042 for byte in 1..Opcode::COUNT as u8 {
3045 let opcode = Opcode::from_byte(byte);
3046 assert!(
3047 opcode.is_some(),
3048 "Opcode::from_byte({byte}) returned None — gap in opcode enum"
3049 );
3050 let opcode = opcode.unwrap();
3051 let name = opcode.name();
3052 assert!(!name.is_empty(), "opcode {byte} has empty name");
3053 }
3054 assert_eq!(Opcode::from_byte(Opcode::COUNT as u8), None);
3055 }
3056
3057 #[test]
3059 fn test_p5_flags_u16_range() {
3060 let op = VdbeOp {
3062 opcode: Opcode::Eq,
3063 p1: 1,
3064 p2: 5,
3065 p3: 2,
3066 p4: P4::None,
3067 p5: 0x1FF, };
3069 assert_eq!(op.p5, 0x1FF);
3070 assert!(op.p5 > 255);
3071
3072 let op2 = VdbeOp {
3073 opcode: Opcode::Noop,
3074 p1: 0,
3075 p2: 0,
3076 p3: 0,
3077 p4: P4::None,
3078 p5: u16::MAX,
3079 };
3080 assert_eq!(op2.p5, 65535);
3081 }
3082
3083 #[test]
3085 fn test_program_builder_basic() {
3086 let mut b = ProgramBuilder::new();
3087
3088 let end_label = b.emit_label();
3090 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
3091 let r1 = b.alloc_reg();
3092 assert_eq!(r1, 1);
3093 b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
3094 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
3095 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3096 b.resolve_label(end_label);
3097
3098 let prog = b.finish().unwrap();
3099 assert_eq!(prog.len(), 4);
3100 assert_eq!(prog.register_count(), 1);
3101 assert_eq!(prog.max_bind_parameter_index().unwrap(), 0);
3102
3103 assert_eq!(prog.get(0).unwrap().opcode, Opcode::Init);
3105 assert_eq!(prog.get(0).unwrap().p2, 4);
3106 }
3107
3108 #[test]
3109 fn test_program_precomputes_max_bind_parameter_index() {
3110 let mut b = ProgramBuilder::new();
3111 b.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
3112 b.emit_op(Opcode::Variable, 4, 2, 0, P4::None, 0);
3113 b.emit_op(Opcode::Variable, 2, 3, 0, P4::None, 0);
3114 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3115 let prog = b.finish().unwrap();
3116 assert_eq!(prog.max_bind_parameter_index(), Ok(4));
3117 }
3118
3119 #[test]
3120 fn test_program_tracks_invalid_bind_parameter_index() {
3121 let mut b = ProgramBuilder::new();
3122 b.emit_op(Opcode::Variable, 0, 1, 0, P4::None, 0);
3123 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3124 let prog = b.finish().unwrap();
3125 assert_eq!(prog.max_bind_parameter_index(), Err(0));
3126 }
3127
3128 #[test]
3129 fn test_program_storage_only_hot_path_does_not_require_attached_memdb() {
3130 let mut b = ProgramBuilder::new();
3131 let end = b.emit_label();
3132 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3133 b.emit_op(Opcode::OpenWrite, 0, 256, 0, P4::Int(1), 0);
3134 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
3135 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
3136 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
3137 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
3138 b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
3139 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
3140 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3141 b.resolve_label(end);
3142
3143 let prog = match b.finish() {
3145 Ok(prog) => prog,
3146 Err(err) => {
3147 assert!(test_failure(), "program should build: {err}");
3148 return;
3149 }
3150 };
3151 assert!(
3152 !prog.requires_attached_memdb(),
3153 "storage-only table hot paths should not force a MemDatabase handoff"
3154 );
3155 }
3156
3157 #[test]
3158 fn test_program_with_ephemeral_cursor_requires_attached_memdb() {
3159 let mut b = ProgramBuilder::new();
3160 let end = b.emit_label();
3161 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3162 b.emit_op(Opcode::OpenEphemeral, 0, 1, 0, P4::None, 0);
3163 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3164 b.resolve_label(end);
3165
3166 let prog = b.finish().expect("program should build");
3167 assert!(
3168 prog.requires_attached_memdb(),
3169 "ephemeral table programs still depend on the attached MemDatabase"
3170 );
3171 }
3172
3173 #[test]
3174 fn test_program_with_sorter_cursor_does_not_require_attached_memdb() -> Result<()> {
3175 let mut b = ProgramBuilder::new();
3176 let end = b.emit_label();
3177 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3178 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::Str("+".to_owned()), 0);
3179 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
3180 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3181 b.resolve_label(end);
3182
3183 let prog = b.finish()?;
3184 assert!(
3185 !prog.requires_attached_memdb(),
3186 "sorter-backed temp/exchange state is owned by VDBE and should not force a MemDatabase handoff"
3187 );
3188 Ok(())
3189 }
3190
3191 #[test]
3192 fn test_program_builder_accumulates_table_index_meta_by_table_cursor() {
3193 use fsqlite_types::opcode::IndexCursorMeta;
3194
3195 let mut b = ProgramBuilder::new();
3196 b.register_table_indexes(
3197 3,
3198 vec![IndexCursorMeta {
3199 cursor_id: 4,
3200 column_indices: vec![0, 2],
3201 }],
3202 );
3203 b.register_table_indexes(
3204 3,
3205 vec![IndexCursorMeta {
3206 cursor_id: 5,
3207 column_indices: vec![1],
3208 }],
3209 );
3210
3211 let prog = b.finish().expect("program should build");
3212 let metas = prog
3213 .table_index_meta()
3214 .get(&3)
3215 .expect("table cursor metadata should be present");
3216 assert_eq!(metas.len(), 2);
3217 assert_eq!(metas[0].cursor_id, 4);
3218 assert_eq!(metas[0].column_indices, vec![0, 2]);
3219 assert_eq!(metas[1].cursor_id, 5);
3220 assert_eq!(metas[1].column_indices, vec![1]);
3221 }
3222
3223 #[test]
3225 fn test_disassemble() {
3226 let mut b = ProgramBuilder::new();
3227 b.emit_op(Opcode::Init, 0, 2, 0, P4::None, 0);
3228 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
3229 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3230 let prog = b.finish().unwrap();
3231
3232 let asm = prog.disassemble();
3233 assert!(asm.contains("Init"));
3234 assert!(asm.contains("Integer"));
3235 assert!(asm.contains("Halt"));
3236 assert!(asm.contains("42")); }
3238
3239 #[test]
3241 fn test_key_info() {
3242 let ki = KeyInfo {
3243 num_fields: 3,
3244 collations: vec![
3245 "BINARY".to_owned(),
3246 "NOCASE".to_owned(),
3247 "BINARY".to_owned(),
3248 ],
3249 sort_orders: vec![SortOrder::Asc, SortOrder::Desc, SortOrder::Asc],
3250 };
3251 assert_eq!(ki.num_fields, 3);
3252 assert_eq!(ki.collations.len(), 3);
3253 assert_eq!(ki.sort_orders[1], SortOrder::Desc);
3254 }
3255
3256 #[test]
3258 fn test_label_already_resolved() {
3259 let mut b = ProgramBuilder::new();
3262 let label = b.emit_label();
3263 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3264 b.resolve_label(label); b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3266
3267 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3269 assert_eq!(b.op_at(jump_addr).unwrap().p2, 1);
3271
3272 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3273
3274 let prog = b.finish().unwrap();
3275 assert_eq!(prog.len(), 4);
3276 }
3277
3278 #[test]
3280 fn test_builder_register_via_builder() {
3281 let mut b = ProgramBuilder::new();
3282 let r1 = b.alloc_reg();
3283 let r2 = b.alloc_reg();
3284 let block = b.alloc_regs(4);
3285 assert_eq!(r1, 1);
3286 assert_eq!(r2, 2);
3287 assert_eq!(block, 3);
3288 assert_eq!(b.register_count(), 6);
3289
3290 let t1 = b.alloc_temp();
3292 assert_eq!(t1, 7);
3293 b.free_temp(t1);
3294 let t2 = b.alloc_temp();
3295 assert_eq!(t2, t1); }
3297
3298 #[test]
3300 fn test_resolve_label_to_specific_address() {
3301 let mut b = ProgramBuilder::new();
3302 let label = b.emit_label();
3303 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3304 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3305 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3306
3307 b.resolve_label_to(label, 42);
3309 assert_eq!(b.op_at(jump_addr).unwrap().p2, 42);
3310 }
3311
3312 #[test]
3314 fn test_empty_program_finishes() {
3315 let b = ProgramBuilder::new();
3316 let prog = b.finish().unwrap();
3317 assert!(prog.is_empty());
3318 assert_eq!(prog.register_count(), 0);
3319 }
3320
3321 #[test]
3323 fn test_unreferenced_unresolved_label_ok() {
3324 let mut b = ProgramBuilder::new();
3327 let _label = b.emit_label();
3328 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3329 let prog = b.finish().unwrap();
3330 assert_eq!(prog.len(), 1);
3331 }
3332
3333 #[cfg(not(target_arch = "wasm32"))]
3336 use std::fs;
3337
3338 use fsqlite_ast::Statement;
3339 use fsqlite_error::FrankenError;
3340 use fsqlite_mvcc::{BeginKind, MvccError, TransactionManager};
3341 use fsqlite_parser::Parser;
3342 use fsqlite_types::{CommitSeq, ObjectId, Oti, PageData, PageNumber, PageSize};
3343 use fsqlite_wal::{
3344 DEFAULT_RAPTORQ_REPAIR_SYMBOLS, WalFecGroupMeta, WalFecGroupMetaInit, WalFecGroupRecord,
3345 WalFecRecoveryOutcome, WalFrameCandidate, WalSalts, append_wal_fec_group,
3346 build_source_page_hashes, generate_wal_fec_repair_symbols,
3347 recover_wal_fec_group_with_decoder, scan_wal_fec,
3348 };
3349 #[cfg(not(target_arch = "wasm32"))]
3350 use tempfile::tempdir;
3351
3352 fn parse_pragma(sql: &str) -> std::result::Result<fsqlite_ast::PragmaStatement, String> {
3353 let mut p = Parser::from_sql(sql);
3354 let stmt = p.parse_statement().expect("parse statement");
3355 match stmt {
3356 Statement::Pragma(p) => Ok(p),
3357 other => Err(format!("expected PRAGMA, got: {other:?}")),
3358 }
3359 }
3360
3361 fn test_page(first_byte: u8) -> PageData {
3362 let mut page = PageData::zeroed(PageSize::DEFAULT);
3363 page.as_bytes_mut()[0] = first_byte;
3364 page
3365 }
3366
3367 fn make_source_pages(seed: u8, k_source: u32) -> Vec<Vec<u8>> {
3368 let page_len = usize::try_from(PageSize::DEFAULT.get()).expect("page size fits usize");
3369 (0..k_source)
3370 .map(|idx| {
3371 let idx_u8 = u8::try_from(idx).expect("test k_source fits u8");
3372 let mut page = vec![seed.wrapping_add(idx_u8); page_len];
3373 page[0] = idx_u8;
3374 page
3375 })
3376 .collect()
3377 }
3378
3379 fn make_wal_fec_group(
3380 start_frame_no: u32,
3381 r_repair: u8,
3382 seed: u8,
3383 ) -> (WalFecGroupRecord, Vec<Vec<u8>>) {
3384 let k_source = 5_u32;
3385 let source_pages = make_source_pages(seed, k_source);
3386 let page_size = PageSize::DEFAULT.get();
3387 let source_hashes = build_source_page_hashes(&source_pages);
3388 let page_numbers = (0..k_source).map(|i| 10 + i).collect::<Vec<_>>();
3389 let oti = Oti {
3390 f: u64::from(k_source) * u64::from(page_size),
3391 al: 1,
3392 t: page_size,
3393 z: 1,
3394 n: 1,
3395 };
3396 let meta = WalFecGroupMeta::from_init(WalFecGroupMetaInit {
3397 wal_salt1: 0xA11C_E001,
3398 wal_salt2: 0xA11C_E002,
3399 start_frame_no,
3400 end_frame_no: start_frame_no + (k_source - 1),
3401 db_size_pages: 256,
3402 page_size,
3403 k_source,
3404 r_repair: u32::from(r_repair),
3405 oti,
3406 object_id: ObjectId::from_bytes([seed; 16]),
3407 page_numbers,
3408 source_page_xxh3_128: source_hashes,
3409 })
3410 .expect("meta");
3411 let repair_symbols =
3412 generate_wal_fec_repair_symbols(&meta, &source_pages).expect("symbols");
3413 (
3414 WalFecGroupRecord::new(meta, repair_symbols).expect("group"),
3415 source_pages,
3416 )
3417 }
3418
3419 #[test]
3420 fn test_pragma_serializable_query_returns_current_setting() {
3421 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3422
3423 let stmt = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3424 let out = pragma::apply(&mut mgr, &stmt).unwrap();
3425 assert_eq!(out, pragma::PragmaOutput::Bool(true));
3426 }
3427
3428 #[test]
3429 fn test_connection_pragma_differential_views_default_query_returns_false() {
3430 let mut state = pragma::ConnectionPragmaState::default();
3431
3432 let stmt = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3433 let out = pragma::apply_connection_pragma(&mut state, &stmt).expect("query pragma");
3434 assert_eq!(out, pragma::PragmaOutput::Bool(false));
3435 }
3436
3437 #[test]
3438 fn test_connection_pragma_differential_views_set_and_query_across_aliases() {
3439 let mut state = pragma::ConnectionPragmaState::default();
3440
3441 let set_on = parse_pragma("PRAGMA fsqlite.differential_views = ON").expect("parse pragma");
3442 assert_eq!(
3443 pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3444 pragma::PragmaOutput::Bool(true)
3445 );
3446 assert!(state.differential_views.is_enabled());
3447
3448 let query = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3449 assert_eq!(
3450 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3451 pragma::PragmaOutput::Bool(true)
3452 );
3453 }
3454
3455 #[test]
3456 fn test_connection_pragma_differential_views_rejects_non_boolean_values() {
3457 let mut state = pragma::ConnectionPragmaState::default();
3458
3459 let stmt = parse_pragma("PRAGMA fsqlite_differential_views = 2").expect("parse pragma");
3460 assert!(matches!(
3461 pragma::apply_connection_pragma(&mut state, &stmt),
3462 Err(FrankenError::TypeMismatch { .. })
3463 ));
3464 }
3465
3466 #[test]
3467 fn test_connection_pragma_query_only_set_and_query() {
3468 let mut state = pragma::ConnectionPragmaState::default();
3469
3470 let query = parse_pragma("PRAGMA query_only").expect("parse pragma");
3471 assert_eq!(
3472 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3473 pragma::PragmaOutput::Int(0)
3474 );
3475
3476 let set_on = parse_pragma("PRAGMA query_only = ON").expect("parse pragma");
3477 assert_eq!(
3478 pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3479 pragma::PragmaOutput::Int(1)
3480 );
3481 assert!(state.query_only);
3482
3483 assert_eq!(
3484 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3485 pragma::PragmaOutput::Int(1)
3486 );
3487 }
3488
3489 #[test]
3490 fn test_connection_pragma_query_only_rejects_non_boolean_values() {
3491 let mut state = pragma::ConnectionPragmaState::default();
3492
3493 let stmt = parse_pragma("PRAGMA query_only = 2").expect("parse pragma");
3494 assert!(matches!(
3495 pragma::apply_connection_pragma(&mut state, &stmt),
3496 Err(FrankenError::TypeMismatch { .. })
3497 ));
3498 }
3499
3500 fn apply_sql(state: &mut pragma::ConnectionPragmaState, sql: &str) -> pragma::PragmaOutput {
3501 let stmt = parse_pragma(sql).expect("parse pragma");
3502 pragma::apply_connection_pragma(state, &stmt).expect("apply pragma")
3503 }
3504
3505 #[test]
3506 fn test_connection_pragma_boolean_readbacks() {
3507 use pragma::PragmaOutput::Bool;
3509 let mut state = pragma::ConnectionPragmaState::default();
3510
3511 assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(true));
3513 assert_eq!(apply_sql(&mut state, "PRAGMA automatic_index"), Bool(true));
3514 assert_eq!(
3515 apply_sql(&mut state, "PRAGMA read_uncommitted"),
3516 Bool(false)
3517 );
3518 assert_eq!(apply_sql(&mut state, "PRAGMA cell_size_check"), Bool(false));
3519 assert_eq!(
3520 apply_sql(&mut state, "PRAGMA checkpoint_fullfsync"),
3521 Bool(false)
3522 );
3523
3524 assert_eq!(
3526 apply_sql(&mut state, "PRAGMA trusted_schema = OFF"),
3527 Bool(false)
3528 );
3529 assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(false));
3530 assert_eq!(
3531 apply_sql(&mut state, "PRAGMA read_uncommitted = ON"),
3532 Bool(true)
3533 );
3534 assert_eq!(
3535 apply_sql(&mut state, "PRAGMA cell_size_check = 1"),
3536 Bool(true)
3537 );
3538 assert_eq!(
3539 apply_sql(&mut state, "PRAGMA checkpoint_fullfsync = TRUE"),
3540 Bool(true)
3541 );
3542 assert_eq!(
3543 apply_sql(&mut state, "PRAGMA automatic_index = 0"),
3544 Bool(false)
3545 );
3546 }
3547
3548 #[test]
3549 fn test_connection_pragma_locking_mode_readback() {
3550 use pragma::PragmaOutput::Text;
3552 let mut state = pragma::ConnectionPragmaState::default();
3553 assert_eq!(
3554 apply_sql(&mut state, "PRAGMA locking_mode"),
3555 Text("normal".to_owned())
3556 );
3557 assert_eq!(
3558 apply_sql(&mut state, "PRAGMA locking_mode = EXCLUSIVE"),
3559 Text("exclusive".to_owned())
3560 );
3561 assert_eq!(
3562 apply_sql(&mut state, "PRAGMA locking_mode"),
3563 Text("exclusive".to_owned())
3564 );
3565 assert_eq!(
3567 apply_sql(&mut state, "PRAGMA locking_mode = bogus"),
3568 Text("exclusive".to_owned())
3569 );
3570 }
3571
3572 #[test]
3573 fn test_connection_pragma_secure_delete_tristate() {
3574 use pragma::PragmaOutput::Int;
3576 let mut state = pragma::ConnectionPragmaState::default();
3577 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete"), Int(0));
3578 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = ON"), Int(1));
3579 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = FAST"), Int(2));
3580 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = OFF"), Int(0));
3581 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = 2"), Int(2));
3582 }
3583
3584 #[test]
3585 fn test_connection_pragma_threads_readback() {
3586 use pragma::PragmaOutput::Int;
3589 let mut state = pragma::ConnectionPragmaState::default();
3590 assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(0));
3591 assert_eq!(apply_sql(&mut state, "PRAGMA threads = 4"), Int(4));
3592 assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(4));
3593 assert_eq!(apply_sql(&mut state, "PRAGMA threads = 100"), Int(8));
3595 assert_eq!(apply_sql(&mut state, "PRAGMA threads = -1"), Int(8));
3597 }
3598
3599 #[test]
3600 fn test_pragma_serializable_set_and_query() {
3601 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3602
3603 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3604 assert_eq!(
3605 pragma::apply(&mut mgr, &set_off).unwrap(),
3606 pragma::PragmaOutput::Bool(false)
3607 );
3608
3609 let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3610 assert_eq!(
3611 pragma::apply(&mut mgr, &query).unwrap(),
3612 pragma::PragmaOutput::Bool(false)
3613 );
3614 }
3615
3616 #[test]
3617 fn test_pragma_scope_per_connection_via_handler() {
3618 let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3619 let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3620
3621 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3622 let _ = pragma::apply(&mut conn_a, &set_off).unwrap();
3623
3624 let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3625 assert_eq!(
3626 pragma::apply(&mut conn_a, &query).unwrap(),
3627 pragma::PragmaOutput::Bool(false)
3628 );
3629 assert_eq!(
3630 pragma::apply(&mut conn_b, &query).unwrap(),
3631 pragma::PragmaOutput::Bool(true)
3632 );
3633 }
3634
3635 #[test]
3636 fn test_pragma_not_retroactive_to_active_txn_via_handler() {
3637 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3638
3639 let mut txn = mgr.begin(BeginKind::Concurrent).unwrap();
3640 mgr.write_page(&mut txn, PageNumber::new(1).unwrap(), test_page(0x01))
3641 .unwrap();
3642 txn.has_in_rw = true;
3643 txn.has_out_rw = true;
3644 assert!(txn.has_dangerous_structure());
3645
3646 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3648 let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3649
3650 assert_eq!(
3651 mgr.commit(&mut txn).unwrap_err(),
3652 MvccError::BusySnapshot,
3653 "PRAGMA change must not be retroactive to an active txn"
3654 );
3655 }
3656
3657 #[test]
3658 fn test_e2e_serializable_pragma_switch_changes_behavior() {
3659 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3660
3661 let set_on = parse_pragma("PRAGMA fsqlite.serializable = ON").expect("parse pragma");
3663 let _ = pragma::apply(&mut mgr, &set_on).unwrap();
3664
3665 let mut txn_on = mgr.begin(BeginKind::Concurrent).unwrap();
3666 mgr.write_page(&mut txn_on, PageNumber::new(1).unwrap(), test_page(0x10))
3667 .unwrap();
3668 txn_on.has_in_rw = true;
3669 txn_on.has_out_rw = true;
3670 assert_eq!(
3671 mgr.commit(&mut txn_on).unwrap_err(),
3672 MvccError::BusySnapshot,
3673 "serializable=ON must enforce SSI (abort)"
3674 );
3675
3676 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3678 let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3679
3680 let mut txn_off = mgr.begin(BeginKind::Concurrent).unwrap();
3681 mgr.write_page(&mut txn_off, PageNumber::new(2).unwrap(), test_page(0x20))
3682 .unwrap();
3683 txn_off.has_in_rw = true;
3684 txn_off.has_out_rw = true;
3685
3686 let seq = mgr.commit(&mut txn_off).unwrap();
3687 assert!(
3688 seq > CommitSeq::ZERO,
3689 "serializable=OFF must allow write skew"
3690 );
3691 }
3692
3693 #[test]
3694 fn test_pragma_raptorq_repair_symbols_default_query() {
3695 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3696 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3697 assert_eq!(
3698 pragma::apply(&mut mgr, &query).expect("query pragma"),
3699 pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3700 );
3701 }
3702
3703 #[cfg(not(target_arch = "wasm32"))]
3704 #[test]
3705 fn test_bd_1hi_12_unit_compliance_gate() {
3706 let dir = tempdir().expect("tempdir");
3707 let sidecar = dir.path().join("unit.wal-fec");
3708 let db_path = dir.path().join("unit.db");
3709 fs::write(&db_path, vec![0_u8; 100]).expect("seed db header");
3710
3711 let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3712 let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3713
3714 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3715 assert_eq!(
3716 pragma::apply_with_sidecar(&mut conn_a, &query, Some(&sidecar)).expect("query default"),
3717 pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
3718 );
3719
3720 let set_max = parse_pragma("PRAGMA raptorq_repair_symbols = 255").expect("parse set max");
3721 assert_eq!(
3722 pragma::apply_with_sidecar(&mut conn_a, &set_max, Some(&sidecar)).expect("set max"),
3723 pragma::PragmaOutput::Int(255)
3724 );
3725
3726 let set_too_high =
3727 parse_pragma("PRAGMA raptorq_repair_symbols = 256").expect("parse set too high");
3728 assert!(matches!(
3729 pragma::apply_with_sidecar(&mut conn_a, &set_too_high, Some(&sidecar)),
3730 Err(FrankenError::OutOfRange { .. })
3731 ));
3732
3733 let set_negative =
3734 parse_pragma("PRAGMA raptorq_repair_symbols = -1").expect("parse set negative");
3735 assert!(matches!(
3736 pragma::apply_with_sidecar(&mut conn_a, &set_negative, Some(&sidecar)),
3737 Err(FrankenError::OutOfRange { .. })
3738 ));
3739
3740 let set_non_integer =
3741 parse_pragma("PRAGMA raptorq_repair_symbols = ON").expect("parse set non-integer");
3742 assert!(matches!(
3743 pragma::apply_with_sidecar(&mut conn_a, &set_non_integer, Some(&sidecar)),
3744 Err(FrankenError::TypeMismatch { .. })
3745 ));
3746
3747 let query_new_conn = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3748 assert_eq!(
3749 pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3750 .expect("query persisted value"),
3751 pragma::PragmaOutput::Int(255)
3752 );
3753
3754 let set_shared = parse_pragma("PRAGMA raptorq_repair_symbols = 7").expect("parse shared");
3755 let _ = pragma::apply_with_sidecar(&mut conn_a, &set_shared, Some(&sidecar))
3756 .expect("persist shared setting");
3757 assert_eq!(
3758 pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
3759 .expect("cross-connection visibility"),
3760 pragma::PragmaOutput::Int(7)
3761 );
3762
3763 let db_bytes = fs::read(&db_path).expect("read db header");
3764 assert!(
3765 db_bytes[72..92].iter().all(|&byte| byte == 0),
3766 "sqlite header reserved bytes must remain untouched"
3767 );
3768 }
3769
3770 #[cfg(not(target_arch = "wasm32"))]
3771 #[test]
3772 fn prop_bd_1hi_12_structure_compliance() {
3773 let dir = tempdir().expect("tempdir");
3774 let sidecar = dir.path().join("property.wal-fec");
3775 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3776 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
3777
3778 for value in 0_u16..=255_u16 {
3779 let sql = format!("PRAGMA raptorq_repair_symbols = {value}");
3780 let set_stmt = parse_pragma(&sql).expect("parse set statement");
3781 assert_eq!(
3782 pragma::apply_with_sidecar(&mut mgr, &set_stmt, Some(&sidecar)).expect("set value"),
3783 pragma::PragmaOutput::Int(i64::from(value))
3784 );
3785 assert_eq!(
3786 pragma::apply_with_sidecar(&mut mgr, &query, Some(&sidecar)).expect("query value"),
3787 pragma::PragmaOutput::Int(i64::from(value))
3788 );
3789 }
3790 }
3791
3792 #[cfg(not(target_arch = "wasm32"))]
3793 #[test]
3794 #[allow(clippy::too_many_lines)]
3795 fn test_e2e_bd_1hi_12_compliance() {
3796 let dir = tempdir().expect("tempdir");
3797 let sidecar = dir.path().join("e2e.wal-fec");
3798 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3799
3800 let set_zero = parse_pragma("PRAGMA raptorq_repair_symbols = 0").expect("parse set 0");
3801 let _ = pragma::apply_with_sidecar(&mut mgr, &set_zero, Some(&sidecar)).expect("set 0");
3802 if mgr.raptorq_repair_symbols() > 0 {
3803 let (group, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x10);
3804 append_wal_fec_group(&sidecar, &group).expect("append group");
3805 }
3806 let after_zero = scan_wal_fec(&sidecar).expect("scan after zero");
3807 assert!(
3808 after_zero.groups.is_empty(),
3809 "N=0 must produce no .wal-fec groups for new commits"
3810 );
3811
3812 let set_one = parse_pragma("PRAGMA raptorq_repair_symbols = 1").expect("parse set 1");
3813 let _ = pragma::apply_with_sidecar(&mut mgr, &set_one, Some(&sidecar)).expect("set 1");
3814 let (group_r1, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x11);
3815 append_wal_fec_group(&sidecar, &group_r1).expect("append r=1 group");
3816
3817 let set_two = parse_pragma("PRAGMA raptorq_repair_symbols = 2").expect("parse set 2");
3818 let _ = pragma::apply_with_sidecar(&mut mgr, &set_two, Some(&sidecar)).expect("set 2");
3819 let (group_r2, _) = make_wal_fec_group(6, mgr.raptorq_repair_symbols(), 0x22);
3820 append_wal_fec_group(&sidecar, &group_r2).expect("append r=2 group");
3821
3822 let set_four = parse_pragma("PRAGMA raptorq_repair_symbols = 4").expect("parse set 4");
3823 let _ = pragma::apply_with_sidecar(&mut mgr, &set_four, Some(&sidecar)).expect("set 4");
3824 let (group_r4, source_pages_r4) =
3825 make_wal_fec_group(11, mgr.raptorq_repair_symbols(), 0x33);
3826 append_wal_fec_group(&sidecar, &group_r4).expect("append r=4 group");
3827
3828 let scan = scan_wal_fec(&sidecar).expect("scan sidecar");
3829 assert_eq!(scan.groups.len(), 3);
3830 assert_eq!(scan.groups[0].repair_symbols.len(), 1);
3831 assert_eq!(scan.groups[1].repair_symbols.len(), 2);
3832 assert_eq!(scan.groups[2].repair_symbols.len(), 4);
3833 assert_eq!(scan.groups[1].meta.r_repair, 2);
3834 assert_eq!(scan.groups[2].meta.r_repair, 4);
3835
3836 let group_id = group_r4.meta.group_id();
3837 let wal_salts = WalSalts {
3838 salt1: group_r4.meta.wal_salt1,
3839 salt2: group_r4.meta.wal_salt2,
3840 };
3841 let k_source = usize::try_from(group_r4.meta.k_source).expect("k fits usize");
3842
3843 let mut corrupt_three_frames = Vec::new();
3844 for (idx, page) in source_pages_r4.iter().enumerate() {
3845 let mut payload = page.clone();
3846 if idx < 3 {
3847 payload[0] ^= 0xFF;
3848 }
3849 corrupt_three_frames.push(WalFrameCandidate {
3850 frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3851 page_data: payload,
3852 });
3853 }
3854 let expected_pages = source_pages_r4.clone();
3855 let recovered = recover_wal_fec_group_with_decoder(
3856 &sidecar,
3857 group_id,
3858 wal_salts,
3859 group_r4.meta.start_frame_no,
3860 &corrupt_three_frames,
3861 move |meta: &WalFecGroupMeta, symbols| {
3862 if symbols.len() < usize::try_from(meta.k_source).expect("k fits usize") {
3863 return Err(FrankenError::WalCorrupt {
3864 detail: "insufficient symbols".to_owned(),
3865 });
3866 }
3867 Ok(expected_pages.clone())
3868 },
3869 )
3870 .expect("recover with <=R corruption");
3871 assert!(
3872 matches!(recovered, WalFecRecoveryOutcome::Recovered(_)),
3873 "expected recovered outcome"
3874 );
3875 let WalFecRecoveryOutcome::Recovered(group) = recovered else {
3876 unreachable!("asserted recovered outcome above");
3877 };
3878 assert_eq!(group.recovered_pages.len(), k_source);
3879
3880 let mut corrupt_five_frames = Vec::new();
3881 for (idx, page) in source_pages_r4.iter().enumerate() {
3882 let mut payload = page.clone();
3883 payload[0] ^= 0x55;
3884 corrupt_five_frames.push(WalFrameCandidate {
3885 frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
3886 page_data: payload,
3887 });
3888 }
3889 let truncated = recover_wal_fec_group_with_decoder(
3890 &sidecar,
3891 group_id,
3892 wal_salts,
3893 group_r4.meta.start_frame_no,
3894 &corrupt_five_frames,
3895 |_meta: &WalFecGroupMeta, _symbols| {
3896 Err(FrankenError::WalCorrupt {
3897 detail: "decoder should not be able to recover".to_owned(),
3898 })
3899 },
3900 )
3901 .expect("recover with >R corruption");
3902 assert!(matches!(
3903 truncated,
3904 WalFecRecoveryOutcome::TruncateBeforeGroup { .. }
3905 ));
3906 }
3907}