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