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 use std::sync::atomic::{AtomicI64, Ordering};
1491
1492 use fsqlite_ast::{Expr, Literal, PragmaStatement, PragmaValue, QualifiedName, UnaryOp};
1493 use fsqlite_error::{FrankenError, Result};
1494 use fsqlite_mvcc::TransactionManager;
1495 use fsqlite_wal::{
1496 DEFAULT_RAPTORQ_REPAIR_SYMBOLS, MAX_RAPTORQ_REPAIR_SYMBOLS,
1497 persist_wal_fec_raptorq_repair_symbols, read_wal_fec_raptorq_repair_symbols,
1498 };
1499 use tracing::{debug, error, info, warn};
1500
1501 static SOFT_HEAP_LIMIT: AtomicI64 = AtomicI64::new(0);
1505 static HARD_HEAP_LIMIT: AtomicI64 = AtomicI64::new(0);
1510
1511 #[derive(Debug, Clone, PartialEq, Eq)]
1513 pub enum PragmaOutput {
1514 Unsupported,
1516 Bool(bool),
1518 Int(i64),
1520 Text(String),
1522 }
1523
1524 #[derive(Debug, Clone, Copy)]
1531 pub enum DifferentialViewsSetting {
1532 Off,
1533 On,
1534 }
1535
1536 impl DifferentialViewsSetting {
1537 #[must_use]
1538 pub const fn is_enabled(&self) -> bool {
1539 matches!(self, Self::On)
1540 }
1541
1542 #[must_use]
1543 pub const fn from_enabled(enabled: bool) -> Self {
1544 if enabled { Self::On } else { Self::Off }
1545 }
1546 }
1547
1548 #[derive(Debug, Clone)]
1549 #[allow(clippy::struct_excessive_bools)]
1550 pub struct ConnectionPragmaState {
1551 pub journal_mode: String,
1553 pub synchronous: String,
1555 pub cache_size: i64,
1557 pub page_size: u32,
1559 pub busy_timeout_ms: i64,
1561 pub temp_store: i64,
1563 pub mmap_size: i64,
1565 pub auto_vacuum: i64,
1567 pub wal_autocheckpoint: i64,
1569 pub journal_size_limit: i64,
1575 pub user_version: i64,
1577 pub application_id: i64,
1579 pub default_cache_size: i64,
1584 pub foreign_keys: bool,
1586 pub recursive_triggers: bool,
1588 pub query_only: bool,
1590 pub serializable: bool,
1592 pub differential_views: DifferentialViewsSetting,
1594 pub raptorq_repair_symbols: u8,
1596 pub mvcc_max_chain_length: usize,
1599 pub mvcc_writer_lease_secs: u64,
1602 pub writable_schema: bool,
1604 pub case_sensitive_like: bool,
1607 pub trusted_schema: bool,
1610 pub read_uncommitted: bool,
1612 pub cell_size_check: bool,
1614 pub checkpoint_fullfsync: bool,
1616 pub automatic_index: bool,
1618 pub reverse_unordered_selects: bool,
1623 pub locking_mode: String,
1626 pub secure_delete: i64,
1629 pub threads: i64,
1633 pub cache_spill_pages: i64,
1638 pub cache_spill_enabled: bool,
1641 }
1642
1643 impl Default for ConnectionPragmaState {
1644 fn default() -> Self {
1645 Self {
1646 journal_mode: "wal".to_owned(),
1647 synchronous: "NORMAL".to_owned(),
1648 cache_size: -2000,
1649 page_size: 4096,
1650 busy_timeout_ms: 5000,
1651 temp_store: 0,
1652 mmap_size: 0,
1653 auto_vacuum: 0,
1654 wal_autocheckpoint: 1000,
1655 journal_size_limit: -1,
1656 user_version: 0,
1657 application_id: 0,
1658 default_cache_size: 0,
1659 foreign_keys: false,
1660 recursive_triggers: false,
1661 query_only: false,
1662 serializable: true,
1663 differential_views: DifferentialViewsSetting::Off,
1664 raptorq_repair_symbols: DEFAULT_RAPTORQ_REPAIR_SYMBOLS,
1665 mvcc_max_chain_length: 64,
1666 mvcc_writer_lease_secs: 30,
1667 writable_schema: false,
1668 case_sensitive_like: false,
1669 trusted_schema: true,
1670 read_uncommitted: false,
1671 cell_size_check: false,
1672 checkpoint_fullfsync: false,
1673 automatic_index: true,
1674 reverse_unordered_selects: false,
1675 locking_mode: "normal".to_owned(),
1676 secure_delete: 0,
1677 threads: 0,
1678 cache_spill_pages: 1,
1679 cache_spill_enabled: true,
1680 }
1681 }
1682 }
1683
1684 pub fn apply(mgr: &mut TransactionManager, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1694 apply_with_sidecar(mgr, stmt, None)
1695 }
1696
1697 pub fn apply_with_sidecar(
1699 mgr: &mut TransactionManager,
1700 stmt: &PragmaStatement,
1701 wal_fec_sidecar_path: Option<&Path>,
1702 ) -> Result<PragmaOutput> {
1703 if is_fsqlite_serializable(&stmt.name) {
1704 return apply_serializable(mgr, stmt);
1705 }
1706 if is_raptorq_repair_symbols(&stmt.name) {
1707 return apply_raptorq_repair_symbols(mgr, stmt, wal_fec_sidecar_path);
1708 }
1709 Ok(PragmaOutput::Unsupported)
1710 }
1711
1712 pub fn apply_connection_pragma(
1718 state: &mut ConnectionPragmaState,
1719 stmt: &PragmaStatement,
1720 ) -> Result<PragmaOutput> {
1721 let name = &stmt.name.name;
1722 if is_fsqlite_serializable(&stmt.name) {
1723 return apply_serializable_connection(state, stmt);
1724 }
1725 if is_fsqlite_differential_views(&stmt.name) {
1726 return apply_differential_views_connection(state, stmt);
1727 }
1728 if is_raptorq_repair_symbols(&stmt.name) {
1729 return apply_raptorq_repair_symbols_connection(state, stmt);
1730 }
1731 if name.eq_ignore_ascii_case("journal_mode") {
1732 return apply_journal_mode(state, stmt);
1733 }
1734 if name.eq_ignore_ascii_case("synchronous") {
1735 return apply_synchronous(state, stmt);
1736 }
1737 if name.eq_ignore_ascii_case("cache_size") {
1738 return apply_cache_size(state, stmt);
1739 }
1740 if name.eq_ignore_ascii_case("page_size") {
1741 return apply_page_size(state, stmt);
1742 }
1743 if name.eq_ignore_ascii_case("cache_spill") {
1744 return Ok(apply_cache_spill(state, stmt));
1745 }
1746 if name.eq_ignore_ascii_case("busy_timeout") {
1747 return apply_busy_timeout(state, stmt);
1748 }
1749 if name.eq_ignore_ascii_case("temp_store") {
1750 return apply_temp_store(state, stmt);
1751 }
1752 if name.eq_ignore_ascii_case("mmap_size") {
1753 return apply_mmap_size(state, stmt);
1754 }
1755 if name.eq_ignore_ascii_case("auto_vacuum") {
1756 return apply_auto_vacuum(state, stmt);
1757 }
1758 if name.eq_ignore_ascii_case("wal_autocheckpoint") {
1759 return apply_wal_autocheckpoint(state, stmt);
1760 }
1761 if name.eq_ignore_ascii_case("journal_size_limit") {
1762 return apply_journal_size_limit(state, stmt);
1763 }
1764 if name.eq_ignore_ascii_case("user_version") {
1765 return apply_user_version(state, stmt);
1766 }
1767 if name.eq_ignore_ascii_case("application_id") {
1768 return apply_application_id(state, stmt);
1769 }
1770 if name.eq_ignore_ascii_case("default_cache_size") {
1771 return apply_default_cache_size(state, stmt);
1772 }
1773 if name.eq_ignore_ascii_case("foreign_keys") {
1774 return apply_foreign_keys(state, stmt);
1775 }
1776 if name.eq_ignore_ascii_case("recursive_triggers") {
1777 return apply_recursive_triggers(state, stmt);
1778 }
1779 if name.eq_ignore_ascii_case("query_only") {
1780 return apply_query_only(state, stmt);
1781 }
1782 if name.eq_ignore_ascii_case("writable_schema") {
1783 return apply_writable_schema(state, stmt);
1784 }
1785 if name.eq_ignore_ascii_case("case_sensitive_like") {
1786 return apply_case_sensitive_like(state, stmt);
1787 }
1788 if name.eq_ignore_ascii_case("trusted_schema") {
1789 return apply_bool_toggle(&mut state.trusted_schema, stmt);
1790 }
1791 if name.eq_ignore_ascii_case("read_uncommitted") {
1792 return apply_bool_toggle(&mut state.read_uncommitted, stmt);
1793 }
1794 if name.eq_ignore_ascii_case("cell_size_check") {
1795 return apply_bool_toggle(&mut state.cell_size_check, stmt);
1796 }
1797 if name.eq_ignore_ascii_case("checkpoint_fullfsync") {
1798 return apply_bool_toggle(&mut state.checkpoint_fullfsync, stmt);
1799 }
1800 if name.eq_ignore_ascii_case("automatic_index") {
1801 return apply_bool_toggle(&mut state.automatic_index, stmt);
1802 }
1803 if name.eq_ignore_ascii_case("reverse_unordered_selects") {
1804 return apply_bool_toggle(&mut state.reverse_unordered_selects, stmt);
1805 }
1806 if name.eq_ignore_ascii_case("soft_heap_limit") {
1807 return Ok(apply_soft_heap_limit(stmt));
1808 }
1809 if name.eq_ignore_ascii_case("hard_heap_limit") {
1810 return Ok(apply_hard_heap_limit(stmt));
1811 }
1812 if name.eq_ignore_ascii_case("locking_mode") {
1813 return apply_locking_mode(state, stmt);
1814 }
1815 if name.eq_ignore_ascii_case("secure_delete") {
1816 return apply_secure_delete(state, stmt);
1817 }
1818 if name.eq_ignore_ascii_case("threads") {
1819 return apply_threads(state, stmt);
1820 }
1821 if is_fsqlite_mvcc_max_chain_length(&stmt.name) {
1822 return apply_mvcc_max_chain_length(state, stmt);
1823 }
1824 if is_fsqlite_mvcc_writer_lease_secs(&stmt.name) {
1825 return apply_mvcc_writer_lease_secs(state, stmt);
1826 }
1827 Ok(PragmaOutput::Unsupported)
1828 }
1829
1830 fn apply_serializable_connection(
1831 state: &mut ConnectionPragmaState,
1832 stmt: &PragmaStatement,
1833 ) -> Result<PragmaOutput> {
1834 match &stmt.value {
1835 None => Ok(PragmaOutput::Bool(state.serializable)),
1836 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1837 let enabled = parse_bool(expr)?;
1838 state.serializable = enabled;
1839 Ok(PragmaOutput::Bool(enabled))
1840 }
1841 }
1842 }
1843
1844 fn apply_bool_toggle(flag: &mut bool, stmt: &PragmaStatement) -> Result<PragmaOutput> {
1850 match &stmt.value {
1851 None => Ok(PragmaOutput::Bool(*flag)),
1852 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1853 let enabled = parse_bool(expr)?;
1854 *flag = enabled;
1855 Ok(PragmaOutput::Bool(enabled))
1856 }
1857 }
1858 }
1859
1860 fn apply_soft_heap_limit(stmt: &PragmaStatement) -> PragmaOutput {
1868 if let Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) = &stmt.value
1869 && let Ok(n) = parse_integer_expr(expr)
1870 && n >= 0
1871 {
1872 let hard = HARD_HEAP_LIMIT.load(Ordering::Relaxed);
1873 let effective = if hard > 0 && (n > hard || n == 0) { hard } else { n };
1874 SOFT_HEAP_LIMIT.store(effective, Ordering::Relaxed);
1875 }
1876 PragmaOutput::Int(SOFT_HEAP_LIMIT.load(Ordering::Relaxed))
1877 }
1878
1879 fn apply_hard_heap_limit(stmt: &PragmaStatement) -> PragmaOutput {
1887 if let Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) = &stmt.value
1888 && let Ok(n) = parse_integer_expr(expr)
1889 {
1890 let prior = HARD_HEAP_LIMIT.load(Ordering::Relaxed);
1891 if n > 0 && (prior == 0 || prior > n) {
1892 HARD_HEAP_LIMIT.store(n, Ordering::Relaxed);
1893 let soft = SOFT_HEAP_LIMIT.load(Ordering::Relaxed);
1894 if n < soft || soft == 0 {
1895 SOFT_HEAP_LIMIT.store(n, Ordering::Relaxed);
1896 }
1897 }
1898 }
1899 PragmaOutput::Int(HARD_HEAP_LIMIT.load(Ordering::Relaxed))
1900 }
1901
1902 #[cfg(test)]
1907 pub(crate) fn reset_heap_limits_for_test() {
1908 SOFT_HEAP_LIMIT.store(0, Ordering::Relaxed);
1909 HARD_HEAP_LIMIT.store(0, Ordering::Relaxed);
1910 }
1911
1912 fn apply_locking_mode(
1917 state: &mut ConnectionPragmaState,
1918 stmt: &PragmaStatement,
1919 ) -> Result<PragmaOutput> {
1920 match &stmt.value {
1921 None => Ok(PragmaOutput::Text(state.locking_mode.clone())),
1922 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1923 let requested = parse_text_expr(expr)?.to_ascii_lowercase();
1924 if requested == "normal" || requested == "exclusive" {
1925 state.locking_mode = requested;
1926 }
1927 Ok(PragmaOutput::Text(state.locking_mode.clone()))
1928 }
1929 }
1930 }
1931
1932 fn apply_secure_delete(
1937 state: &mut ConnectionPragmaState,
1938 stmt: &PragmaStatement,
1939 ) -> Result<PragmaOutput> {
1940 match &stmt.value {
1941 None => Ok(PragmaOutput::Int(state.secure_delete)),
1942 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1943 let value = parse_secure_delete_value(expr)?;
1944 state.secure_delete = value;
1945 Ok(PragmaOutput::Int(value))
1946 }
1947 }
1948 }
1949
1950 fn parse_secure_delete_value(expr: &Expr) -> Result<i64> {
1951 if let Expr::Literal(Literal::Integer(n), _) = expr {
1952 return match *n {
1953 0..=2 => Ok(*n),
1954 _ => Err(FrankenError::OutOfRange {
1955 what: "secure_delete".to_owned(),
1956 value: n.to_string(),
1957 }),
1958 };
1959 }
1960 if let Ok(text) = parse_text_expr(expr)
1961 && text.eq_ignore_ascii_case("fast")
1962 {
1963 return Ok(2);
1964 }
1965 Ok(i64::from(parse_bool(expr)?))
1967 }
1968
1969 fn apply_threads(
1974 state: &mut ConnectionPragmaState,
1975 stmt: &PragmaStatement,
1976 ) -> Result<PragmaOutput> {
1977 const MAX_WORKER_THREADS: i64 = 8;
1979 match &stmt.value {
1980 None => Ok(PragmaOutput::Int(state.threads)),
1981 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
1982 let n = parse_integer_expr(expr)?;
1983 if n >= 0 {
1984 state.threads = n.min(MAX_WORKER_THREADS);
1985 }
1986 Ok(PragmaOutput::Int(state.threads))
1987 }
1988 }
1989 }
1990
1991 fn apply_case_sensitive_like(
1997 state: &mut ConnectionPragmaState,
1998 stmt: &PragmaStatement,
1999 ) -> Result<PragmaOutput> {
2000 match &stmt.value {
2001 None => Ok(PragmaOutput::Bool(state.case_sensitive_like)),
2002 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2003 let enabled = parse_bool(expr)?;
2004 state.case_sensitive_like = enabled;
2005 Ok(PragmaOutput::Bool(enabled))
2006 }
2007 }
2008 }
2009
2010 fn apply_raptorq_repair_symbols_connection(
2011 state: &mut ConnectionPragmaState,
2012 stmt: &PragmaStatement,
2013 ) -> Result<PragmaOutput> {
2014 match &stmt.value {
2015 None => Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols))),
2016 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2017 let value = parse_integer_expr(expr)?;
2018 if !(0..=i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS)).contains(&value) {
2019 return Err(FrankenError::OutOfRange {
2020 what: "raptorq_repair_symbols".to_owned(),
2021 value: value.to_string(),
2022 });
2023 }
2024 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2025 {
2026 state.raptorq_repair_symbols = value as u8;
2027 }
2028 Ok(PragmaOutput::Int(i64::from(state.raptorq_repair_symbols)))
2029 }
2030 }
2031 }
2032
2033 fn apply_differential_views_connection(
2034 state: &mut ConnectionPragmaState,
2035 stmt: &PragmaStatement,
2036 ) -> Result<PragmaOutput> {
2037 match &stmt.value {
2038 None => Ok(PragmaOutput::Bool(state.differential_views.is_enabled())),
2039 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2040 let enabled = parse_bool(expr)?;
2041 state.differential_views = DifferentialViewsSetting::from_enabled(enabled);
2042 Ok(PragmaOutput::Bool(enabled))
2043 }
2044 }
2045 }
2046
2047 fn apply_journal_mode(
2048 state: &mut ConnectionPragmaState,
2049 stmt: &PragmaStatement,
2050 ) -> Result<PragmaOutput> {
2051 match &stmt.value {
2052 None => Ok(PragmaOutput::Text(state.journal_mode.clone())),
2053 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2054 let mode = parse_text_expr(expr)?;
2055 let lower = mode.to_ascii_lowercase();
2056 match lower.as_str() {
2057 "delete" | "truncate" | "persist" | "memory" | "wal" | "off" => {
2058 state.journal_mode.clone_from(&lower);
2059 Ok(PragmaOutput::Text(lower))
2060 }
2061 _ => Err(FrankenError::TypeMismatch {
2062 expected: "delete|truncate|persist|memory|wal|off".to_owned(),
2063 actual: mode,
2064 }),
2065 }
2066 }
2067 }
2068 }
2069
2070 fn apply_synchronous(
2071 state: &mut ConnectionPragmaState,
2072 stmt: &PragmaStatement,
2073 ) -> Result<PragmaOutput> {
2074 match &stmt.value {
2075 None => Ok(PragmaOutput::Text(state.synchronous.clone())),
2076 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2077 let val = parse_synchronous_value(expr)?;
2078 state.synchronous.clone_from(&val);
2079 Ok(PragmaOutput::Text(val))
2080 }
2081 }
2082 }
2083
2084 fn parse_synchronous_value(expr: &Expr) -> Result<String> {
2085 if let Expr::Literal(Literal::Integer(n), _) = expr {
2087 match n {
2088 0 => Ok("OFF".to_owned()),
2089 1 => Ok("NORMAL".to_owned()),
2090 2 => Ok("FULL".to_owned()),
2091 3 => Ok("EXTRA".to_owned()),
2092 _ => Err(FrankenError::OutOfRange {
2093 what: "synchronous".to_owned(),
2094 value: n.to_string(),
2095 }),
2096 }
2097 } else {
2098 let text = parse_text_expr(expr)?;
2099 let upper = text.to_ascii_uppercase();
2100 match upper.as_str() {
2101 "OFF" | "NORMAL" | "FULL" | "EXTRA" => Ok(upper),
2102 _ => Err(FrankenError::TypeMismatch {
2103 expected: "OFF|NORMAL|FULL|EXTRA|0|1|2|3".to_owned(),
2104 actual: text,
2105 }),
2106 }
2107 }
2108 }
2109
2110 fn apply_cache_size(
2111 state: &mut ConnectionPragmaState,
2112 stmt: &PragmaStatement,
2113 ) -> Result<PragmaOutput> {
2114 match &stmt.value {
2115 None => Ok(PragmaOutput::Int(state.cache_size)),
2116 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2117 let val = parse_integer_expr(expr)?;
2118 state.cache_size = val;
2119 Ok(PragmaOutput::Int(val))
2120 }
2121 }
2122 }
2123
2124 fn apply_page_size(
2125 state: &mut ConnectionPragmaState,
2126 stmt: &PragmaStatement,
2127 ) -> Result<PragmaOutput> {
2128 match &stmt.value {
2129 None => Ok(PragmaOutput::Int(i64::from(state.page_size))),
2130 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2131 let val = parse_integer_expr(expr)?;
2132 if !(512..=65536).contains(&val) || !is_power_of_two(val) {
2133 return Err(FrankenError::OutOfRange {
2134 what: "page_size".to_owned(),
2135 value: val.to_string(),
2136 });
2137 }
2138 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2139 {
2140 state.page_size = val as u32;
2141 }
2142 Ok(PragmaOutput::Int(val))
2143 }
2144 }
2145 }
2146
2147 fn cache_pages_from_size(cache_size: i64, page_size: u32) -> i64 {
2156 if cache_size >= 0 {
2157 cache_size
2158 } else {
2159 let page_bytes = i64::from(page_size).max(1);
2160 cache_size.saturating_mul(-1024) / page_bytes
2161 }
2162 }
2163
2164 fn apply_cache_spill(state: &mut ConnectionPragmaState, stmt: &PragmaStatement) -> PragmaOutput {
2175 if let Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) = &stmt.value {
2176 let n = parse_integer_expr(expr).unwrap_or(0);
2177 if n > 0 {
2178 state.cache_spill_pages = n;
2179 } else if n < 0 {
2180 let page_bytes = i64::from(state.page_size).max(1);
2181 state.cache_spill_pages = n.saturating_mul(-1024) / page_bytes;
2182 }
2183 state.cache_spill_enabled = parse_bool(expr).unwrap_or(n != 0);
2184 }
2185 let value = if state.cache_spill_enabled {
2186 cache_pages_from_size(state.cache_size, state.page_size).max(state.cache_spill_pages)
2187 } else {
2188 0
2189 };
2190 PragmaOutput::Int(value)
2191 }
2192
2193 fn apply_busy_timeout(
2194 state: &mut ConnectionPragmaState,
2195 stmt: &PragmaStatement,
2196 ) -> Result<PragmaOutput> {
2197 match &stmt.value {
2198 None => Ok(PragmaOutput::Int(state.busy_timeout_ms)),
2199 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2200 let val = parse_integer_expr(expr)?;
2201 state.busy_timeout_ms = val.max(0);
2202 Ok(PragmaOutput::Int(state.busy_timeout_ms))
2203 }
2204 }
2205 }
2206
2207 fn apply_temp_store(
2208 state: &mut ConnectionPragmaState,
2209 stmt: &PragmaStatement,
2210 ) -> Result<PragmaOutput> {
2211 match &stmt.value {
2212 None => Ok(PragmaOutput::Int(state.temp_store)),
2213 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2214 let val = parse_temp_store_value(expr)?;
2215 state.temp_store = val;
2216 Ok(PragmaOutput::Int(val))
2217 }
2218 }
2219 }
2220
2221 fn apply_mmap_size(
2222 state: &mut ConnectionPragmaState,
2223 stmt: &PragmaStatement,
2224 ) -> Result<PragmaOutput> {
2225 match &stmt.value {
2226 None => Ok(PragmaOutput::Int(state.mmap_size)),
2227 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2228 let val = parse_integer_expr(expr)?;
2229 state.mmap_size = val.max(0);
2230 Ok(PragmaOutput::Int(state.mmap_size))
2231 }
2232 }
2233 }
2234
2235 fn apply_auto_vacuum(
2236 state: &mut ConnectionPragmaState,
2237 stmt: &PragmaStatement,
2238 ) -> Result<PragmaOutput> {
2239 match &stmt.value {
2240 None => Ok(PragmaOutput::Int(state.auto_vacuum)),
2241 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2242 let val = parse_auto_vacuum_value(expr)?;
2243 state.auto_vacuum = val;
2244 Ok(PragmaOutput::Int(val))
2245 }
2246 }
2247 }
2248
2249 fn apply_wal_autocheckpoint(
2250 state: &mut ConnectionPragmaState,
2251 stmt: &PragmaStatement,
2252 ) -> Result<PragmaOutput> {
2253 match &stmt.value {
2254 None => Ok(PragmaOutput::Int(state.wal_autocheckpoint)),
2255 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2256 let val = parse_integer_expr(expr)?;
2257 state.wal_autocheckpoint = val.max(0);
2258 Ok(PragmaOutput::Int(state.wal_autocheckpoint))
2259 }
2260 }
2261 }
2262
2263 fn apply_journal_size_limit(
2264 state: &mut ConnectionPragmaState,
2265 stmt: &PragmaStatement,
2266 ) -> Result<PragmaOutput> {
2267 match &stmt.value {
2268 None => Ok(PragmaOutput::Int(state.journal_size_limit)),
2269 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2270 let val = parse_integer_expr(expr)?;
2271 state.journal_size_limit = val;
2275 Ok(PragmaOutput::Int(state.journal_size_limit))
2276 }
2277 }
2278 }
2279
2280 fn apply_user_version(
2281 state: &mut ConnectionPragmaState,
2282 stmt: &PragmaStatement,
2283 ) -> Result<PragmaOutput> {
2284 match &stmt.value {
2285 None => Ok(PragmaOutput::Int(state.user_version)),
2286 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2287 let val = i64::from(i32::try_from(parse_integer_expr(expr)?).unwrap_or(0));
2291 state.user_version = val;
2292 Ok(PragmaOutput::Int(val))
2293 }
2294 }
2295 }
2296
2297 fn apply_application_id(
2298 state: &mut ConnectionPragmaState,
2299 stmt: &PragmaStatement,
2300 ) -> Result<PragmaOutput> {
2301 match &stmt.value {
2302 None => Ok(PragmaOutput::Int(state.application_id)),
2303 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2304 let val = i64::from(i32::try_from(parse_integer_expr(expr)?).unwrap_or(0));
2307 state.application_id = val;
2308 Ok(PragmaOutput::Int(val))
2309 }
2310 }
2311 }
2312
2313 fn apply_default_cache_size(
2321 state: &mut ConnectionPragmaState,
2322 stmt: &PragmaStatement,
2323 ) -> Result<PragmaOutput> {
2324 let report = |stored: i64| if stored == 0 { -2000 } else { stored };
2326 match &stmt.value {
2327 None => Ok(PragmaOutput::Int(report(state.default_cache_size))),
2328 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2329 let stored = parse_integer_expr(expr)?.saturating_abs();
2330 state.default_cache_size = stored;
2331 state.cache_size = stored;
2332 Ok(PragmaOutput::Int(report(stored)))
2333 }
2334 }
2335 }
2336
2337 fn apply_foreign_keys(
2338 state: &mut ConnectionPragmaState,
2339 stmt: &PragmaStatement,
2340 ) -> Result<PragmaOutput> {
2341 match &stmt.value {
2342 None => Ok(PragmaOutput::Int(i64::from(state.foreign_keys))),
2343 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2344 let enabled = parse_bool(expr)?;
2345 state.foreign_keys = enabled;
2346 Ok(PragmaOutput::Int(i64::from(enabled)))
2347 }
2348 }
2349 }
2350
2351 fn apply_recursive_triggers(
2352 state: &mut ConnectionPragmaState,
2353 stmt: &PragmaStatement,
2354 ) -> Result<PragmaOutput> {
2355 match &stmt.value {
2356 None => Ok(PragmaOutput::Int(i64::from(state.recursive_triggers))),
2357 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2358 let enabled = parse_bool(expr)?;
2359 state.recursive_triggers = enabled;
2360 Ok(PragmaOutput::Int(i64::from(enabled)))
2361 }
2362 }
2363 }
2364
2365 fn apply_query_only(
2366 state: &mut ConnectionPragmaState,
2367 stmt: &PragmaStatement,
2368 ) -> Result<PragmaOutput> {
2369 match &stmt.value {
2370 None => Ok(PragmaOutput::Int(i64::from(state.query_only))),
2371 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2372 let enabled = parse_bool(expr)?;
2373 state.query_only = enabled;
2374 Ok(PragmaOutput::Int(i64::from(enabled)))
2375 }
2376 }
2377 }
2378
2379 fn apply_writable_schema(
2380 state: &mut ConnectionPragmaState,
2381 stmt: &PragmaStatement,
2382 ) -> Result<PragmaOutput> {
2383 match &stmt.value {
2384 None => Ok(PragmaOutput::Int(i64::from(state.writable_schema))),
2385 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2386 if expr_is_reset(expr) {
2394 state.writable_schema = false;
2395 return Ok(PragmaOutput::Int(0));
2396 }
2397 let enabled = parse_bool(expr)?;
2398 state.writable_schema = enabled;
2399 Ok(PragmaOutput::Int(i64::from(enabled)))
2400 }
2401 }
2402 }
2403
2404 fn expr_is_reset(expr: &Expr) -> bool {
2407 match expr {
2408 Expr::Column(col, _) => col.column.eq_ignore_ascii_case("reset"),
2409 Expr::Literal(Literal::String(s), _) => s.eq_ignore_ascii_case("reset"),
2410 _ => false,
2411 }
2412 }
2413
2414 fn parse_temp_store_value(expr: &Expr) -> Result<i64> {
2415 if let Expr::Literal(Literal::Integer(n), _) = expr {
2416 return match *n {
2417 0..=2 => Ok(*n),
2418 _ => Err(FrankenError::OutOfRange {
2419 what: "temp_store".to_owned(),
2420 value: n.to_string(),
2421 }),
2422 };
2423 }
2424
2425 let text = parse_text_expr(expr)?;
2426 match text.to_ascii_lowercase().as_str() {
2427 "default" => Ok(0),
2428 "file" => Ok(1),
2429 "memory" => Ok(2),
2430 _ => Err(FrankenError::TypeMismatch {
2431 expected: "DEFAULT|FILE|MEMORY|0|1|2".to_owned(),
2432 actual: text,
2433 }),
2434 }
2435 }
2436
2437 fn parse_auto_vacuum_value(expr: &Expr) -> Result<i64> {
2438 if let Expr::Literal(Literal::Integer(n), _) = expr {
2439 return match *n {
2440 0..=2 => Ok(*n),
2441 _ => Err(FrankenError::OutOfRange {
2442 what: "auto_vacuum".to_owned(),
2443 value: n.to_string(),
2444 }),
2445 };
2446 }
2447
2448 let text = parse_text_expr(expr)?;
2449 match text.to_ascii_lowercase().as_str() {
2450 "none" => Ok(0),
2451 "full" => Ok(1),
2452 "incremental" => Ok(2),
2453 _ => Err(FrankenError::TypeMismatch {
2454 expected: "NONE|FULL|INCREMENTAL|0|1|2".to_owned(),
2455 actual: text,
2456 }),
2457 }
2458 }
2459
2460 fn is_power_of_two(n: i64) -> bool {
2461 n > 0 && (n & (n - 1)) == 0
2462 }
2463
2464 fn parse_text_expr(expr: &Expr) -> Result<String> {
2466 match expr {
2467 Expr::Literal(Literal::String(s), _) => Ok(s.clone()),
2468 Expr::Column(col, _) => Ok(col.column.to_string()),
2469 Expr::Literal(Literal::Integer(n), _) => Ok(n.to_string()),
2470 other => Err(FrankenError::TypeMismatch {
2471 expected: "text or identifier".to_owned(),
2472 actual: format!("{other:?}"),
2473 }),
2474 }
2475 }
2476
2477 fn apply_serializable(
2478 mgr: &mut TransactionManager,
2479 stmt: &PragmaStatement,
2480 ) -> Result<PragmaOutput> {
2481 match &stmt.value {
2482 None => Ok(PragmaOutput::Bool(mgr.ssi_enabled())),
2483 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2484 let enabled = parse_bool(expr)?;
2485 mgr.set_ssi_enabled(enabled);
2486 Ok(PragmaOutput::Bool(mgr.ssi_enabled()))
2487 }
2488 }
2489 }
2490
2491 fn is_fsqlite_serializable(name: &QualifiedName) -> bool {
2492 name.schema
2493 .as_deref()
2494 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2495 && name.name.eq_ignore_ascii_case("serializable")
2496 }
2497
2498 fn is_fsqlite_differential_views(name: &QualifiedName) -> bool {
2499 match name.schema.as_deref() {
2500 Some(schema) => {
2501 schema.eq_ignore_ascii_case("fsqlite")
2502 && name.name.eq_ignore_ascii_case("differential_views")
2503 }
2504 None => name.name.eq_ignore_ascii_case("fsqlite_differential_views"),
2505 }
2506 }
2507
2508 fn is_raptorq_repair_symbols(name: &QualifiedName) -> bool {
2509 let schema_ok = match name.schema.as_deref() {
2510 None => true,
2511 Some(schema) => schema.eq_ignore_ascii_case("fsqlite"),
2512 };
2513 schema_ok && name.name.eq_ignore_ascii_case("raptorq_repair_symbols")
2514 }
2515
2516 fn is_fsqlite_mvcc_max_chain_length(name: &QualifiedName) -> bool {
2517 name.schema
2518 .as_deref()
2519 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2520 && name.name.eq_ignore_ascii_case("mvcc_max_chain_length")
2521 }
2522
2523 fn is_fsqlite_mvcc_writer_lease_secs(name: &QualifiedName) -> bool {
2524 name.schema
2525 .as_deref()
2526 .is_some_and(|s| s.eq_ignore_ascii_case("fsqlite"))
2527 && name.name.eq_ignore_ascii_case("mvcc_writer_lease_secs")
2528 }
2529
2530 fn apply_mvcc_max_chain_length(
2531 state: &mut ConnectionPragmaState,
2532 stmt: &PragmaStatement,
2533 ) -> Result<PragmaOutput> {
2534 match &stmt.value {
2535 None => Ok(PragmaOutput::Int(state.mvcc_max_chain_length as i64)),
2536 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2537 let value = parse_integer_expr(expr)?;
2538 if value < 2 {
2539 return Err(FrankenError::OutOfRange {
2540 what: "fsqlite.mvcc_max_chain_length".into(),
2541 value: format!("{value} (minimum 2)"),
2542 });
2543 }
2544 #[allow(clippy::cast_sign_loss)]
2545 {
2546 state.mvcc_max_chain_length = value as usize;
2547 }
2548 Ok(PragmaOutput::Int(value))
2554 }
2555 }
2556 }
2557
2558 fn apply_mvcc_writer_lease_secs(
2559 state: &mut ConnectionPragmaState,
2560 stmt: &PragmaStatement,
2561 ) -> Result<PragmaOutput> {
2562 match &stmt.value {
2563 None => Ok(PragmaOutput::Int(state.mvcc_writer_lease_secs as i64)),
2564 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2565 let value = parse_integer_expr(expr)?;
2566 if value < 1 {
2567 return Err(FrankenError::OutOfRange {
2568 what: "fsqlite.mvcc_writer_lease_secs".into(),
2569 value: format!("{value} (minimum 1)"),
2570 });
2571 }
2572 #[allow(clippy::cast_sign_loss)]
2573 {
2574 state.mvcc_writer_lease_secs = value as u64;
2575 }
2576 Ok(PragmaOutput::Int(value))
2578 }
2579 }
2580 }
2581
2582 fn apply_raptorq_repair_symbols(
2583 mgr: &mut TransactionManager,
2584 stmt: &PragmaStatement,
2585 wal_fec_sidecar_path: Option<&Path>,
2586 ) -> Result<PragmaOutput> {
2587 match &stmt.value {
2588 None => {
2589 if let Some(sidecar) = wal_fec_sidecar_path {
2590 let persisted = read_wal_fec_raptorq_repair_symbols(sidecar)?;
2591 mgr.set_raptorq_repair_symbols(persisted);
2592 debug!(
2593 sidecar = %sidecar.display(),
2594 raptorq_repair_symbols = persisted,
2595 "loaded raptorq_repair_symbols from wal-fec sidecar"
2596 );
2597 }
2598 Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2599 }
2600 Some(PragmaValue::Assign(expr) | PragmaValue::Call(expr)) => {
2601 let requested = parse_raptorq_repair_symbols(expr)?;
2602 mgr.set_raptorq_repair_symbols(requested);
2603
2604 if let Some(sidecar) = wal_fec_sidecar_path {
2605 persist_wal_fec_raptorq_repair_symbols(sidecar, requested)?;
2606 info!(
2607 sidecar = %sidecar.display(),
2608 raptorq_repair_symbols = requested,
2609 "persisted raptorq_repair_symbols to wal-fec sidecar"
2610 );
2611 }
2612
2613 Ok(PragmaOutput::Int(i64::from(mgr.raptorq_repair_symbols())))
2614 }
2615 }
2616 }
2617
2618 fn parse_raptorq_repair_symbols(expr: &Expr) -> Result<u8> {
2619 let raw = parse_integer_expr(expr)?;
2620 if raw < 0 {
2621 warn!(
2622 value = raw,
2623 "rejecting negative raptorq_repair_symbols value"
2624 );
2625 return Err(FrankenError::OutOfRange {
2626 what: "raptorq_repair_symbols".to_owned(),
2627 value: raw.to_string(),
2628 });
2629 }
2630
2631 let max = i64::from(MAX_RAPTORQ_REPAIR_SYMBOLS);
2632 if raw > max {
2633 warn!(
2634 value = raw,
2635 max = MAX_RAPTORQ_REPAIR_SYMBOLS,
2636 "rejecting out-of-range raptorq_repair_symbols value"
2637 );
2638 return Err(FrankenError::OutOfRange {
2639 what: "raptorq_repair_symbols".to_owned(),
2640 value: raw.to_string(),
2641 });
2642 }
2643
2644 u8::try_from(raw).map_err(|_| {
2645 error!(
2646 value = raw,
2647 "failed to convert validated raptorq_repair_symbols to u8"
2648 );
2649 FrankenError::OutOfRange {
2650 what: "raptorq_repair_symbols".to_owned(),
2651 value: raw.to_string(),
2652 }
2653 })
2654 }
2655
2656 fn parse_integer_expr(expr: &Expr) -> Result<i64> {
2657 match expr {
2658 Expr::Literal(Literal::Integer(n), _) => Ok(*n),
2659 Expr::UnaryOp {
2660 op: UnaryOp::Negate,
2661 expr,
2662 ..
2663 } => Ok(-parse_integer_expr(expr)?),
2664 Expr::UnaryOp {
2665 op: UnaryOp::Plus,
2666 expr,
2667 ..
2668 } => parse_integer_expr(expr),
2669 Expr::Column(col, _) => {
2670 col.column
2671 .parse::<i64>()
2672 .map_err(|_| FrankenError::TypeMismatch {
2673 expected: "integer (0..255)".to_owned(),
2674 actual: col.column.to_string(),
2675 })
2676 }
2677 other => Err(FrankenError::TypeMismatch {
2678 expected: "integer (0..255)".to_owned(),
2679 actual: format!("{other:?}"),
2680 }),
2681 }
2682 }
2683
2684 fn parse_bool(expr: &Expr) -> Result<bool> {
2685 let (raw, parsed) = match expr {
2686 Expr::Literal(Literal::Integer(n), _) => (format!("{n}"), parse_int_bool(*n)),
2687 Expr::Literal(Literal::String(s), _) => (s.clone(), parse_str_bool(s)),
2688 Expr::Literal(Literal::True, _) => ("TRUE".to_owned(), Some(true)),
2689 Expr::Literal(Literal::False, _) => ("FALSE".to_owned(), Some(false)),
2690 Expr::Column(col, _) => (col.column.to_string(), parse_str_bool(&col.column)),
2691 other => {
2692 return Err(FrankenError::TypeMismatch {
2693 expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2694 actual: format!("{other:?}"),
2695 });
2696 }
2697 };
2698
2699 parsed.ok_or_else(|| FrankenError::TypeMismatch {
2700 expected: "ON|OFF|TRUE|FALSE|1|0".to_owned(),
2701 actual: raw,
2702 })
2703 }
2704
2705 fn parse_int_bool(n: i64) -> Option<bool> {
2706 match n {
2707 0 => Some(false),
2708 1 => Some(true),
2709 _ => None,
2710 }
2711 }
2712
2713 fn parse_str_bool(s: &str) -> Option<bool> {
2714 if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("true") {
2715 Some(true)
2716 } else if s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("false") {
2717 Some(false)
2718 } else if s == "1" {
2719 Some(true)
2720 } else if s == "0" {
2721 Some(false)
2722 } else {
2723 None
2724 }
2725 }
2726}
2727
2728#[cfg(test)]
2731mod tests {
2732 use super::*;
2733
2734 fn test_failure() -> bool {
2735 false
2736 }
2737
2738 #[test]
2740 fn test_vdbe_op_struct_size() {
2741 let op = VdbeOp {
2743 opcode: Opcode::Integer,
2744 p1: 42,
2745 p2: 1,
2746 p3: 0,
2747 p4: P4::None,
2748 p5: 0,
2749 };
2750 assert_eq!(op.opcode, Opcode::Integer);
2751 assert_eq!(op.p1, 42_i32);
2752 assert_eq!(op.p2, 1_i32);
2753 assert_eq!(op.p3, 0_i32);
2754 assert_eq!(op.p4, P4::None);
2755 assert_eq!(op.p5, 0_u16);
2756 }
2757
2758 #[test]
2759 fn test_schema_evaluation_context_encoding_and_nested_restoration() {
2760 const CONSTANT_ARGUMENT_MASK: i32 = 0x15;
2761
2762 let mut builder = ProgramBuilder::new();
2763 let output = builder.alloc_reg();
2764 builder.with_schema_evaluation_context(SchemaEvaluationContext::Index, |builder| {
2765 builder.emit_op(
2766 Opcode::PureFunc,
2767 CONSTANT_ARGUMENT_MASK,
2768 0,
2769 output,
2770 P4::FuncName("INDEX_FN".to_owned()),
2771 0,
2772 );
2773 builder.with_schema_evaluation_context(
2774 SchemaEvaluationContext::CheckConstraint,
2775 |builder| {
2776 builder.emit_op(
2777 Opcode::Function,
2778 CONSTANT_ARGUMENT_MASK,
2779 0,
2780 output,
2781 P4::FuncName("CHECK_FN".to_owned()),
2782 0,
2783 );
2784 },
2785 );
2786 builder.emit_op(
2787 Opcode::PureFunc,
2788 CONSTANT_ARGUMENT_MASK,
2789 0,
2790 output,
2791 P4::FuncName("INDEX_AGAIN_FN".to_owned()),
2792 0,
2793 );
2794 });
2795 builder.with_schema_evaluation_context(
2796 SchemaEvaluationContext::GeneratedColumn,
2797 |builder| {
2798 builder.emit_op(
2799 Opcode::PureFunc,
2800 CONSTANT_ARGUMENT_MASK,
2801 0,
2802 output,
2803 P4::FuncName("GENERATED_FN".to_owned()),
2804 0,
2805 );
2806 },
2807 );
2808 builder.emit_op(
2809 Opcode::PureFunc,
2810 CONSTANT_ARGUMENT_MASK,
2811 0,
2812 output,
2813 P4::FuncName("ORDINARY_FN".to_owned()),
2814 0,
2815 );
2816 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2817
2818 let program = builder
2819 .finish()
2820 .expect("schema-context program should build");
2821 let function_ops: Vec<_> = program
2822 .ops()
2823 .iter()
2824 .filter(|op| matches!(op.opcode, Opcode::Function | Opcode::PureFunc))
2825 .collect();
2826 let contexts: Vec<_> = function_ops
2827 .iter()
2828 .map(|op| SchemaEvaluationContext::from_function_p1(op.p1))
2829 .collect();
2830
2831 assert_eq!(
2832 contexts,
2833 [
2834 Some(SchemaEvaluationContext::Index),
2835 Some(SchemaEvaluationContext::CheckConstraint),
2836 Some(SchemaEvaluationContext::Index),
2837 Some(SchemaEvaluationContext::GeneratedColumn),
2838 None,
2839 ]
2840 );
2841 assert!(
2842 function_ops
2843 .iter()
2844 .all(|op| { op.p1 & !FUNCTION_SCHEMA_CONTEXT_MASK == CONSTANT_ARGUMENT_MASK })
2845 );
2846 }
2847
2848 #[test]
2850 fn test_p4_variant_all_types() {
2851 let variants: Vec<P4> = vec![
2853 P4::None,
2854 P4::Int(42),
2855 P4::Int64(i64::MAX),
2856 P4::Real(1.234_567_89),
2857 P4::Str("hello".to_owned()),
2858 P4::Blob(vec![0xDE, 0xAD]),
2859 P4::Collation("BINARY".to_owned()),
2860 P4::FuncName("count".to_owned()),
2861 P4::Table("users".to_owned()),
2862 P4::Affinity("ddd".to_owned()),
2863 P4::PrecomputedHeader(fsqlite_types::record::PrecomputedRecordHeader::new(&[
2864 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
2865 fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
2866 ])),
2867 ];
2868 assert_eq!(variants.len(), 11);
2869
2870 assert!(matches!(variants[0], P4::None));
2872 assert!(matches!(variants[1], P4::Int(42)));
2873 assert!(matches!(variants[2], P4::Int64(i64::MAX)));
2874 assert!(matches!(variants[3], P4::Real(_)));
2875 assert!(matches!(variants[4], P4::Str(_)));
2876 assert!(matches!(variants[5], P4::Blob(_)));
2877 assert!(matches!(variants[6], P4::Collation(_)));
2878 assert!(matches!(variants[7], P4::FuncName(ref s) if s == "count"));
2879 assert!(matches!(variants[8], P4::Table(ref s) if s == "users"));
2880 assert!(matches!(variants[9], P4::Affinity(ref s) if s == "ddd"));
2881 assert!(matches!(
2882 variants[10],
2883 P4::PrecomputedHeader(ref header) if header.template == vec![3, 0, 0]
2884 ));
2885 }
2886
2887 #[test]
2889 fn test_label_emit_and_resolve() {
2890 let mut b = ProgramBuilder::new();
2891
2892 let label_a = b.emit_label();
2894 let label_b = b.emit_label();
2895 assert_ne!(label_a, label_b);
2896
2897 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label_a, P4::None, 0);
2899 assert_eq!(b.op_at(jump_addr).unwrap().p2, -1); b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
2903 b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
2904
2905 b.resolve_label(label_a);
2907
2908 assert_eq!(b.op_at(jump_addr).unwrap().p2, 3);
2910
2911 let jump2 = b.emit_jump_to_label(Opcode::If, 1, 0, label_b, P4::None, 0);
2913 b.resolve_label(label_b);
2914 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2915 assert_eq!(b.op_at(jump2).unwrap().p2, 4);
2916
2917 let prog = b.finish().unwrap();
2919 assert_eq!(prog.len(), 5);
2920 }
2921
2922 #[test]
2924 fn test_unresolved_label_error() {
2925 let mut b = ProgramBuilder::new();
2926 let label = b.emit_label();
2927 b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
2928
2929 let result = b.finish();
2931 assert!(result.is_err());
2932 }
2933
2934 #[test]
2936 fn test_register_alloc_sequential() {
2937 let mut alloc = RegisterAllocator::new();
2938
2939 assert_eq!(alloc.alloc_reg(), 1);
2941 assert_eq!(alloc.alloc_reg(), 2);
2942 assert_eq!(alloc.alloc_reg(), 3);
2943
2944 let block_start = alloc.alloc_regs(3);
2946 assert_eq!(block_start, 4);
2947 assert_eq!(alloc.alloc_reg(), 7);
2949
2950 assert_eq!(alloc.count(), 7);
2951 }
2952
2953 #[test]
2955 fn test_register_temp_pool_reuse() {
2956 let mut alloc = RegisterAllocator::new();
2957
2958 let r1 = alloc.alloc_reg(); let t1 = alloc.alloc_temp(); let t2 = alloc.alloc_temp(); assert_eq!(r1, 1);
2962 assert_eq!(t1, 2);
2963 assert_eq!(t2, 3);
2964
2965 alloc.free_temp(t1);
2967 alloc.free_temp(t2);
2968
2969 let t3 = alloc.alloc_temp();
2971 let t4 = alloc.alloc_temp();
2972 assert_eq!(t3, t2); assert_eq!(t4, t1); assert_eq!(alloc.count(), 3);
2977 }
2978
2979 #[test]
2981 fn test_coroutine_init_yield_end() {
2982 let yield_reg = 1;
2984 let body_pc = 10;
2985 let mut co = CoroutineState::new(yield_reg, body_pc);
2986 assert_eq!(co.yield_reg, yield_reg);
2987 assert_eq!(co.saved_pc, body_pc);
2988 assert!(!co.exhausted);
2989
2990 let resume = co.yield_swap(5);
2993 assert_eq!(resume, 10); assert_eq!(co.saved_pc, 5); let resume2 = co.yield_swap(15);
2998 assert_eq!(resume2, 5); assert_eq!(co.saved_pc, 15);
3000
3001 let final_pc = co.end();
3003 assert_eq!(final_pc, 15); assert!(co.exhausted);
3005 }
3006
3007 #[test]
3009 fn test_coroutine_multi_row_production() {
3010 let mut co = CoroutineState::new(1, 10); let mut rows_consumed = 0;
3013 let caller_start_pc = 5;
3014
3015 let mut next_pc = co.yield_swap(caller_start_pc);
3017 assert_eq!(next_pc, 10); for row in 1..=5 {
3021 let body_pc = 10 + row; next_pc = co.yield_swap(body_pc);
3024 assert_eq!(next_pc, caller_start_pc);
3026 rows_consumed += 1;
3027
3028 if row < 5 {
3029 next_pc = co.yield_swap(caller_start_pc);
3031 assert_eq!(next_pc, body_pc); }
3033 }
3034
3035 assert_eq!(rows_consumed, 5);
3036
3037 let final_pc = co.end();
3039 assert!(co.exhausted);
3040 assert!(final_pc > 0); }
3042
3043 #[test]
3044 fn test_program_builder_infers_register_count_from_manual_opcode_registers() {
3045 let mut builder = ProgramBuilder::new();
3046 builder.emit_op(Opcode::Integer, 11, 3, 0, P4::None, 0);
3047 builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
3048 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3049
3050 let program = builder.finish().expect("program should build");
3051 assert_eq!(
3052 program.register_count(),
3053 3,
3054 "bytecode that writes raw registers must still allocate a large enough register file",
3055 );
3056 }
3057
3058 #[test]
3059 fn test_program_builder_infers_register_count_for_non_contiguous_comparison_operands() {
3060 let mut builder = ProgramBuilder::new();
3061 builder.emit_op(Opcode::Eq, 2, 0, 7, P4::None, 0);
3062 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3063
3064 let program = builder.finish().expect("program should build");
3065 assert_eq!(
3066 program.register_count(),
3067 7,
3068 "comparison opcodes must account for both read registers even when they are not contiguous",
3069 );
3070 }
3071
3072 #[test]
3073 fn test_program_builder_infers_register_count_for_store_p2_comparisons() {
3074 let mut builder = ProgramBuilder::new();
3075 builder.emit_op(Opcode::Eq, 2, 9, 7, P4::None, 0x20);
3076 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3077
3078 let program = builder.finish().expect("program should build");
3079 assert_eq!(
3080 program.register_count(),
3081 9,
3082 "SQLITE_STOREP2 comparisons must reserve the destination register in the pre-sized register file",
3083 );
3084 }
3085
3086 #[test]
3087 fn test_program_builder_tracks_sorter_preflight_and_runtime_bound_registers() {
3088 let mut builder = ProgramBuilder::new();
3089 builder.emit_op(
3090 Opcode::SorterOpen,
3091 0,
3092 1,
3093 8,
3094 P4::None,
3095 SORTER_OPEN_TOP_N_REGISTER,
3096 );
3097 builder.emit_op(
3098 Opcode::SorterCompare,
3099 0,
3100 2,
3101 7,
3102 P4::None,
3103 fsqlite_types::opcode::SORTER_COMPARE_TOP_N_PREFLIGHT,
3104 );
3105 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3106
3107 let program = builder.finish().expect("program should build");
3108 assert_eq!(
3109 program.register_count(),
3110 8,
3111 "runtime SorterOpen.p3 and SorterCompare.p3 must contribute to register sizing"
3112 );
3113 }
3114
3115 #[test]
3116 fn test_sorter_compare_register_spans_distinguish_preflight_consumption() {
3117 let ordinary = VdbeOp {
3118 opcode: Opcode::SorterCompare,
3119 p1: 0,
3120 p2: 1,
3121 p3: 7,
3122 p4: P4::None,
3123 p5: 0,
3124 };
3125 let ordinary_spans = opcode_register_spans(&ordinary);
3126 let preflight = VdbeOp {
3127 p5: SORTER_COMPARE_TOP_N_PREFLIGHT,
3128 ..ordinary
3129 };
3130 let preflight_spans = opcode_register_spans(&preflight);
3131
3132 assert_eq!(
3133 ordinary_spans,
3134 OpcodeRegisterSpans {
3135 read_start: 7,
3136 read_len: 1,
3137 write_start: -1,
3138 write_len: 0,
3139 },
3140 "ordinary SorterCompare must leave P3 read-only"
3141 );
3142 assert_eq!(
3143 preflight_spans,
3144 OpcodeRegisterSpans {
3145 read_start: 7,
3146 read_len: 1,
3147 write_start: 7,
3148 write_len: 1,
3149 },
3150 "top-N preflight must model P3 as consumed"
3151 );
3152 }
3153
3154 #[test]
3155 fn test_program_builder_does_not_treat_immediate_sorter_bound_as_register() {
3156 let mut builder = ProgramBuilder::new();
3157 builder.emit_op(Opcode::SorterOpen, 0, 1, 500, P4::None, 0);
3158 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3159
3160 let program = builder.finish().expect("program should build");
3161 assert_eq!(
3162 program.register_count(),
3163 0,
3164 "legacy immediate SorterOpen.p3 is not a register operand"
3165 );
3166 }
3167
3168 #[test]
3169 fn test_program_builder_rejects_out_of_bounds_goto_target() {
3170 let mut builder = ProgramBuilder::new();
3171 builder.emit_op(Opcode::Goto, 0, 99, 0, P4::None, 0);
3172 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3173
3174 let err = builder.finish().expect_err("invalid jump target must fail");
3175 match err {
3176 FrankenError::Internal(message) => {
3177 assert!(message.contains("Goto"));
3178 assert!(message.contains("p2"));
3179 }
3180 other => assert!(
3181 matches!(other, FrankenError::Internal(_)),
3182 "expected internal verifier error, got {other:?}"
3183 ),
3184 }
3185 }
3186
3187 #[test]
3188 fn test_program_builder_rejects_out_of_bounds_jump_branch_target() {
3189 let mut builder = ProgramBuilder::new();
3190 builder.emit_op(Opcode::Jump, 0, 1, 42, P4::None, 0);
3191 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3192
3193 let err = builder
3194 .finish()
3195 .expect_err("invalid branch target must fail");
3196 match err {
3197 FrankenError::Internal(message) => {
3198 assert!(message.contains("Jump"));
3199 assert!(message.contains("p3"));
3200 }
3201 other => assert!(
3202 matches!(other, FrankenError::Internal(_)),
3203 "expected internal verifier error, got {other:?}"
3204 ),
3205 }
3206 }
3207
3208 #[test]
3210 fn test_all_opcode_dispatch_coverage() {
3211 for byte in 1..Opcode::COUNT as u8 {
3214 let opcode = Opcode::from_byte(byte);
3215 assert!(
3216 opcode.is_some(),
3217 "Opcode::from_byte({byte}) returned None — gap in opcode enum"
3218 );
3219 let opcode = opcode.unwrap();
3220 let name = opcode.name();
3221 assert!(!name.is_empty(), "opcode {byte} has empty name");
3222 }
3223 assert_eq!(Opcode::from_byte(Opcode::COUNT as u8), None);
3224 }
3225
3226 #[test]
3228 fn test_p5_flags_u16_range() {
3229 let op = VdbeOp {
3231 opcode: Opcode::Eq,
3232 p1: 1,
3233 p2: 5,
3234 p3: 2,
3235 p4: P4::None,
3236 p5: 0x1FF, };
3238 assert_eq!(op.p5, 0x1FF);
3239 assert!(op.p5 > 255);
3240
3241 let op2 = VdbeOp {
3242 opcode: Opcode::Noop,
3243 p1: 0,
3244 p2: 0,
3245 p3: 0,
3246 p4: P4::None,
3247 p5: u16::MAX,
3248 };
3249 assert_eq!(op2.p5, 65535);
3250 }
3251
3252 #[test]
3254 fn test_program_builder_basic() {
3255 let mut b = ProgramBuilder::new();
3256
3257 let end_label = b.emit_label();
3259 b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
3260 let r1 = b.alloc_reg();
3261 assert_eq!(r1, 1);
3262 b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
3263 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
3264 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3265 b.resolve_label(end_label);
3266
3267 let prog = b.finish().unwrap();
3268 assert_eq!(prog.len(), 4);
3269 assert_eq!(prog.register_count(), 1);
3270 assert_eq!(prog.max_bind_parameter_index().unwrap(), 0);
3271
3272 assert_eq!(prog.get(0).unwrap().opcode, Opcode::Init);
3274 assert_eq!(prog.get(0).unwrap().p2, 4);
3275 }
3276
3277 #[test]
3278 fn test_program_precomputes_max_bind_parameter_index() {
3279 let mut b = ProgramBuilder::new();
3280 b.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
3281 b.emit_op(Opcode::Variable, 4, 2, 0, P4::None, 0);
3282 b.emit_op(Opcode::Variable, 2, 3, 0, P4::None, 0);
3283 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3284 let prog = b.finish().unwrap();
3285 assert_eq!(prog.max_bind_parameter_index(), Ok(4));
3286 }
3287
3288 #[test]
3289 fn test_program_tracks_invalid_bind_parameter_index() {
3290 let mut b = ProgramBuilder::new();
3291 b.emit_op(Opcode::Variable, 0, 1, 0, P4::None, 0);
3292 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3293 let prog = b.finish().unwrap();
3294 assert_eq!(prog.max_bind_parameter_index(), Err(0));
3295 }
3296
3297 #[test]
3298 fn test_program_storage_only_hot_path_does_not_require_attached_memdb() {
3299 let mut b = ProgramBuilder::new();
3300 let end = b.emit_label();
3301 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3302 b.emit_op(Opcode::OpenWrite, 0, 256, 0, P4::Int(1), 0);
3303 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
3304 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
3305 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
3306 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
3307 b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
3308 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
3309 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3310 b.resolve_label(end);
3311
3312 let prog = match b.finish() {
3314 Ok(prog) => prog,
3315 Err(err) => {
3316 assert!(test_failure(), "program should build: {err}");
3317 return;
3318 }
3319 };
3320 assert!(
3321 !prog.requires_attached_memdb(),
3322 "storage-only table hot paths should not force a MemDatabase handoff"
3323 );
3324 }
3325
3326 #[test]
3327 fn test_program_with_ephemeral_cursor_requires_attached_memdb() {
3328 let mut b = ProgramBuilder::new();
3329 let end = b.emit_label();
3330 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3331 b.emit_op(Opcode::OpenEphemeral, 0, 1, 0, P4::None, 0);
3332 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3333 b.resolve_label(end);
3334
3335 let prog = b.finish().expect("program should build");
3336 assert!(
3337 prog.requires_attached_memdb(),
3338 "ephemeral table programs still depend on the attached MemDatabase"
3339 );
3340 }
3341
3342 #[test]
3343 fn test_program_with_sorter_cursor_does_not_require_attached_memdb() -> Result<()> {
3344 let mut b = ProgramBuilder::new();
3345 let end = b.emit_label();
3346 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
3347 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::Str("+".to_owned()), 0);
3348 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
3349 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3350 b.resolve_label(end);
3351
3352 let prog = b.finish()?;
3353 assert!(
3354 !prog.requires_attached_memdb(),
3355 "sorter-backed temp/exchange state is owned by VDBE and should not force a MemDatabase handoff"
3356 );
3357 Ok(())
3358 }
3359
3360 #[test]
3361 fn test_program_builder_accumulates_table_index_meta_by_table_cursor() {
3362 use fsqlite_types::opcode::IndexCursorMeta;
3363
3364 let mut b = ProgramBuilder::new();
3365 b.register_table_indexes(
3366 3,
3367 vec![IndexCursorMeta {
3368 cursor_id: 4,
3369 column_indices: vec![0, 2],
3370 }],
3371 );
3372 b.register_table_indexes(
3373 3,
3374 vec![IndexCursorMeta {
3375 cursor_id: 5,
3376 column_indices: vec![1],
3377 }],
3378 );
3379
3380 let prog = b.finish().expect("program should build");
3381 let metas = prog
3382 .table_index_meta()
3383 .get(&3)
3384 .expect("table cursor metadata should be present");
3385 assert_eq!(metas.len(), 2);
3386 assert_eq!(metas[0].cursor_id, 4);
3387 assert_eq!(metas[0].column_indices, vec![0, 2]);
3388 assert_eq!(metas[1].cursor_id, 5);
3389 assert_eq!(metas[1].column_indices, vec![1]);
3390 }
3391
3392 #[test]
3394 fn test_disassemble() {
3395 let mut b = ProgramBuilder::new();
3396 b.emit_op(Opcode::Init, 0, 2, 0, P4::None, 0);
3397 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
3398 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3399 let prog = b.finish().unwrap();
3400
3401 let asm = prog.disassemble();
3402 assert!(asm.contains("Init"));
3403 assert!(asm.contains("Integer"));
3404 assert!(asm.contains("Halt"));
3405 assert!(asm.contains("42")); }
3407
3408 #[test]
3410 fn test_key_info() {
3411 let ki = KeyInfo {
3412 num_fields: 3,
3413 collations: vec![
3414 "BINARY".to_owned(),
3415 "NOCASE".to_owned(),
3416 "BINARY".to_owned(),
3417 ],
3418 sort_orders: vec![SortOrder::Asc, SortOrder::Desc, SortOrder::Asc],
3419 };
3420 assert_eq!(ki.num_fields, 3);
3421 assert_eq!(ki.collations.len(), 3);
3422 assert_eq!(ki.sort_orders[1], SortOrder::Desc);
3423 }
3424
3425 #[test]
3427 fn test_label_already_resolved() {
3428 let mut b = ProgramBuilder::new();
3431 let label = b.emit_label();
3432 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3433 b.resolve_label(label); b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3435
3436 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3438 assert_eq!(b.op_at(jump_addr).unwrap().p2, 1);
3440
3441 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3442
3443 let prog = b.finish().unwrap();
3444 assert_eq!(prog.len(), 4);
3445 }
3446
3447 #[test]
3449 fn test_builder_register_via_builder() {
3450 let mut b = ProgramBuilder::new();
3451 let r1 = b.alloc_reg();
3452 let r2 = b.alloc_reg();
3453 let block = b.alloc_regs(4);
3454 assert_eq!(r1, 1);
3455 assert_eq!(r2, 2);
3456 assert_eq!(block, 3);
3457 assert_eq!(b.register_count(), 6);
3458
3459 let t1 = b.alloc_temp();
3461 assert_eq!(t1, 7);
3462 b.free_temp(t1);
3463 let t2 = b.alloc_temp();
3464 assert_eq!(t2, t1); }
3466
3467 #[test]
3469 fn test_resolve_label_to_specific_address() {
3470 let mut b = ProgramBuilder::new();
3471 let label = b.emit_label();
3472 let jump_addr = b.emit_jump_to_label(Opcode::Goto, 0, 0, label, P4::None, 0);
3473 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3474 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
3475
3476 b.resolve_label_to(label, 42);
3478 assert_eq!(b.op_at(jump_addr).unwrap().p2, 42);
3479 }
3480
3481 #[test]
3483 fn test_empty_program_finishes() {
3484 let b = ProgramBuilder::new();
3485 let prog = b.finish().unwrap();
3486 assert!(prog.is_empty());
3487 assert_eq!(prog.register_count(), 0);
3488 }
3489
3490 #[test]
3492 fn test_unreferenced_unresolved_label_ok() {
3493 let mut b = ProgramBuilder::new();
3496 let _label = b.emit_label();
3497 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3498 let prog = b.finish().unwrap();
3499 assert_eq!(prog.len(), 1);
3500 }
3501
3502 #[cfg(not(target_arch = "wasm32"))]
3505 use std::fs;
3506
3507 use fsqlite_ast::Statement;
3508 use fsqlite_error::FrankenError;
3509 use fsqlite_mvcc::{BeginKind, MvccError, TransactionManager};
3510 use fsqlite_parser::Parser;
3511 use fsqlite_types::{CommitSeq, ObjectId, Oti, PageData, PageNumber, PageSize};
3512 use fsqlite_wal::{
3513 DEFAULT_RAPTORQ_REPAIR_SYMBOLS, WalFecGroupMeta, WalFecGroupMetaInit, WalFecGroupRecord,
3514 WalFecRecoveryOutcome, WalFrameCandidate, WalSalts, append_wal_fec_group,
3515 build_source_page_hashes, generate_wal_fec_repair_symbols,
3516 recover_wal_fec_group_with_decoder, scan_wal_fec,
3517 };
3518 #[cfg(not(target_arch = "wasm32"))]
3519 use tempfile::tempdir;
3520
3521 fn parse_pragma(sql: &str) -> std::result::Result<fsqlite_ast::PragmaStatement, String> {
3522 let mut p = Parser::from_sql(sql);
3523 let stmt = p.parse_statement().expect("parse statement");
3524 match stmt {
3525 Statement::Pragma(p) => Ok(p),
3526 other => Err(format!("expected PRAGMA, got: {other:?}")),
3527 }
3528 }
3529
3530 fn test_page(first_byte: u8) -> PageData {
3531 let mut page = PageData::zeroed(PageSize::DEFAULT);
3532 page.as_bytes_mut()[0] = first_byte;
3533 page
3534 }
3535
3536 fn make_source_pages(seed: u8, k_source: u32) -> Vec<Vec<u8>> {
3537 let page_len = usize::try_from(PageSize::DEFAULT.get()).expect("page size fits usize");
3538 (0..k_source)
3539 .map(|idx| {
3540 let idx_u8 = u8::try_from(idx).expect("test k_source fits u8");
3541 let mut page = vec![seed.wrapping_add(idx_u8); page_len];
3542 page[0] = idx_u8;
3543 page
3544 })
3545 .collect()
3546 }
3547
3548 fn make_wal_fec_group(
3549 start_frame_no: u32,
3550 r_repair: u8,
3551 seed: u8,
3552 ) -> (WalFecGroupRecord, Vec<Vec<u8>>) {
3553 let k_source = 5_u32;
3554 let source_pages = make_source_pages(seed, k_source);
3555 let page_size = PageSize::DEFAULT.get();
3556 let source_hashes = build_source_page_hashes(&source_pages);
3557 let page_numbers = (0..k_source).map(|i| 10 + i).collect::<Vec<_>>();
3558 let oti = Oti {
3559 f: u64::from(k_source) * u64::from(page_size),
3560 al: 1,
3561 t: page_size,
3562 z: 1,
3563 n: 1,
3564 };
3565 let meta = WalFecGroupMeta::from_init(WalFecGroupMetaInit {
3566 wal_salt1: 0xA11C_E001,
3567 wal_salt2: 0xA11C_E002,
3568 start_frame_no,
3569 end_frame_no: start_frame_no + (k_source - 1),
3570 db_size_pages: 256,
3571 page_size,
3572 k_source,
3573 r_repair: u32::from(r_repair),
3574 oti,
3575 object_id: ObjectId::from_bytes([seed; 16]),
3576 page_numbers,
3577 source_page_xxh3_128: source_hashes,
3578 })
3579 .expect("meta");
3580 let repair_symbols =
3581 generate_wal_fec_repair_symbols(&meta, &source_pages).expect("symbols");
3582 (
3583 WalFecGroupRecord::new(meta, repair_symbols).expect("group"),
3584 source_pages,
3585 )
3586 }
3587
3588 #[test]
3589 fn test_pragma_serializable_query_returns_current_setting() {
3590 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3591
3592 let stmt = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3593 let out = pragma::apply(&mut mgr, &stmt).unwrap();
3594 assert_eq!(out, pragma::PragmaOutput::Bool(true));
3595 }
3596
3597 #[test]
3598 fn test_connection_pragma_differential_views_default_query_returns_false() {
3599 let mut state = pragma::ConnectionPragmaState::default();
3600
3601 let stmt = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3602 let out = pragma::apply_connection_pragma(&mut state, &stmt).expect("query pragma");
3603 assert_eq!(out, pragma::PragmaOutput::Bool(false));
3604 }
3605
3606 #[test]
3607 fn test_connection_pragma_differential_views_set_and_query_across_aliases() {
3608 let mut state = pragma::ConnectionPragmaState::default();
3609
3610 let set_on = parse_pragma("PRAGMA fsqlite.differential_views = ON").expect("parse pragma");
3611 assert_eq!(
3612 pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3613 pragma::PragmaOutput::Bool(true)
3614 );
3615 assert!(state.differential_views.is_enabled());
3616
3617 let query = parse_pragma("PRAGMA fsqlite_differential_views").expect("parse pragma");
3618 assert_eq!(
3619 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3620 pragma::PragmaOutput::Bool(true)
3621 );
3622 }
3623
3624 #[test]
3625 fn test_connection_pragma_differential_views_rejects_non_boolean_values() {
3626 let mut state = pragma::ConnectionPragmaState::default();
3627
3628 let stmt = parse_pragma("PRAGMA fsqlite_differential_views = 2").expect("parse pragma");
3629 assert!(matches!(
3630 pragma::apply_connection_pragma(&mut state, &stmt),
3631 Err(FrankenError::TypeMismatch { .. })
3632 ));
3633 }
3634
3635 #[test]
3636 fn test_connection_pragma_query_only_set_and_query() {
3637 let mut state = pragma::ConnectionPragmaState::default();
3638
3639 let query = parse_pragma("PRAGMA query_only").expect("parse pragma");
3640 assert_eq!(
3641 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3642 pragma::PragmaOutput::Int(0)
3643 );
3644
3645 let set_on = parse_pragma("PRAGMA query_only = ON").expect("parse pragma");
3646 assert_eq!(
3647 pragma::apply_connection_pragma(&mut state, &set_on).expect("set pragma"),
3648 pragma::PragmaOutput::Int(1)
3649 );
3650 assert!(state.query_only);
3651
3652 assert_eq!(
3653 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3654 pragma::PragmaOutput::Int(1)
3655 );
3656 }
3657
3658 #[test]
3659 fn test_connection_pragma_journal_size_limit_set_and_query() {
3660 let mut state = pragma::ConnectionPragmaState::default();
3661
3662 let query = parse_pragma("PRAGMA journal_size_limit").expect("parse pragma");
3665 assert_eq!(
3666 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3667 pragma::PragmaOutput::Int(-1)
3668 );
3669
3670 let set = parse_pragma("PRAGMA journal_size_limit = 32768").expect("parse pragma");
3672 assert_eq!(
3673 pragma::apply_connection_pragma(&mut state, &set).expect("set pragma"),
3674 pragma::PragmaOutput::Int(32768)
3675 );
3676 assert_eq!(state.journal_size_limit, 32768);
3677 assert_eq!(
3678 pragma::apply_connection_pragma(&mut state, &query).expect("query pragma"),
3679 pragma::PragmaOutput::Int(32768)
3680 );
3681
3682 let set_neg = parse_pragma("PRAGMA journal_size_limit = -1").expect("parse pragma");
3685 assert_eq!(
3686 pragma::apply_connection_pragma(&mut state, &set_neg).expect("set pragma"),
3687 pragma::PragmaOutput::Int(-1)
3688 );
3689 assert_eq!(state.journal_size_limit, -1);
3690 }
3691
3692 #[test]
3693 fn test_connection_pragma_query_only_rejects_non_boolean_values() {
3694 let mut state = pragma::ConnectionPragmaState::default();
3695
3696 let stmt = parse_pragma("PRAGMA query_only = 2").expect("parse pragma");
3697 assert!(matches!(
3698 pragma::apply_connection_pragma(&mut state, &stmt),
3699 Err(FrankenError::TypeMismatch { .. })
3700 ));
3701 }
3702
3703 fn apply_sql(state: &mut pragma::ConnectionPragmaState, sql: &str) -> pragma::PragmaOutput {
3704 let stmt = parse_pragma(sql).expect("parse pragma");
3705 pragma::apply_connection_pragma(state, &stmt).expect("apply pragma")
3706 }
3707
3708 #[test]
3709 fn test_connection_pragma_boolean_readbacks() {
3710 use pragma::PragmaOutput::Bool;
3712 let mut state = pragma::ConnectionPragmaState::default();
3713
3714 assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(true));
3716 assert_eq!(apply_sql(&mut state, "PRAGMA automatic_index"), Bool(true));
3717 assert_eq!(
3718 apply_sql(&mut state, "PRAGMA read_uncommitted"),
3719 Bool(false)
3720 );
3721 assert_eq!(apply_sql(&mut state, "PRAGMA cell_size_check"), Bool(false));
3722 assert_eq!(
3723 apply_sql(&mut state, "PRAGMA checkpoint_fullfsync"),
3724 Bool(false)
3725 );
3726
3727 assert_eq!(
3729 apply_sql(&mut state, "PRAGMA trusted_schema = OFF"),
3730 Bool(false)
3731 );
3732 assert_eq!(apply_sql(&mut state, "PRAGMA trusted_schema"), Bool(false));
3733 assert_eq!(
3734 apply_sql(&mut state, "PRAGMA read_uncommitted = ON"),
3735 Bool(true)
3736 );
3737 assert_eq!(
3738 apply_sql(&mut state, "PRAGMA cell_size_check = 1"),
3739 Bool(true)
3740 );
3741 assert_eq!(
3742 apply_sql(&mut state, "PRAGMA checkpoint_fullfsync = TRUE"),
3743 Bool(true)
3744 );
3745 assert_eq!(
3746 apply_sql(&mut state, "PRAGMA automatic_index = 0"),
3747 Bool(false)
3748 );
3749 }
3750
3751 #[test]
3752 fn test_connection_pragma_locking_mode_readback() {
3753 use pragma::PragmaOutput::Text;
3755 let mut state = pragma::ConnectionPragmaState::default();
3756 assert_eq!(
3757 apply_sql(&mut state, "PRAGMA locking_mode"),
3758 Text("normal".to_owned())
3759 );
3760 assert_eq!(
3761 apply_sql(&mut state, "PRAGMA locking_mode = EXCLUSIVE"),
3762 Text("exclusive".to_owned())
3763 );
3764 assert_eq!(
3765 apply_sql(&mut state, "PRAGMA locking_mode"),
3766 Text("exclusive".to_owned())
3767 );
3768 assert_eq!(
3770 apply_sql(&mut state, "PRAGMA locking_mode = bogus"),
3771 Text("exclusive".to_owned())
3772 );
3773 }
3774
3775 #[test]
3776 fn test_connection_pragma_secure_delete_tristate() {
3777 use pragma::PragmaOutput::Int;
3779 let mut state = pragma::ConnectionPragmaState::default();
3780 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete"), Int(0));
3781 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = ON"), Int(1));
3782 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = FAST"), Int(2));
3783 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = OFF"), Int(0));
3784 assert_eq!(apply_sql(&mut state, "PRAGMA secure_delete = 2"), Int(2));
3785 }
3786
3787 #[test]
3788 fn test_connection_pragma_heap_limits_gh280() {
3789 use pragma::PragmaOutput::Int;
3794 let mut state = pragma::ConnectionPragmaState::default();
3795
3796 pragma::reset_heap_limits_for_test();
3798 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(0));
3799 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit"), Int(0));
3800
3801 pragma::reset_heap_limits_for_test();
3803 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=10000000"), Int(10000000));
3804 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=50000000"), Int(10000000));
3805 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(10000000));
3806
3807 pragma::reset_heap_limits_for_test();
3809 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=50000000"), Int(50000000));
3810 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=10000000"), Int(10000000));
3811 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(10000000));
3812
3813 pragma::reset_heap_limits_for_test();
3815 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=90000000"), Int(90000000));
3816 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=30000000"), Int(30000000));
3817 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(30000000));
3818 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit"), Int(30000000));
3819
3820 pragma::reset_heap_limits_for_test();
3822 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=-5"), Int(0));
3823 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(0));
3824
3825 pragma::reset_heap_limits_for_test();
3827 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=80000000"), Int(80000000));
3828 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=0"), Int(0));
3829 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit"), Int(0));
3830
3831 pragma::reset_heap_limits_for_test();
3833 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=80000000"), Int(80000000));
3834 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=0"), Int(80000000));
3835 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit"), Int(80000000));
3836
3837 pragma::reset_heap_limits_for_test();
3839 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=40000000"), Int(40000000));
3840 assert_eq!(apply_sql(&mut state, "PRAGMA hard_heap_limit=90000000"), Int(40000000));
3841
3842 pragma::reset_heap_limits_for_test();
3844 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit=70000000"), Int(70000000));
3845 assert_eq!(apply_sql(&mut state, "PRAGMA soft_heap_limit='junk'"), Int(70000000));
3846
3847 pragma::reset_heap_limits_for_test();
3848 }
3849
3850 #[test]
3851 fn test_connection_pragma_cache_spill_gh275() {
3852 use pragma::PragmaOutput::Int;
3856 let mut state = pragma::ConnectionPragmaState::default();
3857 assert_eq!(apply_sql(&mut state, "PRAGMA cache_size=1000"), Int(1000));
3858
3859 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1000));
3861
3862 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill=OFF"), Int(0));
3864 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(0));
3865
3866 apply_sql(&mut state, "PRAGMA cache_spill=ON");
3868 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1000));
3869
3870 apply_sql(&mut state, "PRAGMA cache_spill=1234");
3872 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1234));
3873
3874 apply_sql(&mut state, "PRAGMA cache_spill=ON");
3876 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1234));
3877
3878 apply_sql(&mut state, "PRAGMA cache_spill=OFF");
3880 apply_sql(&mut state, "PRAGMA cache_spill=77");
3881 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1000));
3882
3883 apply_sql(&mut state, "PRAGMA cache_spill=-5");
3885 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(1000));
3886
3887 apply_sql(&mut state, "PRAGMA cache_spill=500");
3890 assert_eq!(apply_sql(&mut state, "PRAGMA cache_size=100"), Int(100));
3891 assert_eq!(apply_sql(&mut state, "PRAGMA cache_spill"), Int(500));
3892 }
3893
3894 #[test]
3895 fn test_connection_pragma_reverse_unordered_selects_gh236() {
3896 use pragma::PragmaOutput::Bool;
3900 let mut state = pragma::ConnectionPragmaState::default();
3901 assert_eq!(
3902 apply_sql(&mut state, "PRAGMA reverse_unordered_selects"),
3903 Bool(false)
3904 );
3905 assert_eq!(
3906 apply_sql(&mut state, "PRAGMA reverse_unordered_selects = ON"),
3907 Bool(true)
3908 );
3909 assert!(state.reverse_unordered_selects);
3910 assert_eq!(
3911 apply_sql(&mut state, "PRAGMA reverse_unordered_selects"),
3912 Bool(true)
3913 );
3914 assert_eq!(
3915 apply_sql(&mut state, "PRAGMA reverse_unordered_selects = OFF"),
3916 Bool(false)
3917 );
3918 assert!(!state.reverse_unordered_selects);
3919 }
3920
3921 #[test]
3922 fn test_connection_pragma_threads_readback() {
3923 use pragma::PragmaOutput::Int;
3926 let mut state = pragma::ConnectionPragmaState::default();
3927 assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(0));
3928 assert_eq!(apply_sql(&mut state, "PRAGMA threads = 4"), Int(4));
3929 assert_eq!(apply_sql(&mut state, "PRAGMA threads"), Int(4));
3930 assert_eq!(apply_sql(&mut state, "PRAGMA threads = 100"), Int(8));
3932 assert_eq!(apply_sql(&mut state, "PRAGMA threads = -1"), Int(8));
3934 }
3935
3936 #[test]
3937 fn test_pragma_serializable_set_and_query() {
3938 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3939
3940 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3941 assert_eq!(
3942 pragma::apply(&mut mgr, &set_off).unwrap(),
3943 pragma::PragmaOutput::Bool(false)
3944 );
3945
3946 let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3947 assert_eq!(
3948 pragma::apply(&mut mgr, &query).unwrap(),
3949 pragma::PragmaOutput::Bool(false)
3950 );
3951 }
3952
3953 #[test]
3954 fn test_pragma_scope_per_connection_via_handler() {
3955 let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
3956 let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
3957
3958 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3959 let _ = pragma::apply(&mut conn_a, &set_off).unwrap();
3960
3961 let query = parse_pragma("PRAGMA fsqlite.serializable").expect("parse pragma");
3962 assert_eq!(
3963 pragma::apply(&mut conn_a, &query).unwrap(),
3964 pragma::PragmaOutput::Bool(false)
3965 );
3966 assert_eq!(
3967 pragma::apply(&mut conn_b, &query).unwrap(),
3968 pragma::PragmaOutput::Bool(true)
3969 );
3970 }
3971
3972 #[test]
3973 fn test_pragma_not_retroactive_to_active_txn_via_handler() {
3974 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3975
3976 let mut txn = mgr.begin(BeginKind::Concurrent).unwrap();
3977 mgr.write_page(&mut txn, PageNumber::new(1).unwrap(), test_page(0x01))
3978 .unwrap();
3979 txn.has_in_rw = true;
3980 txn.has_out_rw = true;
3981 assert!(txn.has_dangerous_structure());
3982
3983 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
3985 let _ = pragma::apply(&mut mgr, &set_off).unwrap();
3986
3987 assert_eq!(
3988 mgr.commit(&mut txn).unwrap_err(),
3989 MvccError::BusySnapshot,
3990 "PRAGMA change must not be retroactive to an active txn"
3991 );
3992 }
3993
3994 #[test]
3995 fn test_e2e_serializable_pragma_switch_changes_behavior() {
3996 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
3997
3998 let set_on = parse_pragma("PRAGMA fsqlite.serializable = ON").expect("parse pragma");
4000 let _ = pragma::apply(&mut mgr, &set_on).unwrap();
4001
4002 let mut txn_on = mgr.begin(BeginKind::Concurrent).unwrap();
4003 mgr.write_page(&mut txn_on, PageNumber::new(1).unwrap(), test_page(0x10))
4004 .unwrap();
4005 txn_on.has_in_rw = true;
4006 txn_on.has_out_rw = true;
4007 assert_eq!(
4008 mgr.commit(&mut txn_on).unwrap_err(),
4009 MvccError::BusySnapshot,
4010 "serializable=ON must enforce SSI (abort)"
4011 );
4012
4013 let set_off = parse_pragma("PRAGMA fsqlite.serializable = OFF").expect("parse pragma");
4015 let _ = pragma::apply(&mut mgr, &set_off).unwrap();
4016
4017 let mut txn_off = mgr.begin(BeginKind::Concurrent).unwrap();
4018 mgr.write_page(&mut txn_off, PageNumber::new(2).unwrap(), test_page(0x20))
4019 .unwrap();
4020 txn_off.has_in_rw = true;
4021 txn_off.has_out_rw = true;
4022
4023 let seq = mgr.commit(&mut txn_off).unwrap();
4024 assert!(
4025 seq > CommitSeq::ZERO,
4026 "serializable=OFF must allow write skew"
4027 );
4028 }
4029
4030 #[test]
4031 fn test_pragma_raptorq_repair_symbols_default_query() {
4032 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
4033 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
4034 assert_eq!(
4035 pragma::apply(&mut mgr, &query).expect("query pragma"),
4036 pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
4037 );
4038 }
4039
4040 #[cfg(not(target_arch = "wasm32"))]
4041 #[test]
4042 fn test_bd_1hi_12_unit_compliance_gate() {
4043 let dir = tempdir().expect("tempdir");
4044 let sidecar = dir.path().join("unit.wal-fec");
4045 let db_path = dir.path().join("unit.db");
4046 fs::write(&db_path, vec![0_u8; 100]).expect("seed db header");
4047
4048 let mut conn_a = TransactionManager::new(PageSize::DEFAULT);
4049 let mut conn_b = TransactionManager::new(PageSize::DEFAULT);
4050
4051 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
4052 assert_eq!(
4053 pragma::apply_with_sidecar(&mut conn_a, &query, Some(&sidecar)).expect("query default"),
4054 pragma::PragmaOutput::Int(i64::from(DEFAULT_RAPTORQ_REPAIR_SYMBOLS))
4055 );
4056
4057 let set_max = parse_pragma("PRAGMA raptorq_repair_symbols = 255").expect("parse set max");
4058 assert_eq!(
4059 pragma::apply_with_sidecar(&mut conn_a, &set_max, Some(&sidecar)).expect("set max"),
4060 pragma::PragmaOutput::Int(255)
4061 );
4062
4063 let set_too_high =
4064 parse_pragma("PRAGMA raptorq_repair_symbols = 256").expect("parse set too high");
4065 assert!(matches!(
4066 pragma::apply_with_sidecar(&mut conn_a, &set_too_high, Some(&sidecar)),
4067 Err(FrankenError::OutOfRange { .. })
4068 ));
4069
4070 let set_negative =
4071 parse_pragma("PRAGMA raptorq_repair_symbols = -1").expect("parse set negative");
4072 assert!(matches!(
4073 pragma::apply_with_sidecar(&mut conn_a, &set_negative, Some(&sidecar)),
4074 Err(FrankenError::OutOfRange { .. })
4075 ));
4076
4077 let set_non_integer =
4078 parse_pragma("PRAGMA raptorq_repair_symbols = ON").expect("parse set non-integer");
4079 assert!(matches!(
4080 pragma::apply_with_sidecar(&mut conn_a, &set_non_integer, Some(&sidecar)),
4081 Err(FrankenError::TypeMismatch { .. })
4082 ));
4083
4084 let query_new_conn = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
4085 assert_eq!(
4086 pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
4087 .expect("query persisted value"),
4088 pragma::PragmaOutput::Int(255)
4089 );
4090
4091 let set_shared = parse_pragma("PRAGMA raptorq_repair_symbols = 7").expect("parse shared");
4092 let _ = pragma::apply_with_sidecar(&mut conn_a, &set_shared, Some(&sidecar))
4093 .expect("persist shared setting");
4094 assert_eq!(
4095 pragma::apply_with_sidecar(&mut conn_b, &query_new_conn, Some(&sidecar))
4096 .expect("cross-connection visibility"),
4097 pragma::PragmaOutput::Int(7)
4098 );
4099
4100 let db_bytes = fs::read(&db_path).expect("read db header");
4101 assert!(
4102 db_bytes[72..92].iter().all(|&byte| byte == 0),
4103 "sqlite header reserved bytes must remain untouched"
4104 );
4105 }
4106
4107 #[cfg(not(target_arch = "wasm32"))]
4108 #[test]
4109 fn prop_bd_1hi_12_structure_compliance() {
4110 let dir = tempdir().expect("tempdir");
4111 let sidecar = dir.path().join("property.wal-fec");
4112 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
4113 let query = parse_pragma("PRAGMA raptorq_repair_symbols").expect("parse query");
4114
4115 for value in 0_u16..=255_u16 {
4116 let sql = format!("PRAGMA raptorq_repair_symbols = {value}");
4117 let set_stmt = parse_pragma(&sql).expect("parse set statement");
4118 assert_eq!(
4119 pragma::apply_with_sidecar(&mut mgr, &set_stmt, Some(&sidecar)).expect("set value"),
4120 pragma::PragmaOutput::Int(i64::from(value))
4121 );
4122 assert_eq!(
4123 pragma::apply_with_sidecar(&mut mgr, &query, Some(&sidecar)).expect("query value"),
4124 pragma::PragmaOutput::Int(i64::from(value))
4125 );
4126 }
4127 }
4128
4129 #[cfg(not(target_arch = "wasm32"))]
4130 #[test]
4131 #[allow(clippy::too_many_lines)]
4132 fn test_e2e_bd_1hi_12_compliance() {
4133 let dir = tempdir().expect("tempdir");
4134 let sidecar = dir.path().join("e2e.wal-fec");
4135 let mut mgr = TransactionManager::new(PageSize::DEFAULT);
4136
4137 let set_zero = parse_pragma("PRAGMA raptorq_repair_symbols = 0").expect("parse set 0");
4138 let _ = pragma::apply_with_sidecar(&mut mgr, &set_zero, Some(&sidecar)).expect("set 0");
4139 if mgr.raptorq_repair_symbols() > 0 {
4140 let (group, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x10);
4141 append_wal_fec_group(&sidecar, &group).expect("append group");
4142 }
4143 let after_zero = scan_wal_fec(&sidecar).expect("scan after zero");
4144 assert!(
4145 after_zero.groups.is_empty(),
4146 "N=0 must produce no .wal-fec groups for new commits"
4147 );
4148
4149 let set_one = parse_pragma("PRAGMA raptorq_repair_symbols = 1").expect("parse set 1");
4150 let _ = pragma::apply_with_sidecar(&mut mgr, &set_one, Some(&sidecar)).expect("set 1");
4151 let (group_r1, _) = make_wal_fec_group(1, mgr.raptorq_repair_symbols(), 0x11);
4152 append_wal_fec_group(&sidecar, &group_r1).expect("append r=1 group");
4153
4154 let set_two = parse_pragma("PRAGMA raptorq_repair_symbols = 2").expect("parse set 2");
4155 let _ = pragma::apply_with_sidecar(&mut mgr, &set_two, Some(&sidecar)).expect("set 2");
4156 let (group_r2, _) = make_wal_fec_group(6, mgr.raptorq_repair_symbols(), 0x22);
4157 append_wal_fec_group(&sidecar, &group_r2).expect("append r=2 group");
4158
4159 let set_four = parse_pragma("PRAGMA raptorq_repair_symbols = 4").expect("parse set 4");
4160 let _ = pragma::apply_with_sidecar(&mut mgr, &set_four, Some(&sidecar)).expect("set 4");
4161 let (group_r4, source_pages_r4) =
4162 make_wal_fec_group(11, mgr.raptorq_repair_symbols(), 0x33);
4163 append_wal_fec_group(&sidecar, &group_r4).expect("append r=4 group");
4164
4165 let scan = scan_wal_fec(&sidecar).expect("scan sidecar");
4166 assert_eq!(scan.groups.len(), 3);
4167 assert_eq!(scan.groups[0].repair_symbols.len(), 1);
4168 assert_eq!(scan.groups[1].repair_symbols.len(), 2);
4169 assert_eq!(scan.groups[2].repair_symbols.len(), 4);
4170 assert_eq!(scan.groups[1].meta.r_repair, 2);
4171 assert_eq!(scan.groups[2].meta.r_repair, 4);
4172
4173 let group_id = group_r4.meta.group_id();
4174 let wal_salts = WalSalts {
4175 salt1: group_r4.meta.wal_salt1,
4176 salt2: group_r4.meta.wal_salt2,
4177 };
4178 let k_source = usize::try_from(group_r4.meta.k_source).expect("k fits usize");
4179
4180 let mut corrupt_three_frames = Vec::new();
4181 for (idx, page) in source_pages_r4.iter().enumerate() {
4182 let mut payload = page.clone();
4183 if idx < 3 {
4184 payload[0] ^= 0xFF;
4185 }
4186 corrupt_three_frames.push(WalFrameCandidate {
4187 frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
4188 page_data: payload,
4189 });
4190 }
4191 let expected_pages = source_pages_r4.clone();
4192 let recovered = recover_wal_fec_group_with_decoder(
4193 &sidecar,
4194 group_id,
4195 wal_salts,
4196 group_r4.meta.start_frame_no,
4197 &corrupt_three_frames,
4198 move |meta: &WalFecGroupMeta, symbols| {
4199 if symbols.len() < usize::try_from(meta.k_source).expect("k fits usize") {
4200 return Err(FrankenError::WalCorrupt {
4201 detail: "insufficient symbols".to_owned(),
4202 });
4203 }
4204 Ok(expected_pages.clone())
4205 },
4206 )
4207 .expect("recover with <=R corruption");
4208 assert!(
4209 matches!(recovered, WalFecRecoveryOutcome::Recovered(_)),
4210 "expected recovered outcome"
4211 );
4212 let WalFecRecoveryOutcome::Recovered(group) = recovered else {
4213 unreachable!("asserted recovered outcome above");
4214 };
4215 assert_eq!(group.recovered_pages.len(), k_source);
4216
4217 let mut corrupt_five_frames = Vec::new();
4218 for (idx, page) in source_pages_r4.iter().enumerate() {
4219 let mut payload = page.clone();
4220 payload[0] ^= 0x55;
4221 corrupt_five_frames.push(WalFrameCandidate {
4222 frame_no: group_r4.meta.start_frame_no + u32::try_from(idx).expect("idx fits u32"),
4223 page_data: payload,
4224 });
4225 }
4226 let truncated = recover_wal_fec_group_with_decoder(
4227 &sidecar,
4228 group_id,
4229 wal_salts,
4230 group_r4.meta.start_frame_no,
4231 &corrupt_five_frames,
4232 |_meta: &WalFecGroupMeta, _symbols| {
4233 Err(FrankenError::WalCorrupt {
4234 detail: "decoder should not be able to recover".to_owned(),
4235 })
4236 },
4237 )
4238 .expect("recover with >R corruption");
4239 assert!(matches!(
4240 truncated,
4241 WalFecRecoveryOutcome::TruncateBeforeGroup { .. }
4242 ));
4243 }
4244}