1#![allow(
13 clippy::match_same_arms,
14 clippy::inline_always,
15 clippy::unnecessary_wraps,
16 clippy::option_option,
17 clippy::useless_let_if_seq,
18 clippy::collapsible_if
19)]
20use hashbrown::{HashMap, HashSet};
21use std::any::Any;
22use std::borrow::Cow;
23#[cfg(test)]
24use std::cell::Cell;
25use std::cell::RefCell;
26use std::cmp::Ordering;
27use std::collections::{BTreeMap, BTreeSet, VecDeque};
28
29use fsqlite_btree::swiss_index::SwissIndex;
30use fsqlite_types::PageSize;
31use std::rc::Rc;
32
33#[derive(Debug)]
38struct CursorSlots<V> {
39 slots: Vec<Option<V>>,
40}
41
42impl<V> Default for CursorSlots<V> {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48#[allow(clippy::trivially_copy_pass_by_ref)]
49impl<V> CursorSlots<V> {
50 #[inline]
51 fn new() -> Self {
52 Self { slots: Vec::new() }
53 }
54
55 #[inline]
56 fn ensure_slot(&mut self, id: i32) {
57 if id < 0 {
59 return;
60 }
61 let idx = id as usize;
62 if idx >= self.slots.len() {
63 self.slots.resize_with(idx + 1, || None);
64 }
65 }
66
67 #[inline]
68 pub fn get(&self, id: &i32) -> Option<&V> {
69 if *id < 0 {
70 return None;
71 }
72 self.slots.get(*id as usize).and_then(Option::as_ref)
73 }
74
75 #[inline]
76 pub fn get_mut(&mut self, id: &i32) -> Option<&mut V> {
77 if *id < 0 {
78 return None;
79 }
80 self.slots.get_mut(*id as usize).and_then(Option::as_mut)
81 }
82
83 #[inline]
84 pub fn insert(&mut self, id: i32, value: V) -> Option<V> {
85 if id < 0 {
86 return None;
87 }
88 self.ensure_slot(id);
89 self.slots[id as usize].replace(value)
90 }
91
92 #[inline]
93 pub fn remove(&mut self, id: &i32) -> Option<V> {
94 if *id < 0 {
95 return None;
96 }
97 let idx = *id as usize;
98 if idx < self.slots.len() {
99 self.slots[idx].take()
100 } else {
101 None
102 }
103 }
104
105 #[inline]
106 pub fn contains_key(&self, id: &i32) -> bool {
107 if *id < 0 {
108 return false;
109 }
110 self.slots.get(*id as usize).is_some_and(Option::is_some)
111 }
112
113 #[inline]
114 pub fn clear(&mut self) {
115 for slot in &mut self.slots {
116 *slot = None;
117 }
118 }
119
120 #[inline]
121 pub fn is_empty(&self) -> bool {
122 self.slots.iter().all(Option::is_none)
123 }
124
125 pub fn values(&self) -> impl Iterator<Item = &V> {
126 self.slots.iter().filter_map(Option::as_ref)
127 }
128
129 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
130 self.slots.iter_mut().filter_map(Option::as_mut)
131 }
132
133 #[allow(clippy::cast_possible_wrap)]
136 pub fn iter(&self) -> impl Iterator<Item = (i32, &V)> {
137 self.slots
138 .iter()
139 .enumerate()
140 .filter_map(|(i, slot)| slot.as_ref().map(|v| (i as i32, v)))
141 }
142}
143use fsqlite_types::sync_primitives::{Duration, Instant};
144use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
145use std::sync::{Arc, LazyLock, Mutex};
146
147use fsqlite_btree::cursor::{CursorPositionStamp, FirstIndexKeyIntegerLocalRunSegment};
148use fsqlite_btree::{
149 BtCursor, BtreeCursorOps, BtreePageHeader, BtreePageType, MemPageStore, PageReader, PageWriter,
150 SeekResult, header_offset_for_page,
151};
152use fsqlite_error::{ErrorCode, FrankenError, Result};
153use fsqlite_func::collation::CollationRegistry;
154use fsqlite_func::vtab::ColumnContext;
155use fsqlite_func::{
156 ApplicationFunctionKind, ErasedAggregateFunction, ErasedWindowFunction, FunctionRegistry,
157 ResolvedScalarFunction,
158};
159use fsqlite_mvcc::ConcurrentPageState;
160use fsqlite_mvcc::{
161 AllocatorKey, CommitIndex, CommitLog, ConcurrentRowIdAllocator, InProcessPageLockTable,
162 MvccError, SharedConcurrentHandle, TimeTravelSnapshot, TimeTravelTarget, VersionStore,
163 concurrent_clear_page_state, concurrent_free_page, concurrent_has_page_state,
164 concurrent_page_is_synthetic_conflict_only, concurrent_page_read_status, concurrent_page_state,
165 concurrent_prepare_write_page, concurrent_restore_page_state,
166 concurrent_stage_prepared_write_marker, concurrent_track_write_conflict_page,
167 create_time_travel_snapshot,
168};
169#[cfg(test)]
170use fsqlite_mvcc::{concurrent_read_page, concurrent_write_page};
171use fsqlite_pager::{TransactionHandle, TransactionKind};
172use fsqlite_types::cx::Cx;
173use fsqlite_types::opcode::{
174 Opcode, P4, SORTER_COMPARE_TOP_N_PREFLIGHT, SORTER_OPEN_TOP_N_REGISTER, VdbeOp,
175};
176use fsqlite_types::record::{
177 ColumnOffset, PrecomputedSerialTypeKind, RecordProfileScope, enter_record_profile_scope,
178 parse_record, record_iter_with_precomputed_header_exact_size, serialize_record,
179 serialize_record_iter_with_precomputed_header_into,
180 serialize_record_iter_with_precomputed_header_into_slice, simd_serialize_integer_record,
181};
182use fsqlite_types::serial_type::{
183 SerialTypeClass, classify_serial_type, read_varint, serial_type_len,
184};
185use fsqlite_types::value::{
186 SmallText, SqlLikeFastPathKind, SqliteValue, pool_return_reusable,
187 sql_like_fast_path_matches_cased,
188};
189use fsqlite_types::{
190 CommitSeq, DATABASE_HEADER_SIZE, DatabaseHeader, PageData, PageNumber, RowId, RowIdMode,
191 SchemaEpoch, StrictColumnType, TableId, WitnessKey,
192};
193
194use crate::{
195 SchemaEvaluationContext, TableIndexMetaMap, VdbeProgram, enter_vdbe_decode_profile_stage,
196 enter_vdbe_execute_profile_stage,
197 jit::{
198 CompiledProgram, CompiledRecordBuilder, ConstantResultRowTemplate, FullScanSelectTemplate,
199 InsertValueSource, RowidLookupSelectTemplate, SimpleInsertTemplate, try_compile_program,
200 },
201 opcode_register_spans,
202};
203
204const VDBE_EXECUTION_CHECKPOINT_INTERVAL: u64 = 4096;
205
206fn validate_schema_function_invocation(
207 context: SchemaEvaluationContext,
208 function_name: &str,
209 safety: fsqlite_func::ScalarSchemaSafety,
210 args: &[SqliteValue],
211) -> Result<()> {
212 let safe = match safety {
213 fsqlite_func::ScalarSchemaSafety::Always => true,
214 fsqlite_func::ScalarSchemaSafety::Never => {
218 context == SchemaEvaluationContext::CheckConstraint
219 }
220 fsqlite_func::ScalarSchemaSafety::DateTimeConditional => {
221 fsqlite_func::datetime::is_datetime_invocation_safe_for_schema(function_name, args)
222 }
223 };
224 if safe {
225 Ok(())
226 } else {
227 let owner = match context {
228 SchemaEvaluationContext::Index => "an index",
229 SchemaEvaluationContext::GeneratedColumn => "a generated column",
230 SchemaEvaluationContext::CheckConstraint => "a CHECK constraint",
231 };
232 Err(FrankenError::function_error(format!(
233 "non-deterministic use of {}() in {owner}",
234 function_name.to_ascii_lowercase(),
235 )))
236 }
237}
238
239const OPFLAG_ISUPDATE: u16 = 0x10;
245const OPFLAG_REPLACE_VICTIM: u16 = 0x20;
248const OPFLAG_IDX_NCHANGE: u16 = 0x40;
251const OPFLAG_HALT_UNIQUE: u16 = 0x01;
254const STORAGE_CURSOR_LAYOUT_PREFIX_BYTES: usize = 256;
255#[cfg(test)]
256const VDBE_ENGINE_INLINE_SIZE_BUDGET_BYTES: usize = 3 * 1024;
257static BUILTIN_COLLATION_REGISTRY: LazyLock<CollationRegistry> =
258 LazyLock::new(CollationRegistry::default);
259
260#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
261struct StatementColdState(u16);
262
263impl StatementColdState {
264 const AGGREGATES: Self = Self(1 << 0);
265 const CONFLICT_TRACKING: Self = Self(1 << 1);
266 const ROWSETS: Self = Self(1 << 2);
267 const SEQUENCE_COUNTERS: Self = Self(1 << 3);
268 const VTAB_CURSORS: Self = Self(1 << 4);
269 const WINDOW_CONTEXTS: Self = Self(1 << 5);
270 const REGISTER_SUBTYPES: Self = Self(1 << 6);
271 const BLOOM_FILTERS: Self = Self(1 << 7);
272
273 #[must_use]
274 const fn empty() -> Self {
275 Self(0)
276 }
277
278 #[cfg(test)]
279 #[must_use]
280 #[allow(dead_code)]
281 const fn contains(self, other: Self) -> bool {
282 self.0 & other.0 != 0
283 }
284
285 #[must_use]
286 const fn is_empty(self) -> bool {
287 self.0 == 0
288 }
289
290 fn insert(&mut self, other: Self) {
291 self.0 |= other.0;
292 }
293
294 fn clear(&mut self) {
295 self.0 = 0;
296 }
297}
298
299#[inline]
300fn observe_execution_cancellation(cx: &Cx) -> Result<()> {
301 cx.checkpoint().map_err(|_| FrankenError::Abort)
302}
303
304const MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES: usize = 9;
305const MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN: usize = 64;
306const MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MAX: usize = 512;
307
308#[inline]
309fn compute_record_header_size_hint(content_size: usize) -> usize {
310 let mut header_size = content_size + 1;
311 loop {
312 let needed = fsqlite_types::serial_type::varint_len(header_size as u64) + content_size;
313 if needed <= header_size {
314 return header_size;
315 }
316 header_size = needed;
317 }
318}
319
320#[inline]
321fn make_record_variable_width_reserve(page_size: PageSize) -> usize {
322 usize::try_from(page_size.get())
323 .unwrap_or(fsqlite_types::limits::DEFAULT_PAGE_SIZE as usize)
324 .div_ceil(16)
325 .clamp(
326 MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN,
327 MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MAX,
328 )
329}
330
331fn estimate_make_record_capacity_for_affinity(
332 affinity: Option<&str>,
333 n_cols: usize,
334 variable_width_hint: usize,
335) -> usize {
336 let chars = affinity.map_or_else(
337 || smallvec::SmallVec::<[char; 16]>::from_elem('C', n_cols),
338 |aff| aff.chars().collect(),
339 );
340 let column_count = chars.len().max(n_cols);
341 let mut header_content_size = 0usize;
342 let mut body_size = 0usize;
343
344 for idx in 0..column_count {
345 match chars.get(idx).copied().unwrap_or('C') {
346 'X' => {
347 header_content_size += 1;
348 }
349 'C' | 'D' | 'E' => {
350 header_content_size += 1;
351 body_size += MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES;
352 }
353 _ => {
354 header_content_size += 2;
355 body_size += variable_width_hint;
356 }
357 }
358 }
359
360 compute_record_header_size_hint(header_content_size) + body_size
361}
362
363fn estimate_make_record_buffer_capacity(program: &VdbeProgram, page_size: PageSize) -> usize {
364 let variable_width_hint = make_record_variable_width_reserve(page_size);
365 program
366 .ops()
367 .iter()
368 .filter(|op| op.opcode == Opcode::MakeRecord)
369 .map(|op| -> usize {
370 let n_cols = usize::try_from(op.p2.max(0)).unwrap_or(0);
371 let capacity = match &op.p4 {
372 P4::Affinity(aff) => estimate_make_record_capacity_for_affinity(
373 Some(aff.as_str()),
374 n_cols,
375 variable_width_hint,
376 ),
377 P4::PrecomputedHeader(header) => {
378 let body_size = header
379 .slots
380 .iter()
381 .map(|slot| match slot.kind {
382 PrecomputedSerialTypeKind::NullPlaceholder => 0,
383 PrecomputedSerialTypeKind::AnyOneByteVarintOrNull => {
384 MAKE_RECORD_VARIABLE_WIDTH_RESERVE_MIN
385 }
386 PrecomputedSerialTypeKind::IntegerOrNull
387 | PrecomputedSerialTypeKind::RealOrNull => {
388 MAKE_RECORD_FIXED_WIDTH_RESERVE_BYTES
389 }
390 })
391 .sum::<usize>();
392 header.template.len() + body_size
393 }
394 _ => estimate_make_record_capacity_for_affinity(None, n_cols, variable_width_hint),
395 };
396 capacity
397 })
398 .max()
399 .unwrap_or(0)
400}
401
402#[inline]
403fn vtab_exec_outcome(opcode: &str, err: FrankenError) -> Result<ExecOutcome> {
404 if matches!(err, FrankenError::Abort) {
405 return Err(FrankenError::Abort);
406 }
407 Ok(ExecOutcome::Error {
408 code: 1,
409 message: format!("{opcode} error: {err}"),
410 })
411}
412
413#[inline]
414fn duration_ns_saturating(duration: Duration) -> u64 {
415 u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
416}
417
418#[inline]
419fn transient_mem_root_pgno(root_pgno: PageNumber) -> PageNumber {
420 if root_pgno.get() == 1 {
421 PageNumber::new(2).expect("page 2 must be a valid transient root page")
424 } else {
425 root_pgno
426 }
427}
428
429#[inline]
430fn add_vdbe_counter(counter: &AtomicU64, delta: u64) {
431 if FSQLITE_VDBE_METRICS_ENABLED.load(AtomicOrdering::Relaxed) {
432 counter.fetch_add(delta, AtomicOrdering::Relaxed);
433 }
434}
435
436#[inline]
437fn add_vdbe_counter_if(enabled: bool, counter: &AtomicU64, delta: u64) {
438 if enabled {
439 counter.fetch_add(delta, AtomicOrdering::Relaxed);
440 }
441}
442
443#[inline]
444fn add_vdbe_duration_if(enabled: bool, counter: &AtomicU64, started: Instant) {
445 if enabled {
446 counter.fetch_add(
447 duration_ns_saturating(started.elapsed()),
448 AtomicOrdering::Relaxed,
449 );
450 }
451}
452
453#[derive(Debug, Clone, Copy)]
454struct BtreeCursorPageLayout {
455 usable_size: u32,
456 page_size: u32,
457}
458
459impl BtreeCursorPageLayout {
460 const fn no_reserved_bytes(page_size: PageSize) -> Self {
461 let page_size = page_size.get();
462 Self {
463 usable_size: page_size,
464 page_size,
465 }
466 }
467}
468
469async fn btree_cursor_page_layout_from_page_one<P: PageReader>(
470 page_reader: &P,
471 cx: &Cx,
472) -> Option<BtreeCursorPageLayout> {
473 let page_one = page_reader.read_page(cx, PageNumber::ONE).await.ok()?;
474 let header_prefix: [u8; DATABASE_HEADER_SIZE] =
475 page_one.get(..DATABASE_HEADER_SIZE)?.try_into().ok()?;
476 let header = DatabaseHeader::from_bytes(&header_prefix).ok()?;
477 let usable_size = header.page_size.usable(header.reserved_per_page);
478 if usable_size <= 4 {
479 return None;
480 }
481 Some(BtreeCursorPageLayout {
482 usable_size,
483 page_size: header.page_size.get(),
484 })
485}
486
487async fn btree_cursor_page_layout_for_reader_or_default<P: PageReader>(
488 page_reader: &P,
489 cx: &Cx,
490 default_page_size: PageSize,
491) -> BtreeCursorPageLayout {
492 btree_cursor_page_layout_from_page_one(page_reader, cx)
497 .await
498 .unwrap_or_else(|| BtreeCursorPageLayout::no_reserved_bytes(default_page_size))
499}
500
501fn configure_btree_cursor_page_size<P>(
502 cursor: &mut BtCursor<P>,
503 page_layout: BtreeCursorPageLayout,
504) {
505 if page_layout.page_size != page_layout.usable_size {
506 cursor.set_page_size(page_layout.page_size);
507 }
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
511pub struct ReusableTableExecutionStateOutcome {
512 pub metadata_rebind_count: u32,
513 pub function_cache_cleared: bool,
515}
516
517#[derive(Clone)]
518#[allow(clippy::struct_excessive_bools)]
519pub struct ReusableTableExecutionState {
520 pub func_registry: Arc<FunctionRegistry>,
521 pub collation_registry: Arc<Mutex<CollationRegistry>>,
522 pub schema_cookie: u32,
523 pub autoincrement_seq_by_root_page: HashMap<i32, i64>,
524 pub rowid_alias_col_by_root_page: Arc<HashMap<i32, usize>>,
525 pub table_column_count_by_root_page: Arc<HashMap<i32, usize>>,
526 pub first_not_null_non_ipk_col_by_root_page: Arc<HashMap<i32, usize>>,
527 pub column_defaults_by_root_page: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
528 pub index_desc_flags_by_root_page: Arc<HashMap<i32, Vec<bool>>>,
529 pub index_collations_by_root_page: Arc<HashMap<i32, Vec<Option<String>>>>,
530 pub reject_mem_fallback: bool,
531 pub memdb_rows_loaded: bool,
532 pub storage_cursor_memdb_count_shortcuts_safe: bool,
533 pub version_store: Option<Arc<VersionStore>>,
534 pub collect_result_rows: bool,
535 pub max_collected_result_rows: Option<usize>,
536}
537
538pub type MemRowValues = Vec<SqliteValue>;
546
547#[derive(Debug, Clone, PartialEq)]
549struct MemRow {
550 rowid: i64,
551 values: MemRowValues,
552}
553
554#[derive(Debug, Clone)]
556struct UniqueConstraintState {
557 columns: Vec<usize>,
558 collations: Vec<Option<String>>,
559 index: Option<BTreeMap<Vec<u8>, BTreeSet<i64>>>,
560}
561
562fn unique_constraint_collation_is_indexable(collation: Option<&str>) -> bool {
563 match collation {
564 None => true,
565 Some(name)
566 if name.eq_ignore_ascii_case("BINARY")
567 || name.eq_ignore_ascii_case("NOCASE")
568 || name.eq_ignore_ascii_case("RTRIM") =>
569 {
570 true
571 }
572 Some(_) => false,
573 }
574}
575
576fn unique_constraint_supports_index(collations: &[Option<String>]) -> bool {
577 collations
578 .iter()
579 .all(|collation| unique_constraint_collation_is_indexable(collation.as_deref()))
580}
581
582fn append_unique_constraint_key_component(
583 key: &mut Vec<u8>,
584 value: &SqliteValue,
585 collation: Option<&str>,
586) -> bool {
587 if !unique_constraint_collation_is_indexable(collation) {
588 return false;
589 }
590
591 let is_nocase = collation.is_some_and(|name| name.eq_ignore_ascii_case("NOCASE"));
592 let is_rtrim = collation.is_some_and(|name| name.eq_ignore_ascii_case("RTRIM"));
593
594 match value {
595 SqliteValue::Null => return false,
596 SqliteValue::Integer(i) => {
597 key.push(1);
598 key.extend_from_slice(&i.to_le_bytes());
599 }
600 SqliteValue::Float(f) => {
601 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(f) {
602 #[allow(clippy::cast_possible_truncation)]
603 let i = *f as i64;
604 #[allow(clippy::cast_precision_loss)]
605 if (i as f64) == *f {
606 key.push(1);
607 key.extend_from_slice(&i.to_le_bytes());
608 return true;
609 }
610 }
611 key.push(2);
612 key.extend_from_slice(&f.to_bits().to_le_bytes());
613 }
614 SqliteValue::Text(text) => {
615 key.push(3);
616 let text_bytes = text.as_bytes_direct();
617 if is_nocase {
618 #[allow(clippy::cast_possible_truncation)]
619 key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
620 key.extend(text_bytes.iter().map(u8::to_ascii_lowercase));
621 } else if is_rtrim {
622 let trimmed = trim_rtrim_collation_text(text_bytes);
623 #[allow(clippy::cast_possible_truncation)]
624 key.extend_from_slice(&(trimmed.len() as u64).to_le_bytes());
625 key.extend_from_slice(trimmed);
626 } else {
627 #[allow(clippy::cast_possible_truncation)]
628 key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
629 key.extend_from_slice(text_bytes);
630 }
631 }
632 SqliteValue::Blob(bytes) => {
633 key.push(4);
634 #[allow(clippy::cast_possible_truncation)]
635 key.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
636 key.extend_from_slice(bytes);
637 }
638 }
639
640 true
641}
642
643impl UniqueConstraintState {
644 fn new(columns: Vec<usize>, mut collations: Vec<Option<String>>) -> Self {
645 if collations.len() < columns.len() {
646 collations.resize(columns.len(), None);
647 } else if collations.len() > columns.len() {
648 collations.truncate(columns.len());
649 }
650 let index = unique_constraint_supports_index(&collations).then(BTreeMap::new);
651 Self {
652 columns,
653 collations,
654 index,
655 }
656 }
657}
658
659#[derive(Debug, Clone)]
660pub struct MemTable {
661 pub num_columns: usize,
664 rows: Vec<MemRow>,
666 next_rowid: i64,
668 unique_constraints: Vec<UniqueConstraintState>,
673 collation_registry: Arc<Mutex<CollationRegistry>>,
676}
677
678impl MemTable {
679 fn new(num_columns: usize) -> Self {
681 Self {
682 num_columns,
683 rows: Vec::new(),
684 next_rowid: 1,
685 unique_constraints: Vec::new(),
686 collation_registry: Arc::new(Mutex::new(CollationRegistry::new())),
687 }
688 }
689
690 pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
691 self.collation_registry = registry;
692 }
693
694 pub fn add_unique_column_group(&mut self, cols: Vec<usize>) {
696 self.add_unique_column_group_with_collations(cols, Vec::new());
697 }
698
699 pub fn add_unique_column_group_with_collations(
702 &mut self,
703 cols: Vec<usize>,
704 collations: Vec<Option<String>>,
705 ) {
706 let mut constraint = UniqueConstraintState::new(cols, collations);
707 if let Some(index) = constraint.index.as_mut() {
708 for row in &self.rows {
709 if let Some(key) = Self::unique_key_for_constraint(
710 &row.values,
711 &constraint.columns,
712 &constraint.collations,
713 ) {
714 index.entry(key).or_default().insert(row.rowid);
715 }
716 }
717 }
718 self.unique_constraints.push(constraint);
719 }
720
721 pub fn remove_unique_column_group_with_collations(
727 &mut self,
728 cols: &[usize],
729 collations: &[Option<String>],
730 ) -> bool {
731 let normalized = UniqueConstraintState::new(cols.to_vec(), collations.to_vec());
732 let Some(position) = self.unique_constraints.iter().rposition(|constraint| {
733 constraint.columns == normalized.columns
734 && constraint.collations.len() == normalized.collations.len()
735 && constraint
736 .collations
737 .iter()
738 .zip(&normalized.collations)
739 .all(|(left, right)| match (left, right) {
740 (Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
741 (None, None) => true,
742 _ => false,
743 })
744 }) else {
745 return false;
746 };
747 self.unique_constraints.remove(position);
748 true
749 }
750
751 pub fn unique_column_group_is_valid(
757 &self,
758 cols: &[usize],
759 collations: &[Option<String>],
760 ) -> bool {
761 let constraint = UniqueConstraintState::new(cols.to_vec(), collations.to_vec());
762 if constraint.index.is_some() {
763 let mut seen = HashSet::new();
764 for row in &self.rows {
765 if let Some(key) = Self::unique_key_for_constraint(
766 &row.values,
767 &constraint.columns,
768 &constraint.collations,
769 ) && !seen.insert(key)
770 {
771 return false;
772 }
773 }
774 return true;
775 }
776
777 let registry = self
778 .collation_registry
779 .lock()
780 .unwrap_or_else(|poisoned| poisoned.into_inner());
781 for (idx, row) in self.rows.iter().enumerate() {
782 if self.rows[idx + 1..].iter().any(|other| {
783 Self::unique_constraint_matches_row(
784 &row.values,
785 &other.values,
786 &constraint.columns,
787 &constraint.collations,
788 ®istry,
789 )
790 }) {
791 return false;
792 }
793 }
794 true
795 }
796
797 pub fn find_unique_conflicts(&self, new_values: &[SqliteValue]) -> Vec<i64> {
800 let mut conflicts = BTreeSet::new();
801 let mut collations = None;
802 for constraint in &self.unique_constraints {
803 if let Some(index) = &constraint.index {
804 if let Some(key) = Self::unique_key_for_constraint(
805 new_values,
806 &constraint.columns,
807 &constraint.collations,
808 ) && let Some(rowids) = index.get(&key)
809 {
810 conflicts.extend(rowids.iter().copied());
811 }
812 continue;
813 }
814
815 let registry = collations.get_or_insert_with(|| {
816 self.collation_registry
817 .lock()
818 .unwrap_or_else(|poisoned| poisoned.into_inner())
819 });
820 for row in &self.rows {
821 if Self::unique_constraint_matches_row(
822 &row.values,
823 new_values,
824 &constraint.columns,
825 &constraint.collations,
826 registry,
827 ) {
828 conflicts.insert(row.rowid);
829 }
830 }
831 }
832 conflicts.into_iter().collect()
833 }
834
835 pub fn alloc_rowid(&mut self) -> i64 {
837 let id = self.next_rowid;
838 self.next_rowid = self.next_rowid.saturating_add(1);
839 id
840 }
841
842 #[must_use]
844 pub fn next_rowid_hint(&self) -> i64 {
845 self.next_rowid
846 }
847
848 #[must_use]
850 pub fn row_count(&self) -> usize {
851 self.rows.len()
852 }
853
854 #[must_use]
856 pub fn max_visible_rowid(&self) -> Option<i64> {
857 self.rows.last().map(|row| row.rowid)
858 }
859
860 fn insert<V>(&mut self, rowid: i64, values: V)
862 where
863 V: Into<MemRowValues>,
864 {
865 let values = values.into();
866 if rowid >= self.next_rowid {
868 self.next_rowid = rowid.saturating_add(1);
869 }
870 let new_keys = self.unique_keys_for_values(&values);
871 if self.rows.last().is_none_or(|row| row.rowid < rowid) {
872 self.rows.push(MemRow { rowid, values });
873 self.insert_unique_keys(rowid, &new_keys);
874 return;
875 }
876 match self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
878 Ok(idx) => {
879 let old_values = {
880 let row = &mut self.rows[idx];
881 std::mem::replace(&mut row.values, values)
882 };
883 self.remove_unique_entries(rowid, &old_values);
884 self.insert_unique_keys(rowid, &new_keys);
885 }
886 Err(idx) => {
887 self.rows.insert(idx, MemRow { rowid, values });
888 self.insert_unique_keys(rowid, &new_keys);
889 }
890 }
891 }
892
893 pub fn delete_by_rowid(&mut self, rowid: i64) -> bool {
895 if let Ok(idx) = self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
896 let row = self.rows.remove(idx);
897 self.remove_unique_entries(row.rowid, &row.values);
898 true
899 } else {
900 false
901 }
902 }
903
904 pub fn clear(&mut self) {
906 self.rows.clear();
907 for constraint in &mut self.unique_constraints {
908 if let Some(index) = constraint.index.as_mut() {
909 index.clear();
910 }
911 }
912 }
913
914 pub fn pad_rows_to_column_count(&mut self, target_cols: usize, default_val: &SqliteValue) {
919 self.num_columns = target_cols;
920 for row in &mut self.rows {
921 while row.values.len() < target_cols {
922 row.values.push(default_val.clone());
923 }
924 }
925 self.rebuild_unique_indexes();
926 }
927
928 fn remove_column_from_rows(&mut self, removed_slot: usize) {
934 self.num_columns = self.num_columns.saturating_sub(1);
935 for row in &mut self.rows {
936 if removed_slot < row.values.len() {
937 row.values.remove(removed_slot);
938 }
939 }
940 for constraint in &mut self.unique_constraints {
941 for column in &mut constraint.columns {
942 if *column > removed_slot {
943 *column -= 1;
944 }
945 }
946 }
947 self.rebuild_unique_indexes();
948 }
949
950 #[inline]
952 pub fn find_by_rowid(&self, rowid: i64) -> Option<usize> {
953 if let Some(idx) = self.dense_rowid_offset(rowid) {
954 return Some(idx);
955 }
956 self.rows.binary_search_by_key(&rowid, |r| r.rowid).ok()
957 }
958
959 #[inline]
960 fn dense_rowid_offset(&self, rowid: i64) -> Option<usize> {
961 let first_rowid = self.rows.first()?.rowid;
962 let offset = rowid.checked_sub(first_rowid)?;
963 let idx = usize::try_from(offset).ok()?;
964 let row = self.rows.get(idx)?;
965 (row.rowid == rowid).then_some(idx)
966 }
967
968 fn rowid_lower_bound(&self, rowid: i64) -> usize {
969 let Some(first) = self.rows.first() else {
970 return 0;
971 };
972 if rowid <= first.rowid {
973 return 0;
974 }
975 if self.rows.last().is_some_and(|last| rowid > last.rowid) {
976 return self.rows.len();
977 }
978 if let Some(idx) = self.dense_rowid_offset(rowid) {
979 return idx;
980 }
981 match self.rows.binary_search_by_key(&rowid, |r| r.rowid) {
982 Ok(idx) | Err(idx) => idx,
983 }
984 }
985
986 #[inline]
988 pub fn row_values_by_rowid(&self, rowid: i64) -> Option<&[SqliteValue]> {
989 let idx = self.find_by_rowid(rowid)?;
990 Some(self.rows[idx].values.as_slice())
991 }
992
993 pub fn count_rowid_range(&self, lower_inclusive: i64, upper_exclusive: i64) -> usize {
995 let start = self.rowid_lower_bound(lower_inclusive);
996 let end = self.rowid_lower_bound(upper_exclusive);
997 end.saturating_sub(start)
998 }
999
1000 pub fn iter_rows_in_rowid_range(
1002 &self,
1003 lower_inclusive: i64,
1004 upper_exclusive: i64,
1005 ) -> impl Iterator<Item = (i64, &[SqliteValue])> + '_ {
1006 let start = self.rowid_lower_bound(lower_inclusive);
1007 let end = self.rowid_lower_bound(upper_exclusive);
1008 let end = end.max(start);
1009 self.rows[start..end]
1010 .iter()
1011 .map(|row| (row.rowid, row.values.as_slice()))
1012 }
1013
1014 pub fn iter_rows(&self) -> impl Iterator<Item = (i64, &[SqliteValue])> + '_ {
1019 self.rows.iter().map(|r| (r.rowid, r.values.as_slice()))
1020 }
1021
1022 pub fn insert_row<V>(&mut self, rowid: i64, values: V)
1027 where
1028 V: Into<MemRowValues>,
1029 {
1030 self.insert(rowid, values);
1031 }
1032
1033 fn unique_key_for_constraint(
1034 values: &[SqliteValue],
1035 columns: &[usize],
1036 collations: &[Option<String>],
1037 ) -> Option<Vec<u8>> {
1038 let mut key = Vec::with_capacity(columns.len() * 8);
1039 for (position, &col_idx) in columns.iter().enumerate() {
1040 let value = values.get(col_idx)?;
1041 if matches!(value, SqliteValue::Null) {
1042 return None;
1043 }
1044 let collation = collations.get(position).and_then(|c| c.as_deref());
1045 if !append_unique_constraint_key_component(&mut key, value, collation) {
1046 return None;
1047 }
1048 }
1049 Some(key)
1050 }
1051
1052 fn unique_constraint_matches_row(
1053 existing_values: &[SqliteValue],
1054 new_values: &[SqliteValue],
1055 columns: &[usize],
1056 collations: &[Option<String>],
1057 registry: &CollationRegistry,
1058 ) -> bool {
1059 for (position, &col_idx) in columns.iter().enumerate() {
1060 let Some(existing_value) = existing_values.get(col_idx) else {
1061 return false;
1062 };
1063 let Some(new_value) = new_values.get(col_idx) else {
1064 return false;
1065 };
1066 if existing_value.is_null() || new_value.is_null() {
1067 return false;
1068 }
1069 let collation = collations.get(position).and_then(|c| c.as_deref());
1070 if cmp_values_collated(existing_value, new_value, collation, registry)
1071 != Ordering::Equal
1072 {
1073 return false;
1074 }
1075 }
1076 true
1077 }
1078
1079 fn remove_unique_entries(&mut self, rowid: i64, values: &[SqliteValue]) {
1080 for constraint in &mut self.unique_constraints {
1081 let Some(index) = constraint.index.as_mut() else {
1082 continue;
1083 };
1084 let Some(key) = Self::unique_key_for_constraint(
1085 values,
1086 &constraint.columns,
1087 &constraint.collations,
1088 ) else {
1089 continue;
1090 };
1091 let remove_entry = if let Some(rowids) = index.get_mut(&key) {
1092 rowids.remove(&rowid);
1093 rowids.is_empty()
1094 } else {
1095 false
1096 };
1097 if remove_entry {
1098 index.remove(&key);
1099 }
1100 }
1101 }
1102
1103 fn unique_keys_for_values(&self, values: &[SqliteValue]) -> Vec<Option<Vec<u8>>> {
1104 self.unique_constraints
1105 .iter()
1106 .map(|constraint| {
1107 constraint.index.as_ref().and_then(|_| {
1108 Self::unique_key_for_constraint(
1109 values,
1110 &constraint.columns,
1111 &constraint.collations,
1112 )
1113 })
1114 })
1115 .collect()
1116 }
1117
1118 fn insert_unique_keys(&mut self, rowid: i64, keys: &[Option<Vec<u8>>]) {
1119 for (maybe_key, constraint) in keys.iter().zip(&mut self.unique_constraints) {
1120 if let (Some(key), Some(index)) = (maybe_key, constraint.index.as_mut()) {
1121 index.entry(key.clone()).or_default().insert(rowid);
1122 }
1123 }
1124 }
1125
1126 fn rebuild_unique_indexes(&mut self) {
1127 for constraint in &mut self.unique_constraints {
1128 if let Some(index) = constraint.index.as_mut() {
1129 index.clear();
1130 }
1131 }
1132 for row in &self.rows {
1133 for constraint in &mut self.unique_constraints {
1134 let Some(index) = constraint.index.as_mut() else {
1135 continue;
1136 };
1137 if let Some(key) = Self::unique_key_for_constraint(
1138 &row.values,
1139 &constraint.columns,
1140 &constraint.collations,
1141 ) {
1142 index.entry(key).or_default().insert(row.rowid);
1143 }
1144 }
1145 }
1146 }
1147}
1148
1149#[derive(Debug, Clone)]
1151struct MemCursor {
1152 root_page: i32,
1154 #[allow(dead_code)]
1156 writable: bool,
1157 position: Option<usize>,
1159 pseudo_row: Option<Vec<SqliteValue>>,
1161 cached_pseudo_row: Option<(SqliteValue, Vec<SqliteValue>)>,
1163 pseudo_reg: Option<i32>,
1165 is_pseudo: bool,
1167}
1168
1169impl MemCursor {
1170 fn new(root_page: i32, writable: bool) -> Self {
1171 Self {
1172 root_page,
1173 writable,
1174 position: None,
1175 pseudo_row: None,
1176 cached_pseudo_row: None,
1177 pseudo_reg: None,
1178 is_pseudo: false,
1179 }
1180 }
1181
1182 fn new_pseudo(reg: i32) -> Self {
1183 Self {
1184 root_page: -1,
1185 writable: false,
1186 position: None,
1187 pseudo_row: None,
1188 cached_pseudo_row: None,
1189 pseudo_reg: Some(reg),
1190 is_pseudo: true,
1191 }
1192 }
1193}
1194
1195#[derive(Debug, Clone)]
1202struct SorterRow {
1203 values: Vec<SqliteValue>,
1205 blob: Vec<u8>,
1207 source_sequence: u64,
1209}
1210
1211#[derive(Clone)]
1218struct SorterCursor {
1219 key_columns: usize,
1221 sort_key_orders: Vec<SortKeyOrder>,
1223 collations: Vec<Option<String>>,
1225 collation_registry: Arc<Mutex<CollationRegistry>>,
1227 rows: Vec<SorterRow>,
1229 position: Option<usize>,
1231 cached_row_position: Option<usize>,
1233 cached_row_decode: RowDecodeScratch,
1235 memory_used: usize,
1237 spill_threshold: usize,
1239 spill_runs: Vec<SpillRun>,
1241 top_n_limit: Option<usize>,
1252 next_source_sequence: u64,
1254 rows_sorted_total: u64,
1256 spill_pages_total: u64,
1258}
1259
1260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1261enum SortKeyOrder {
1262 Asc,
1264 Desc,
1266 AscNullsLast,
1268 DescNullsFirst,
1270}
1271
1272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1273enum DecodeCacheInvalidationReason {
1274 PositionChange,
1275 WriteMutation,
1276 PseudoRowChange,
1277}
1278
1279const SORTER_DEFAULT_SPILL_THRESHOLD: usize = 100 * 1024 * 1024;
1286
1287const BLOOM_FILTER_WORDS: usize = 128;
1289
1290fn bloom_hash(val: &SqliteValue) -> u64 {
1292 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1294 let bytes: &[u8] = match val {
1295 SqliteValue::Null => &[0],
1296 SqliteValue::Integer(i) => {
1297 h ^= 1;
1299 h = h.wrapping_mul(0x0100_0000_01b3);
1300 for b in i.to_le_bytes() {
1301 h ^= u64::from(b);
1302 h = h.wrapping_mul(0x0100_0000_01b3);
1303 }
1304 return h;
1305 }
1306 SqliteValue::Float(f) => {
1307 h ^= 2;
1308 h = h.wrapping_mul(0x0100_0000_01b3);
1309 for b in f.to_le_bytes() {
1310 h ^= u64::from(b);
1311 h = h.wrapping_mul(0x0100_0000_01b3);
1312 }
1313 return h;
1314 }
1315 SqliteValue::Text(s) => s.as_bytes_direct(),
1316 SqliteValue::Blob(b) => b,
1317 };
1318 for &b in bytes {
1319 h ^= u64::from(b);
1320 h = h.wrapping_mul(0x0100_0000_01b3);
1321 }
1322 h
1323}
1324
1325#[cfg(not(target_arch = "wasm32"))]
1333const SORTER_SPILL_PAGE_SIZE: usize = fsqlite_types::limits::DEFAULT_PAGE_SIZE as usize;
1334
1335#[derive(Debug, Clone)]
1340struct SpillRun {
1341 path: std::path::PathBuf,
1343 #[allow(dead_code)]
1345 record_count: u64,
1346 #[allow(dead_code)]
1348 bytes_written: u64,
1349}
1350
1351impl SorterCursor {
1352 #[cfg(test)]
1353 fn new(
1354 key_columns: usize,
1355 sort_key_orders: Vec<SortKeyOrder>,
1356 collations: Vec<Option<String>>,
1357 ) -> Self {
1358 Self::with_collation_registry(
1359 key_columns,
1360 sort_key_orders,
1361 collations,
1362 Arc::new(Mutex::new(CollationRegistry::new())),
1363 None,
1364 )
1365 }
1366
1367 fn with_collation_registry(
1368 key_columns: usize,
1369 mut sort_key_orders: Vec<SortKeyOrder>,
1370 collations: Vec<Option<String>>,
1371 collation_registry: Arc<Mutex<CollationRegistry>>,
1372 top_n_limit: Option<usize>,
1373 ) -> Self {
1374 let key_columns = key_columns.max(1);
1375 if sort_key_orders.len() < key_columns {
1376 sort_key_orders.resize(key_columns, SortKeyOrder::Asc);
1377 }
1378 sort_key_orders.truncate(key_columns);
1379 Self {
1380 key_columns,
1381 sort_key_orders,
1382 collations,
1383 collation_registry,
1384 rows: Vec::new(),
1385 position: None,
1386 cached_row_position: None,
1387 cached_row_decode: RowDecodeScratch::default(),
1388 memory_used: 0,
1389 spill_threshold: SORTER_DEFAULT_SPILL_THRESHOLD,
1390 spill_runs: Vec::new(),
1391 top_n_limit,
1392 next_source_sequence: 0,
1393 rows_sorted_total: 0,
1394 spill_pages_total: 0,
1395 }
1396 }
1397
1398 fn compare_top_n_rows(
1399 &self,
1400 lhs: &SorterRow,
1401 rhs: &SorterRow,
1402 collation_registry: &CollationRegistry,
1403 ) -> Ordering {
1404 let key_ordering = compare_sorter_rows(
1405 &lhs.values,
1406 &rhs.values,
1407 self.key_columns,
1408 &self.sort_key_orders,
1409 &self.collations,
1410 collation_registry,
1411 );
1412 if key_ordering == Ordering::Equal {
1413 lhs.source_sequence.cmp(&rhs.source_sequence)
1414 } else {
1415 key_ordering
1416 }
1417 }
1418
1419 fn sift_top_n_heap_up(&mut self, mut index: usize, collation_registry: &CollationRegistry) {
1420 while index > 0 {
1421 let parent = (index - 1) / 2;
1422 if self.compare_top_n_rows(&self.rows[parent], &self.rows[index], collation_registry)
1423 != Ordering::Less
1424 {
1425 break;
1426 }
1427 self.rows.swap(parent, index);
1428 index = parent;
1429 }
1430 }
1431
1432 fn sift_top_n_heap_down(&mut self, mut index: usize, collation_registry: &CollationRegistry) {
1433 loop {
1434 let left = index * 2 + 1;
1435 if left >= self.rows.len() {
1436 break;
1437 }
1438 let right = left + 1;
1439 let mut worse_child = left;
1440 if right < self.rows.len()
1441 && self.compare_top_n_rows(&self.rows[left], &self.rows[right], collation_registry)
1442 == Ordering::Less
1443 {
1444 worse_child = right;
1445 }
1446 if self.compare_top_n_rows(
1447 &self.rows[index],
1448 &self.rows[worse_child],
1449 collation_registry,
1450 ) != Ordering::Less
1451 {
1452 break;
1453 }
1454 self.rows.swap(index, worse_child);
1455 index = worse_child;
1456 }
1457 }
1458
1459 fn would_retain_top_n(
1466 &self,
1467 candidate_values: &[SqliteValue],
1468 collation_registry: &CollationRegistry,
1469 ) -> bool {
1470 let Some(limit) = self.top_n_limit else {
1471 return true;
1472 };
1473 if limit == 0 {
1474 return false;
1475 }
1476 if self.rows.len() < limit {
1477 return true;
1478 }
1479 self.rows.first().is_some_and(|worst| {
1480 compare_sorter_rows(
1481 candidate_values,
1482 &worst.values,
1483 self.key_columns,
1484 &self.sort_key_orders,
1485 &self.collations,
1486 collation_registry,
1487 ) == Ordering::Less
1488 })
1489 }
1490
1491 fn estimate_row_size(values: &[SqliteValue], blob: &[u8]) -> usize {
1493 let mut size = std::mem::size_of::<SorterRow>() + values.len() * 24 + blob.len();
1495 for val in values {
1496 match val {
1497 SqliteValue::Text(s) => size += s.len(),
1498 SqliteValue::Blob(b) => size += b.len(),
1499 _ => {}
1500 }
1501 }
1502 size
1503 }
1504
1505 fn insert_row(&mut self, values: Vec<SqliteValue>, blob: Vec<u8>) -> Result<()> {
1507 if self.top_n_limit == Some(0) {
1508 return Ok(());
1509 }
1510
1511 let new_row_size = Self::estimate_row_size(&values, &blob);
1512 if let Some(limit) = self.top_n_limit {
1513 #[cfg(not(target_arch = "wasm32"))]
1517 let can_downgrade_before_discarding = self.rows.len() < limit
1518 && self.memory_used.saturating_add(new_row_size) >= self.spill_threshold;
1519 #[cfg(target_arch = "wasm32")]
1520 let can_downgrade_before_discarding = false;
1521 if can_downgrade_before_discarding {
1522 self.rows.sort_by_key(|row| row.source_sequence);
1526 self.top_n_limit = None;
1527 self.next_source_sequence = 0;
1528 self.invalidate_output_row_cache();
1529 } else {
1530 let new_row = SorterRow {
1531 values,
1532 blob,
1533 source_sequence: self.next_source_sequence,
1534 };
1535 self.next_source_sequence = self.next_source_sequence.saturating_add(1);
1536 let collation_registry = Arc::clone(&self.collation_registry);
1537 let coll_guard = collation_registry.lock().unwrap_or_else(|e| e.into_inner());
1538 if self.rows.len() < limit {
1539 self.rows.push(new_row);
1540 self.sift_top_n_heap_up(self.rows.len() - 1, &coll_guard);
1541 self.memory_used += new_row_size;
1542 self.invalidate_output_row_cache();
1543 return Ok(());
1544 }
1545
1546 if self.would_retain_top_n(&new_row.values, &coll_guard) {
1547 let removed_row = std::mem::replace(&mut self.rows[0], new_row);
1548 let removed_size =
1549 Self::estimate_row_size(&removed_row.values, &removed_row.blob);
1550 self.memory_used = self.memory_used.saturating_sub(removed_size);
1551 self.memory_used = self.memory_used.saturating_add(new_row_size);
1552 self.sift_top_n_heap_down(0, &coll_guard);
1553 self.invalidate_output_row_cache();
1554 }
1555 return Ok(());
1556 }
1557 }
1558
1559 self.memory_used += new_row_size;
1560 self.rows.push(SorterRow {
1561 values,
1562 blob,
1563 source_sequence: 0,
1564 });
1565 self.invalidate_output_row_cache();
1566
1567 if self.memory_used >= self.spill_threshold {
1568 self.spill_to_disk()?;
1569 }
1570 Ok(())
1571 }
1572
1573 #[cfg(not(target_arch = "wasm32"))]
1575 fn spill_to_disk(&mut self) -> Result<()> {
1576 use std::io::Write;
1577
1578 if self.rows.is_empty() {
1579 return Ok(());
1580 }
1581
1582 let key_columns = self.key_columns;
1584 let orders = self.sort_key_orders.clone();
1585 let colls = self.collations.clone();
1586 let coll_guard = self
1587 .collation_registry
1588 .lock()
1589 .unwrap_or_else(|e| e.into_inner());
1590 self.rows.sort_by(|lhs, rhs| {
1591 compare_sorter_rows(
1592 &lhs.values,
1593 &rhs.values,
1594 key_columns,
1595 &orders,
1596 &colls,
1597 &coll_guard,
1598 )
1599 });
1600 drop(coll_guard);
1601
1602 let tmp = tempfile::NamedTempFile::new()
1606 .map_err(|e| FrankenError::internal(format!("sorter spill tempfile: {e}")))?;
1607 let (file, path) = tmp
1608 .keep()
1609 .map_err(|e| FrankenError::internal(format!("sorter spill keep: {e}")))?;
1610 let mut writer = std::io::BufWriter::new(file);
1611
1612 let record_count = self.rows.len() as u64;
1613 let mut bytes_written: u64 = 0;
1614 for row in &self.rows {
1615 #[allow(clippy::cast_possible_truncation)]
1620 let len_bytes = (row.blob.len() as u32).to_le_bytes();
1621 writer
1622 .write_all(&len_bytes)
1623 .map_err(|e| FrankenError::internal(format!("sorter spill write len: {e}")))?;
1624 writer
1625 .write_all(&row.blob)
1626 .map_err(|e| FrankenError::internal(format!("sorter spill write data: {e}")))?;
1627 bytes_written += 4 + row.blob.len() as u64;
1628 }
1629 writer
1630 .flush()
1631 .map_err(|e| FrankenError::internal(format!("sorter spill flush: {e}")))?;
1632
1633 #[allow(clippy::cast_possible_truncation)]
1634 let pages = (bytes_written as usize).div_ceil(SORTER_SPILL_PAGE_SIZE);
1635 self.spill_pages_total += pages as u64;
1636
1637 tracing::warn!(
1638 rows = record_count,
1639 bytes = bytes_written,
1640 pages,
1641 run_index = self.spill_runs.len(),
1642 "sorter spilling to disk"
1643 );
1644
1645 self.spill_runs.push(SpillRun {
1646 path,
1647 record_count,
1648 bytes_written,
1649 });
1650
1651 self.rows_sorted_total += record_count;
1652 self.rows.clear();
1653 self.invalidate_output_row_cache();
1654 self.memory_used = 0;
1655 Ok(())
1656 }
1657
1658 #[cfg(target_arch = "wasm32")]
1661 fn spill_to_disk(&mut self) -> Result<()> {
1662 self.spill_threshold = usize::MAX;
1663 tracing::warn!(
1664 rows = self.rows.len(),
1665 bytes = self.memory_used,
1666 "sorter spill requested on wasm; keeping rows in memory"
1667 );
1668 Ok(())
1669 }
1670
1671 #[allow(clippy::too_many_lines)]
1676 fn sort(&mut self) -> Result<()> {
1677 let coll_guard = self
1679 .collation_registry
1680 .lock()
1681 .unwrap_or_else(|e| e.into_inner());
1682
1683 if self.spill_runs.is_empty() {
1684 if self.top_n_limit.is_some() {
1685 let key_columns = self.key_columns;
1686 let orders = self.sort_key_orders.clone();
1687 let colls = self.collations.clone();
1688 self.rows.sort_by(|lhs, rhs| {
1689 let key_ordering = compare_sorter_rows(
1690 &lhs.values,
1691 &rhs.values,
1692 key_columns,
1693 &orders,
1694 &colls,
1695 &coll_guard,
1696 );
1697 if key_ordering == Ordering::Equal {
1698 lhs.source_sequence.cmp(&rhs.source_sequence)
1699 } else {
1700 key_ordering
1701 }
1702 });
1703 self.rows_sorted_total += self.rows.len() as u64;
1704 return Ok(());
1705 }
1706
1707 let key_columns = self.key_columns;
1709 let orders = self.sort_key_orders.clone();
1710 let colls = self.collations.clone();
1711 self.rows.sort_by(|lhs, rhs| {
1712 compare_sorter_rows(
1713 &lhs.values,
1714 &rhs.values,
1715 key_columns,
1716 &orders,
1717 &colls,
1718 &coll_guard,
1719 )
1720 });
1721 self.rows_sorted_total += self.rows.len() as u64;
1722 return Ok(());
1723 }
1724
1725 let key_columns = self.key_columns;
1727 let orders = self.sort_key_orders.clone();
1728 let colls = self.collations.clone();
1729 self.rows.sort_by(|lhs, rhs| {
1730 compare_sorter_rows(
1731 &lhs.values,
1732 &rhs.values,
1733 key_columns,
1734 &orders,
1735 &colls,
1736 &coll_guard,
1737 )
1738 });
1739
1740 let mut run_iters: Vec<RunIterator> = Vec::with_capacity(self.spill_runs.len() + 1);
1742 for run in &self.spill_runs {
1743 run_iters.push(RunIterator::from_file(&run.path, self.key_columns)?);
1744 }
1745 if !self.rows.is_empty() {
1746 let mem_rows = std::mem::take(&mut self.rows);
1747 self.rows_sorted_total += mem_rows.len() as u64;
1748 run_iters.push(RunIterator::from_memory(mem_rows));
1749 }
1750
1751 let mut merged: Vec<SorterRow> = Vec::with_capacity(self.rows_sorted_total as usize);
1753
1754 for iter in &mut run_iters {
1756 iter.advance()?;
1757 }
1758
1759 loop {
1760 let mut best_idx: Option<usize> = None;
1762 for (i, iter) in run_iters.iter().enumerate() {
1763 let Some(row) = iter.current_values() else {
1764 continue;
1765 };
1766 if let Some(bi) = best_idx {
1767 if let Some(best_row) = run_iters[bi].current_values() {
1768 if compare_sorter_rows(
1769 row,
1770 best_row,
1771 key_columns,
1772 &orders,
1773 &colls,
1774 &coll_guard,
1775 ) == Ordering::Less
1776 {
1777 best_idx = Some(i);
1778 }
1779 }
1780 } else {
1781 best_idx = Some(i);
1782 }
1783 }
1784
1785 let Some(idx) = best_idx else {
1786 break; };
1788
1789 if let Some(row) = run_iters[idx].take_current() {
1790 merged.push(row);
1791 }
1792 run_iters[idx].advance()?;
1793 }
1794
1795 tracing::debug!(
1796 rows = merged.len(),
1797 runs = self.spill_runs.len() + 1,
1798 "sorter merge complete"
1799 );
1800
1801 for run in &self.spill_runs {
1803 let _ = std::fs::remove_file(&run.path);
1804 }
1805 self.spill_runs.clear();
1806 self.rows = merged;
1807 drop(coll_guard);
1808 self.invalidate_output_row_cache();
1809 self.memory_used = 0;
1810 Ok(())
1811 }
1812
1813 fn reset(&mut self) {
1815 self.rows.clear();
1816 self.position = None;
1817 self.invalidate_output_row_cache();
1818 self.memory_used = 0;
1819 self.next_source_sequence = 0;
1820 for run in &self.spill_runs {
1822 let _ = std::fs::remove_file(&run.path);
1823 }
1824 self.spill_runs.clear();
1825 }
1826
1827 fn invalidate_output_row_cache(&mut self) {
1828 self.cached_row_position = None;
1829 self.cached_row_decode.invalidate();
1830 }
1831}
1832
1833impl Drop for SorterCursor {
1834 fn drop(&mut self) {
1835 for run in &self.spill_runs {
1836 let _ = std::fs::remove_file(&run.path);
1837 }
1838 }
1839}
1840
1841enum RunIterator {
1843 File {
1845 reader: std::io::BufReader<std::fs::File>,
1846 current: Option<SorterRow>,
1847 key_columns: usize,
1849 },
1850 Memory {
1852 rows: std::vec::IntoIter<SorterRow>,
1853 current: Option<SorterRow>,
1854 },
1855}
1856
1857impl RunIterator {
1858 fn from_file(path: &std::path::Path, key_columns: usize) -> Result<Self> {
1859 let file = std::fs::File::open(path)
1860 .map_err(|e| FrankenError::internal(format!("sorter run open: {e}")))?;
1861 Ok(Self::File {
1862 reader: std::io::BufReader::new(file),
1863 current: None,
1864 key_columns,
1865 })
1866 }
1867
1868 fn from_memory(rows: Vec<SorterRow>) -> Self {
1869 Self::Memory {
1870 rows: rows.into_iter(),
1871 current: None,
1872 }
1873 }
1874
1875 fn current_values(&self) -> Option<&Vec<SqliteValue>> {
1876 match self {
1877 Self::File { current, .. } | Self::Memory { current, .. } => {
1878 current.as_ref().map(|r| &r.values)
1879 }
1880 }
1881 }
1882
1883 fn take_current(&mut self) -> Option<SorterRow> {
1884 match self {
1885 Self::File { current, .. } | Self::Memory { current, .. } => current.take(),
1886 }
1887 }
1888
1889 fn advance(&mut self) -> Result<()> {
1890 match self {
1891 Self::File {
1892 reader,
1893 current,
1894 key_columns,
1895 } => {
1896 use std::io::Read;
1897 let mut len_buf = [0u8; 4];
1898 match reader.read_exact(&mut len_buf) {
1899 Ok(()) => {
1900 let len = u32::from_le_bytes(len_buf) as usize;
1901 let mut buf = vec![0u8; len];
1902 reader
1903 .read_exact(&mut buf)
1904 .map_err(|e| FrankenError::internal(format!("sorter run read: {e}")))?;
1905 let values = fsqlite_types::record::parse_record_prefix(&buf, *key_columns)
1907 .ok_or_else(|| {
1908 FrankenError::internal("sorter run: malformed record")
1909 })?;
1910 *current = Some(SorterRow {
1911 values,
1912 blob: buf,
1913 source_sequence: 0,
1914 });
1915 }
1916 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
1917 *current = None;
1918 }
1919 Err(e) => {
1920 return Err(FrankenError::internal(format!("sorter run read len: {e}")));
1921 }
1922 }
1923 }
1924 Self::Memory { rows, current } => {
1925 *current = rows.next();
1926 }
1927 }
1928 Ok(())
1929 }
1930}
1931
1932#[derive(Clone)]
1950struct ConcurrentContext {
1951 session_id: u64,
1953 txn_id: u64,
1955 snapshot_high: CommitSeq,
1957 handle: SharedConcurrentHandle,
1959 lock_table: Arc<InProcessPageLockTable>,
1961 commit_index: Arc<CommitIndex>,
1963 busy_timeout_ms: u64,
1965}
1966
1967impl ConcurrentContext {
1968 fn new(
1969 session_id: u64,
1970 handle: SharedConcurrentHandle,
1971 lock_table: Arc<InProcessPageLockTable>,
1972 commit_index: Arc<CommitIndex>,
1973 busy_timeout_ms: u64,
1974 ) -> Self {
1975 let (txn_id, snapshot_high) = {
1976 let handle_guard = handle.lock();
1977 (
1978 handle_guard.txn_token().id.get(),
1979 handle_guard.snapshot().high,
1980 )
1981 };
1982 Self {
1983 session_id,
1984 txn_id,
1985 snapshot_high,
1986 handle,
1987 lock_table,
1988 commit_index,
1989 busy_timeout_ms,
1990 }
1991 }
1992}
1993
1994#[derive(Clone)]
2000pub struct SharedTxnPageIo {
2001 txn: Rc<RefCell<TransactionKind>>,
2002 concurrent: Rc<RefCell<Option<ConcurrentContext>>>,
2005}
2006
2007#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2008enum ConcurrentWriteTier {
2009 Tier0AlreadyOwned,
2010 Tier1FirstTouch,
2011 Tier2CommitSurfaceRare,
2012}
2013
2014#[cfg(test)]
2015std::thread_local! {
2016 static CONCURRENT_PAGE_LOCK_WINDOW_HOOK: RefCell<Option<Box<dyn FnOnce()>>> =
2017 RefCell::new(None);
2018}
2019
2020#[cfg(test)]
2021fn install_concurrent_page_lock_window_hook(hook: impl FnOnce() + 'static) {
2022 CONCURRENT_PAGE_LOCK_WINDOW_HOOK.with(|slot| {
2023 let replaced = slot.borrow_mut().replace(Box::new(hook));
2024 assert!(
2025 replaced.is_none(),
2026 "concurrent page-lock window hook already installed"
2027 );
2028 });
2029}
2030
2031#[inline]
2032fn fire_concurrent_page_lock_window_hook() {
2033 #[cfg(test)]
2034 CONCURRENT_PAGE_LOCK_WINDOW_HOOK.with(|slot| {
2035 if let Some(hook) = slot.borrow_mut().take() {
2036 hook();
2037 }
2038 });
2039}
2040
2041impl std::fmt::Debug for SharedTxnPageIo {
2042 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2043 f.debug_struct("SharedTxnPageIo")
2044 .field("rc_count", &Rc::strong_count(&self.txn))
2045 .field("concurrent", &self.concurrent.borrow().is_some())
2046 .finish()
2047 }
2048}
2049
2050impl SharedTxnPageIo {
2051 fn from_parts(txn: TransactionKind, concurrent: Option<ConcurrentContext>) -> Self {
2052 Self {
2053 txn: Rc::new(RefCell::new(txn)),
2054 concurrent: Rc::new(RefCell::new(concurrent)),
2055 }
2056 }
2057
2058 #[cfg(test)]
2059 fn new(txn: impl Into<TransactionKind>) -> Self {
2060 Self::from_parts(txn.into(), None)
2061 }
2062
2063 pub fn with_concurrent(
2065 txn: impl Into<TransactionKind>,
2066 session_id: u64,
2067 handle: SharedConcurrentHandle,
2068 lock_table: Arc<InProcessPageLockTable>,
2069 commit_index: Arc<CommitIndex>,
2070 busy_timeout_ms: u64,
2071 ) -> Self {
2072 Self::from_parts(
2073 txn.into(),
2074 Some(ConcurrentContext::new(
2075 session_id,
2076 handle,
2077 lock_table,
2078 commit_index,
2079 busy_timeout_ms,
2080 )),
2081 )
2082 }
2083
2084 pub fn into_inner(self) -> Result<TransactionKind> {
2087 match Rc::try_unwrap(self.txn) {
2088 Ok(cell) => Ok(cell.into_inner()),
2089 Err(rc) => Err(FrankenError::Internal(format!(
2090 "SharedTxnPageIo: {} outstanding Rc references",
2091 Rc::strong_count(&rc),
2092 ))),
2093 }
2094 }
2095
2096 fn refill(
2100 &self,
2101 txn: TransactionKind,
2102 concurrent: Option<ConcurrentContext>,
2103 ) -> TransactionKind {
2104 self.concurrent.replace(concurrent);
2105 self.txn.replace(txn)
2106 }
2107
2108 fn drain(&self) -> TransactionKind {
2113 self.txn.replace(TransactionKind::Drained)
2114 }
2115
2116 fn concurrent_context(&self) -> Option<ConcurrentContext> {
2117 self.concurrent.borrow().clone()
2118 }
2119
2120 fn clear_stale_synthetic_pending_commit_surface(
2121 &self,
2122 _cx: &Cx,
2123 operation: &str,
2124 ) -> Result<()> {
2125 let Some(ctx) = self.concurrent_context() else {
2126 return Ok(());
2127 };
2128 let page_one_is_synthetic = {
2132 let handle = ctx.handle.lock();
2133 concurrent_page_is_synthetic_conflict_only(&handle, PageNumber::ONE)
2134 };
2135 if !page_one_is_synthetic {
2136 return Ok(());
2137 }
2138 if self.txn.borrow().page_one_in_pending_commit_surface()? {
2139 return Ok(());
2140 }
2141
2142 let metrics_enabled = vdbe_metrics_enabled();
2143 let clear_started = metrics_enabled.then(Instant::now);
2144 let mut handle = ctx.handle.lock();
2145 if concurrent_page_is_synthetic_conflict_only(&handle, PageNumber::ONE) {
2146 concurrent_clear_page_state(
2147 &mut handle,
2148 &ctx.lock_table,
2149 ctx.session_id,
2150 PageNumber::ONE,
2151 )
2152 .map_err(|restore_error| {
2153 FrankenError::Internal(format!(
2154 "MVCC pending commit surface clear failed for page {} during {operation}: {restore_error}",
2155 PageNumber::ONE.get()
2156 ))
2157 })?;
2158 add_vdbe_counter_if(
2159 metrics_enabled,
2160 &FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL,
2161 1,
2162 );
2163 if let Some(clear_started) = clear_started {
2164 add_vdbe_duration_if(
2165 metrics_enabled,
2166 &FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL,
2167 clear_started,
2168 );
2169 }
2170 }
2171
2172 Ok(())
2173 }
2174
2175 fn classify_concurrent_write_tier(
2176 ctx: &ConcurrentContext,
2177 page_no: PageNumber,
2178 ) -> ConcurrentWriteTier {
2179 if ctx.handle.lock().holds_page_lock(page_no) {
2180 return ConcurrentWriteTier::Tier0AlreadyOwned;
2181 }
2182
2183 if page_no == PageNumber::ONE {
2184 ConcurrentWriteTier::Tier2CommitSurfaceRare
2185 } else {
2186 ConcurrentWriteTier::Tier1FirstTouch
2187 }
2188 }
2189
2190 fn restore_concurrent_page_state(
2191 ctx: &ConcurrentContext,
2192 page_state: &ConcurrentPageState,
2193 restore_label: &str,
2194 ) -> Result<()> {
2195 let mut handle = ctx.handle.lock();
2196 concurrent_restore_page_state(&mut handle, &ctx.lock_table, ctx.session_id, page_state)
2197 .map_err(|restore_error| {
2198 FrankenError::Internal(format!("{restore_label}: {restore_error}"))
2199 })
2200 }
2201
2202 fn post_acquire_fcw_conflict(
2203 ctx: &ConcurrentContext,
2204 page_no: PageNumber,
2205 ) -> Result<Option<CommitSeq>> {
2206 if !ctx.handle.lock().holds_page_lock(page_no) {
2207 return Err(FrankenError::Internal(format!(
2208 "MVCC page-lock acquisition for page {} returned success without ownership",
2209 page_no.get()
2210 )));
2211 }
2212 Ok(ctx
2213 .commit_index
2214 .latest(page_no)
2215 .filter(|seq| *seq > ctx.snapshot_high))
2216 }
2217
2218 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2223 async fn write_page_tier0_already_owned(
2224 &self,
2225 cx: &Cx,
2226 ctx: &ConcurrentContext,
2227 page_no: PageNumber,
2228 page_data_base: PageData,
2229 ) -> Result<()> {
2230 add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL, 1);
2231 let mut handle = ctx.handle.lock();
2232 let prior_state = concurrent_page_state(&handle, page_no);
2233 if let Err(prepare_error) =
2234 concurrent_prepare_write_page(&mut handle, &ctx.lock_table, ctx.session_id, page_no)
2235 {
2236 return Err(FrankenError::Internal(format!(
2237 "MVCC fast-path prepare failed: {prepare_error}"
2238 )));
2239 }
2240 if let Err(stage_error) = concurrent_stage_prepared_write_marker(&mut handle, page_no) {
2241 if let Err(restore_error) = concurrent_restore_page_state(
2242 &mut handle,
2243 &ctx.lock_table,
2244 ctx.session_id,
2245 &prior_state,
2246 ) {
2247 return Err(FrankenError::Internal(format!(
2248 "MVCC fast-path staging failed: {stage_error}; MVCC fast-path restore failed: {restore_error}"
2249 )));
2250 }
2251 return Err(FrankenError::Internal(format!(
2252 "MVCC fast-path staging failed: {stage_error}"
2253 )));
2254 }
2255 drop(handle);
2256
2257 if let Err(write_error) = self
2258 .txn
2259 .borrow_mut()
2260 .write_page_data(cx, page_no, page_data_base)
2261 .await
2262 {
2263 Self::restore_concurrent_page_state(
2264 ctx,
2265 &prior_state,
2266 &format!("pager write_page failed: {write_error}; MVCC fast-path restore failed"),
2267 )?;
2268 return Err(write_error);
2269 }
2270
2271 self.clear_stale_synthetic_pending_commit_surface(cx, "write_page_fast")?;
2272 Ok(())
2273 }
2274
2275 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2280 async fn write_page_tier1_first_touch(
2281 &self,
2282 cx: &Cx,
2283 ctx: &ConcurrentContext,
2284 page_no: PageNumber,
2285 page_data_base: PageData,
2286 ) -> Result<()> {
2287 add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL, 1);
2288 let prior_page_state = {
2289 let handle = ctx.handle.lock();
2290 concurrent_page_state(&handle, page_no)
2291 };
2292
2293 let started = Instant::now();
2294 let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2295 loop {
2296 observe_execution_cancellation(cx)?;
2297 let (write_result, conflicting_commit_seq) = {
2298 let mut handle = ctx.handle.lock();
2299 let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2300 .then(|| ctx.commit_index.latest(page_no))
2301 .flatten()
2302 .filter(|seq| *seq > ctx.snapshot_high);
2303 let write_result = conflicting_commit_seq.is_none().then(|| {
2304 fire_concurrent_page_lock_window_hook();
2305 concurrent_prepare_write_page(
2306 &mut handle,
2307 &ctx.lock_table,
2308 ctx.session_id,
2309 page_no,
2310 )
2311 });
2312 (write_result, conflicting_commit_seq)
2313 };
2314 let txn_id = ctx.txn_id;
2315 let snapshot_high = ctx.snapshot_high.get();
2316
2317 if let Some(conflicting_commit_seq) = conflicting_commit_seq {
2318 add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2319 let error = FrankenError::BusySnapshot {
2320 conflicting_pages: page_no.get().to_string(),
2321 };
2322 Self::restore_concurrent_page_state(
2323 ctx,
2324 &prior_page_state,
2325 &format!("{error}; MVCC state restore failed"),
2326 )?;
2327 tracing::warn!(
2328 txn_id,
2329 commit_seq = conflicting_commit_seq.get(),
2330 snapshot_high,
2331 page_id = page_no.get(),
2332 visibility_decision = "write_snapshot_stale",
2333 conflict_reason = "fcw_base_drift",
2334 "mvcc write rejected due to stale snapshot"
2335 );
2336 return Err(error);
2337 }
2338
2339 match write_result.ok_or_else(|| {
2340 FrankenError::Internal("write result must exist when snapshot is valid".to_owned())
2341 })? {
2342 Ok(()) => {
2343 match Self::post_acquire_fcw_conflict(ctx, page_no) {
2344 Ok(None) => {}
2345 Ok(Some(conflicting_commit_seq)) => {
2346 add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2347 let error = FrankenError::BusySnapshot {
2348 conflicting_pages: page_no.get().to_string(),
2349 };
2350 Self::restore_concurrent_page_state(
2351 ctx,
2352 &prior_page_state,
2353 &format!("{error}; MVCC post-acquire state restore failed"),
2354 )?;
2355 tracing::warn!(
2356 txn_id,
2357 commit_seq = conflicting_commit_seq.get(),
2358 snapshot_high,
2359 page_id = page_no.get(),
2360 visibility_decision = "write_post_acquire_snapshot_stale",
2361 conflict_reason = "fcw_base_drift",
2362 write_tier = "tier1_first_touch",
2363 "mvcc write rejected after page-lock acquisition"
2364 );
2365 return Err(error);
2366 }
2367 Err(error) => {
2368 Self::restore_concurrent_page_state(
2369 ctx,
2370 &prior_page_state,
2371 &format!("{error}; MVCC post-acquire state restore failed"),
2372 )?;
2373 return Err(error);
2374 }
2375 }
2376 tracing::debug!(
2377 txn_id,
2378 commit_seq = snapshot_high,
2379 snapshot_high,
2380 page_id = page_no.get(),
2381 visibility_decision = "write_lock_acquired",
2382 conflict_reason = "none",
2383 write_tier = "tier1_first_touch",
2384 "mvcc write visibility decision"
2385 );
2386 break;
2387 }
2388 Err(MvccError::Busy) => {
2389 let remaining = deadline.saturating_sub(started.elapsed());
2390 match wait_for_page_lock_holder_change(cx, ctx, page_no, remaining) {
2391 Ok(true) => {
2392 add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL, 1);
2393 tracing::warn!(
2394 txn_id,
2395 commit_seq = snapshot_high,
2396 snapshot_high,
2397 page_id = page_no.get(),
2398 visibility_decision = "write_retry",
2399 conflict_reason = "page_lock_busy",
2400 retry_policy = "park_wake",
2401 retry_wait_ms = remaining.as_millis(),
2402 write_tier = "tier1_first_touch",
2403 "mvcc write conflict detected"
2404 );
2405 }
2406 Ok(false) => {
2407 add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL, 1);
2408 let error = FrankenError::Busy;
2409 Self::restore_concurrent_page_state(
2410 ctx,
2411 &prior_page_state,
2412 &format!("{error}; MVCC state restore failed"),
2413 )?;
2414 tracing::warn!(
2415 txn_id,
2416 commit_seq = snapshot_high,
2417 snapshot_high,
2418 page_id = page_no.get(),
2419 visibility_decision = "write_busy_timeout",
2420 conflict_reason = "page_lock_busy",
2421 retry_policy = "park_wake",
2422 write_tier = "tier1_first_touch",
2423 "mvcc write conflict exceeded busy timeout"
2424 );
2425 return Err(error);
2426 }
2427 Err(wait_error) => {
2428 Self::restore_concurrent_page_state(
2429 ctx,
2430 &prior_page_state,
2431 &format!("{wait_error}; MVCC state restore failed"),
2432 )?;
2433 return Err(wait_error);
2434 }
2435 }
2436 }
2437 Err(e) => {
2438 let error = FrankenError::Internal(format!("MVCC write_page failed: {e}"));
2439 Self::restore_concurrent_page_state(
2440 ctx,
2441 &prior_page_state,
2442 &format!("{error}; MVCC state restore failed"),
2443 )?;
2444 tracing::warn!(
2445 txn_id,
2446 commit_seq = snapshot_high,
2447 snapshot_high,
2448 page_id = page_no.get(),
2449 visibility_decision = "write_abort",
2450 conflict_reason = %e,
2451 write_tier = "tier1_first_touch",
2452 "mvcc write failed"
2453 );
2454 return Err(error);
2455 }
2456 }
2457 }
2458
2459 {
2460 let mut handle = ctx.handle.lock();
2461 if let Err(stage_error) = concurrent_stage_prepared_write_marker(&mut handle, page_no) {
2462 Self::restore_concurrent_page_state(
2463 ctx,
2464 &prior_page_state,
2465 &format!("MVCC write staging failed: {stage_error}; MVCC state restore failed"),
2466 )?;
2467 return Err(FrankenError::Internal(format!(
2468 "MVCC write staging failed: {stage_error}"
2469 )));
2470 }
2471 }
2472
2473 if let Err(write_error) = self
2474 .txn
2475 .borrow_mut()
2476 .write_page_data(cx, page_no, page_data_base)
2477 .await
2478 {
2479 Self::restore_concurrent_page_state(
2480 ctx,
2481 &prior_page_state,
2482 &format!("pager write_page failed: {write_error}; MVCC state restore failed"),
2483 )?;
2484 return Err(write_error);
2485 }
2486
2487 self.clear_stale_synthetic_pending_commit_surface(cx, "write_page_tier1")?;
2488 Ok(())
2489 }
2490
2491 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2496 async fn write_page_tier2_commit_surface_rare(
2497 &self,
2498 cx: &Cx,
2499 ctx: &ConcurrentContext,
2500 page_no: PageNumber,
2501 page_data_base: PageData,
2502 ) -> Result<()> {
2503 add_vdbe_counter(&FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL, 1);
2504 let page_one_tracking_required = if page_no == PageNumber::ONE {
2505 false
2506 } else {
2507 self.txn
2508 .borrow()
2509 .write_page_requires_page_one_conflict_tracking(page_no)?
2510 };
2511 let handle = ctx.handle.lock();
2512 let prior_page_state = Some(concurrent_page_state(&handle, page_no));
2513 let page_one_state =
2514 if page_one_tracking_required && !handle.tracks_write_conflict_page(PageNumber::ONE) {
2515 Some(concurrent_page_state(&handle, PageNumber::ONE))
2516 } else {
2517 None
2518 };
2519 drop(handle);
2520
2521 let restore_concurrent_state = || -> std::result::Result<(), String> {
2522 let mut handle = ctx.handle.lock();
2523 if let Some(prior_page_state) = prior_page_state.as_ref()
2524 && let Err(restore_error) = concurrent_restore_page_state(
2525 &mut handle,
2526 &ctx.lock_table,
2527 ctx.session_id,
2528 prior_page_state,
2529 )
2530 {
2531 return Err(format!("MVCC state restore failed: {restore_error}"));
2532 }
2533 if let Some(page_one_state) = page_one_state.as_ref()
2534 && let Err(restore_error) = concurrent_restore_page_state(
2535 &mut handle,
2536 &ctx.lock_table,
2537 ctx.session_id,
2538 page_one_state,
2539 )
2540 {
2541 return Err(format!("MVCC page1 restore failed: {restore_error}"));
2542 }
2543 Ok(())
2544 };
2545
2546 if page_one_state.is_some() {
2547 track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "write_page")?;
2548 }
2549 let started = Instant::now();
2550 let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2551
2552 loop {
2553 observe_execution_cancellation(cx)?;
2554 let (write_result, conflicting_commit_seq) = {
2555 let mut handle = ctx.handle.lock();
2556 let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2557 .then(|| ctx.commit_index.latest(page_no))
2558 .flatten()
2559 .filter(|seq| *seq > ctx.snapshot_high);
2560 let write_result = conflicting_commit_seq.is_none().then(|| {
2561 fire_concurrent_page_lock_window_hook();
2562 concurrent_prepare_write_page(
2563 &mut handle,
2564 &ctx.lock_table,
2565 ctx.session_id,
2566 page_no,
2567 )
2568 });
2569 (write_result, conflicting_commit_seq)
2570 };
2571 let txn_id = ctx.txn_id;
2572 let snapshot_high = ctx.snapshot_high.get();
2573
2574 if let Some(conflicting_commit_seq) = conflicting_commit_seq {
2575 add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2576 let error = FrankenError::BusySnapshot {
2577 conflicting_pages: page_no.get().to_string(),
2578 };
2579 if let Err(restore_error) = restore_concurrent_state() {
2580 return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2581 }
2582 tracing::warn!(
2583 txn_id,
2584 commit_seq = conflicting_commit_seq.get(),
2585 snapshot_high,
2586 page_id = page_no.get(),
2587 visibility_decision = "write_snapshot_stale",
2588 conflict_reason = "fcw_base_drift",
2589 write_tier = "tier2_commit_surface_rare",
2590 "mvcc write rejected due to stale snapshot"
2591 );
2592 return Err(error);
2593 }
2594
2595 match write_result.ok_or_else(|| {
2596 FrankenError::Internal("write result must exist when snapshot is valid".to_owned())
2597 })? {
2598 Ok(()) => {
2599 match Self::post_acquire_fcw_conflict(ctx, page_no) {
2600 Ok(None) => {}
2601 Ok(Some(conflicting_commit_seq)) => {
2602 add_vdbe_counter(&FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL, 1);
2603 let error = FrankenError::BusySnapshot {
2604 conflicting_pages: page_no.get().to_string(),
2605 };
2606 if let Err(restore_error) = restore_concurrent_state() {
2607 return Err(FrankenError::Internal(format!(
2608 "{error}; {restore_error}"
2609 )));
2610 }
2611 tracing::warn!(
2612 txn_id,
2613 commit_seq = conflicting_commit_seq.get(),
2614 snapshot_high,
2615 page_id = page_no.get(),
2616 visibility_decision = "write_post_acquire_snapshot_stale",
2617 conflict_reason = "fcw_base_drift",
2618 write_tier = "tier2_commit_surface_rare",
2619 "mvcc write rejected after page-lock acquisition"
2620 );
2621 return Err(error);
2622 }
2623 Err(error) => {
2624 if let Err(restore_error) = restore_concurrent_state() {
2625 return Err(FrankenError::Internal(format!(
2626 "{error}; {restore_error}"
2627 )));
2628 }
2629 return Err(error);
2630 }
2631 }
2632 tracing::debug!(
2633 txn_id,
2634 commit_seq = snapshot_high,
2635 snapshot_high,
2636 page_id = page_no.get(),
2637 visibility_decision = "write_lock_acquired",
2638 conflict_reason = "none",
2639 write_tier = "tier2_commit_surface_rare",
2640 "mvcc write visibility decision"
2641 );
2642 break;
2643 }
2644 Err(MvccError::Busy) => {
2645 let remaining = deadline.saturating_sub(started.elapsed());
2646 match wait_for_page_lock_holder_change(cx, ctx, page_no, remaining) {
2647 Ok(true) => {
2648 add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL, 1);
2649 tracing::warn!(
2650 txn_id,
2651 commit_seq = snapshot_high,
2652 snapshot_high,
2653 page_id = page_no.get(),
2654 visibility_decision = "write_retry",
2655 conflict_reason = "page_lock_busy",
2656 retry_policy = "park_wake",
2657 retry_wait_ms = remaining.as_millis(),
2658 write_tier = "tier2_commit_surface_rare",
2659 "mvcc write conflict detected"
2660 );
2661 }
2662 Ok(false) => {
2663 add_vdbe_counter(&FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL, 1);
2664 let error = FrankenError::Busy;
2665 if let Err(restore_error) = restore_concurrent_state() {
2666 return Err(FrankenError::Internal(format!(
2667 "{error}; {restore_error}"
2668 )));
2669 }
2670 tracing::warn!(
2671 txn_id,
2672 commit_seq = snapshot_high,
2673 snapshot_high,
2674 page_id = page_no.get(),
2675 visibility_decision = "write_busy_timeout",
2676 conflict_reason = "page_lock_busy",
2677 retry_policy = "park_wake",
2678 write_tier = "tier2_commit_surface_rare",
2679 "mvcc write conflict exceeded busy timeout"
2680 );
2681 return Err(error);
2682 }
2683 Err(wait_error) => {
2684 if let Err(restore_error) = restore_concurrent_state() {
2685 return Err(FrankenError::Internal(format!(
2686 "{wait_error}; {restore_error}"
2687 )));
2688 }
2689 return Err(wait_error);
2690 }
2691 }
2692 }
2693 Err(e) => {
2694 let error = FrankenError::Internal(format!("MVCC write_page failed: {e}"));
2695 if let Err(restore_error) = restore_concurrent_state() {
2696 return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2697 }
2698 tracing::warn!(
2699 txn_id,
2700 commit_seq = snapshot_high,
2701 snapshot_high,
2702 page_id = page_no.get(),
2703 visibility_decision = "write_abort",
2704 conflict_reason = %e,
2705 write_tier = "tier2_commit_surface_rare",
2706 "mvcc write failed"
2707 );
2708 return Err(error);
2709 }
2710 }
2711 }
2712
2713 let stage_result = {
2714 let mut handle = ctx.handle.lock();
2715 concurrent_stage_prepared_write_marker(&mut handle, page_no)
2716 };
2717 if let Err(stage_error) = stage_result {
2718 let error = FrankenError::Internal(format!("MVCC write staging failed: {stage_error}"));
2719 if let Err(restore_error) = restore_concurrent_state() {
2720 return Err(FrankenError::Internal(format!("{error}; {restore_error}")));
2721 }
2722 return Err(error);
2723 }
2724
2725 if let Err(write_error) = self
2726 .txn
2727 .borrow_mut()
2728 .write_page_data(cx, page_no, page_data_base)
2729 .await
2730 {
2731 let mut handle = ctx.handle.lock();
2732 if let Some(prior_page_state) = prior_page_state.as_ref()
2733 && let Err(restore_error) = concurrent_restore_page_state(
2734 &mut handle,
2735 &ctx.lock_table,
2736 ctx.session_id,
2737 prior_page_state,
2738 )
2739 {
2740 return Err(FrankenError::Internal(format!(
2741 "pager write_page failed: {write_error}; MVCC state restore failed: {restore_error}"
2742 )));
2743 }
2744 if let Some(page_one_state) = page_one_state.as_ref()
2745 && let Err(restore_error) = concurrent_restore_page_state(
2746 &mut handle,
2747 &ctx.lock_table,
2748 ctx.session_id,
2749 page_one_state,
2750 )
2751 {
2752 return Err(FrankenError::Internal(format!(
2753 "pager write_page failed: {write_error}; MVCC page1 restore failed: {restore_error}"
2754 )));
2755 }
2756 return Err(write_error);
2757 }
2758
2759 self.clear_stale_synthetic_pending_commit_surface(cx, "write_page")?;
2760 Ok(())
2761 }
2762
2763 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
2768 async fn write_page_internal(
2769 &self,
2770 cx: &Cx,
2771 page_no: PageNumber,
2772 page_data_base: PageData,
2773 ) -> Result<()> {
2774 let Some(ctx) = self.concurrent_context() else {
2775 return self
2776 .txn
2777 .borrow_mut()
2778 .write_page_data(cx, page_no, page_data_base)
2779 .await;
2780 };
2781
2782 match Self::classify_concurrent_write_tier(&ctx, page_no) {
2783 ConcurrentWriteTier::Tier0AlreadyOwned => {
2784 self.write_page_tier0_already_owned(cx, &ctx, page_no, page_data_base)
2785 .await
2786 }
2787 ConcurrentWriteTier::Tier1FirstTouch => {
2788 self.write_page_tier1_first_touch(cx, &ctx, page_no, page_data_base)
2789 .await
2790 }
2791 ConcurrentWriteTier::Tier2CommitSurfaceRare => {
2792 self.write_page_tier2_commit_surface_rare(cx, &ctx, page_no, page_data_base)
2793 .await
2794 }
2795 }
2796 }
2797}
2798
2799const PAGE_LOCK_WAIT_CANCELLATION_POLL: Duration = Duration::from_millis(5);
2800const PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL: Duration = Duration::from_millis(50);
2801
2802fn normalize_owned_page_data(page_size: usize, data: &[u8]) -> Result<PageData> {
2803 let metrics_enabled = vdbe_metrics_enabled();
2804 add_vdbe_counter_if(
2805 metrics_enabled,
2806 &FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL,
2807 1,
2808 );
2809 if data.len() == page_size {
2810 add_vdbe_counter_if(
2811 metrics_enabled,
2812 &FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL,
2813 1,
2814 );
2815 add_vdbe_counter_if(
2816 metrics_enabled,
2817 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2818 u64::try_from(data.len()).unwrap_or(u64::MAX),
2819 );
2820 return Ok(PageData::from_vec(data.to_vec()));
2821 }
2822 if data.len() > page_size {
2823 return Err(FrankenError::Internal(format!(
2824 "page buffer exceeds page size invariant: {} > {}",
2825 data.len(),
2826 page_size
2827 )));
2828 }
2829
2830 let mut page = vec![0_u8; page_size];
2831 page[..data.len()].copy_from_slice(data);
2832 add_vdbe_counter_if(
2833 metrics_enabled,
2834 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2835 u64::try_from(data.len()).unwrap_or(u64::MAX),
2836 );
2837 add_vdbe_counter_if(
2838 metrics_enabled,
2839 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2840 u64::try_from(page_size.saturating_sub(data.len())).unwrap_or(u64::MAX),
2841 );
2842 Ok(PageData::from_vec(page))
2843}
2844
2845fn normalize_page_data_to_size(page_size: usize, data: PageData) -> Result<PageData> {
2846 let metrics_enabled = vdbe_metrics_enabled();
2847 add_vdbe_counter_if(
2848 metrics_enabled,
2849 &FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL,
2850 1,
2851 );
2852 if data.len() == page_size {
2853 add_vdbe_counter_if(
2854 metrics_enabled,
2855 &FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL,
2856 1,
2857 );
2858 return Ok(data);
2859 }
2860 if data.len() > page_size {
2861 return Err(FrankenError::Internal(format!(
2862 "page buffer exceeds page size invariant: {} > {}",
2863 data.len(),
2864 page_size
2865 )));
2866 }
2867
2868 let payload_len = data.len();
2869 let zero_fill_len = page_size.saturating_sub(payload_len);
2870 let mut data = data;
2871 if data.try_zero_extend_owned_to(page_size) {
2872 add_vdbe_counter_if(
2873 metrics_enabled,
2874 &FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL,
2875 1,
2876 );
2877 add_vdbe_counter_if(
2878 metrics_enabled,
2879 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2880 u64::try_from(zero_fill_len).unwrap_or(u64::MAX),
2881 );
2882 return Ok(data);
2883 }
2884
2885 let mut page = vec![0_u8; page_size];
2886 page[..payload_len].copy_from_slice(data.as_bytes());
2887 add_vdbe_counter_if(
2888 metrics_enabled,
2889 &FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL,
2890 1,
2891 );
2892 add_vdbe_counter_if(
2893 metrics_enabled,
2894 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL,
2895 u64::try_from(payload_len).unwrap_or(u64::MAX),
2896 );
2897 add_vdbe_counter_if(
2898 metrics_enabled,
2899 &FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL,
2900 u64::try_from(zero_fill_len).unwrap_or(u64::MAX),
2901 );
2902 Ok(PageData::from_vec(page))
2903}
2904
2905fn wait_for_page_lock_holder_change(
2906 cx: &Cx,
2907 ctx: &ConcurrentContext,
2908 page_no: PageNumber,
2909 remaining: Duration,
2910) -> Result<bool> {
2911 observe_execution_cancellation(cx)?;
2914
2915 let Some(holder) = ctx.lock_table.holder(page_no) else {
2916 return Ok(true);
2917 };
2918 if remaining.is_zero() {
2919 return Ok(false);
2920 }
2921
2922 let metrics_enabled = vdbe_metrics_enabled();
2923 let started = Instant::now();
2924 let mut next_full_checkpoint = PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL;
2925 loop {
2926 let mut elapsed = started.elapsed();
2927 if elapsed >= next_full_checkpoint {
2933 observe_execution_cancellation(cx)?;
2934 elapsed = started.elapsed();
2935 next_full_checkpoint = elapsed.saturating_add(PAGE_LOCK_WAIT_FULL_CHECKPOINT_POLL);
2936 } else if cx.is_cancel_requested() {
2937 return Err(FrankenError::Abort);
2938 }
2939
2940 let wait_budget = remaining.saturating_sub(elapsed);
2941 if wait_budget.is_zero() {
2942 return Ok(false);
2943 }
2944
2945 let wait_slice = wait_budget.min(PAGE_LOCK_WAIT_CANCELLATION_POLL);
2946 if ctx
2947 .lock_table
2948 .wait_for_holder_change(page_no, holder, wait_slice)
2949 {
2950 add_vdbe_counter_if(metrics_enabled, &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL, 1);
2951 add_vdbe_duration_if(
2952 metrics_enabled,
2953 &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL,
2954 started,
2955 );
2956 return Ok(true);
2957 }
2958
2959 if wait_budget == wait_slice {
2960 add_vdbe_counter_if(metrics_enabled, &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL, 1);
2961 add_vdbe_duration_if(
2962 metrics_enabled,
2963 &FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL,
2964 started,
2965 );
2966 return Ok(false);
2967 }
2968 }
2969}
2970
2971fn track_concurrent_conflict_only_page(
2972 cx: &Cx,
2973 ctx: &ConcurrentContext,
2974 page_no: PageNumber,
2975 operation: &str,
2976) -> Result<()> {
2977 let metrics_enabled = vdbe_metrics_enabled();
2978 let track_started = metrics_enabled.then(Instant::now);
2979 let started = Instant::now();
2980 let deadline = Duration::from_millis(ctx.busy_timeout_ms);
2981 let prior_page_state = {
2982 let handle = ctx.handle.lock();
2983 concurrent_page_state(&handle, page_no)
2984 };
2985
2986 loop {
2987 observe_execution_cancellation(cx)?;
2988 let (track_result, already_tracked, conflicting_commit_seq) = {
2989 let mut handle = ctx.handle.lock();
2990 let already_tracked = handle.tracks_write_conflict_page(page_no);
2991 let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
2992 .then(|| ctx.commit_index.latest(page_no))
2993 .flatten()
2994 .filter(|seq| *seq > ctx.snapshot_high);
2995 let track_result = conflicting_commit_seq.is_none().then(|| {
2996 fire_concurrent_page_lock_window_hook();
2997 concurrent_track_write_conflict_page(
2998 &mut handle,
2999 &ctx.lock_table,
3000 ctx.session_id,
3001 page_no,
3002 )
3003 });
3004 (track_result, already_tracked, conflicting_commit_seq)
3005 };
3006 let txn_id = ctx.txn_id;
3007 let snapshot_high = ctx.snapshot_high.get();
3008
3009 if already_tracked {
3010 tracing::debug!(
3011 txn_id,
3012 commit_seq = snapshot_high,
3013 snapshot_high,
3014 page_id = page_no.get(),
3015 visibility_decision = "conflict_only_already_tracked",
3016 conflict_reason = "none",
3017 operation,
3018 "mvcc conflict-only page already tracked"
3019 );
3020 return Ok(());
3021 }
3022
3023 if let Some(conflicting_commit_seq) = conflicting_commit_seq {
3024 tracing::warn!(
3025 txn_id,
3026 commit_seq = conflicting_commit_seq.get(),
3027 snapshot_high,
3028 page_id = page_no.get(),
3029 visibility_decision = "conflict_only_snapshot_stale",
3030 conflict_reason = "fcw_base_drift",
3031 operation,
3032 "mvcc conflict-only page rejected due to stale snapshot"
3033 );
3034 return Err(FrankenError::BusySnapshot {
3035 conflicting_pages: page_no.get().to_string(),
3036 });
3037 }
3038
3039 match track_result.ok_or_else(|| {
3040 FrankenError::Internal("track result must exist when snapshot is valid".to_owned())
3041 })? {
3042 Ok(()) => {
3043 match SharedTxnPageIo::post_acquire_fcw_conflict(ctx, page_no) {
3044 Ok(None) => {}
3045 Ok(Some(conflicting_commit_seq)) => {
3046 let error = FrankenError::BusySnapshot {
3047 conflicting_pages: page_no.get().to_string(),
3048 };
3049 SharedTxnPageIo::restore_concurrent_page_state(
3050 ctx,
3051 &prior_page_state,
3052 &format!("{error}; MVCC conflict-only state restore failed"),
3053 )?;
3054 tracing::warn!(
3055 txn_id,
3056 commit_seq = conflicting_commit_seq.get(),
3057 snapshot_high,
3058 page_id = page_no.get(),
3059 visibility_decision = "conflict_only_post_acquire_snapshot_stale",
3060 conflict_reason = "fcw_base_drift",
3061 operation,
3062 "mvcc conflict-only page rejected after lock acquisition"
3063 );
3064 return Err(error);
3065 }
3066 Err(error) => {
3067 SharedTxnPageIo::restore_concurrent_page_state(
3068 ctx,
3069 &prior_page_state,
3070 &format!("{error}; MVCC conflict-only state restore failed"),
3071 )?;
3072 return Err(error);
3073 }
3074 }
3075 add_vdbe_counter_if(
3076 metrics_enabled,
3077 &FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL,
3078 1,
3079 );
3080 if let Some(track_started) = track_started {
3081 add_vdbe_duration_if(
3082 metrics_enabled,
3083 &FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL,
3084 track_started,
3085 );
3086 }
3087 tracing::debug!(
3088 txn_id,
3089 commit_seq = snapshot_high,
3090 snapshot_high,
3091 page_id = page_no.get(),
3092 visibility_decision = "conflict_only_tracked",
3093 conflict_reason = "none",
3094 operation,
3095 "mvcc conflict-only page tracked"
3096 );
3097 return Ok(());
3098 }
3099 Err(MvccError::Busy) => {
3100 let remaining = deadline.saturating_sub(started.elapsed());
3101 if !wait_for_page_lock_holder_change(cx, ctx, page_no, remaining)? {
3102 tracing::warn!(
3103 txn_id,
3104 commit_seq = snapshot_high,
3105 snapshot_high,
3106 page_id = page_no.get(),
3107 visibility_decision = "conflict_only_busy_timeout",
3108 conflict_reason = "page_lock_busy",
3109 operation,
3110 retry_policy = "park_wake",
3111 "mvcc conflict-only page exceeded busy timeout"
3112 );
3113 return Err(FrankenError::Busy);
3114 }
3115
3116 tracing::warn!(
3117 txn_id,
3118 commit_seq = snapshot_high,
3119 snapshot_high,
3120 page_id = page_no.get(),
3121 visibility_decision = "conflict_only_retry",
3122 conflict_reason = "page_lock_busy",
3123 operation,
3124 retry_policy = "park_wake",
3125 retry_wait_ms = remaining.as_millis(),
3126 "mvcc conflict-only page contention detected"
3127 );
3128 }
3129 Err(e) => {
3130 tracing::warn!(
3131 txn_id,
3132 commit_seq = snapshot_high,
3133 snapshot_high,
3134 page_id = page_no.get(),
3135 visibility_decision = "conflict_only_abort",
3136 conflict_reason = %e,
3137 operation,
3138 "mvcc conflict-only page tracking failed"
3139 );
3140 return Err(FrankenError::Internal(format!(
3141 "MVCC conflict-only page tracking failed: {e}"
3142 )));
3143 }
3144 }
3145 }
3146}
3147
3148impl PageReader for SharedTxnPageIo {
3149 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3154 async fn read_page(&self, cx: &Cx, page_no: PageNumber) -> Result<Vec<u8>> {
3155 if let Some(ctx) = self.concurrent_context() {
3156 let txn_id = ctx.txn_id;
3160 let snapshot_high = ctx.snapshot_high.get();
3161 let (handle, has_staged_write) = {
3162 let mut handle = ctx.handle.lock();
3163 let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3166 concurrent_page_read_status(&handle, page_no)
3167 } else {
3168 (false, false)
3169 };
3170 if is_freed {
3171 return Err(FrankenError::DatabaseCorrupt {
3172 detail: format!(
3173 "page {} was freed earlier in concurrent transaction {}",
3174 page_no.get(),
3175 txn_id
3176 ),
3177 });
3178 }
3179 handle.record_read(page_no);
3180 (handle, has_staged_write)
3181 };
3182 drop(handle);
3183
3184 if has_staged_write {
3185 let page = self.txn.borrow().get_page(cx, page_no).await?;
3186 tracing::debug!(
3187 txn_id,
3188 commit_seq = snapshot_high,
3189 snapshot_high,
3190 page_id = page_no.get(),
3191 visibility_decision = "write_set_hit",
3192 conflict_reason = "none",
3193 "mvcc visibility decision"
3194 );
3195 return Ok(page.into_vec());
3196 }
3197
3198 tracing::debug!(
3199 txn_id,
3200 commit_seq = snapshot_high,
3201 snapshot_high,
3202 page_id = page_no.get(),
3203 visibility_decision = "snapshot_pager_read",
3204 conflict_reason = "none",
3205 "mvcc visibility decision"
3206 );
3207 }
3208
3209 let page = self.txn.borrow().get_page(cx, page_no).await?.into_vec();
3210 Ok(page)
3211 }
3212
3213 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3220 async fn read_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3221 if let Some(ctx) = self.concurrent_context() {
3222 let has_staged_write = {
3223 let mut handle = ctx.handle.lock();
3224 let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3225 concurrent_page_read_status(&handle, page_no)
3226 } else {
3227 (false, false)
3228 };
3229 if is_freed {
3230 return Err(FrankenError::DatabaseCorrupt {
3231 detail: format!(
3232 "page {} was freed earlier in concurrent transaction {}",
3233 page_no.get(),
3234 ctx.txn_id
3235 ),
3236 });
3237 }
3238 handle.record_read(page_no);
3239 has_staged_write
3240 };
3241
3242 if has_staged_write {
3243 return self.txn.borrow().get_page(cx, page_no).await;
3244 }
3245 }
3246 self.txn.borrow().get_page(cx, page_no).await
3247 }
3248
3249 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3254 async fn read_btree_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3255 if let Some(ctx) = self.concurrent_context() {
3256 let has_staged_write = {
3257 let handle = ctx.handle.lock();
3258 let (is_freed, has_staged_write) = if concurrent_has_page_state(&handle) {
3259 concurrent_page_read_status(&handle, page_no)
3260 } else {
3261 (false, false)
3262 };
3263 if is_freed {
3264 return Err(FrankenError::DatabaseCorrupt {
3265 detail: format!(
3266 "page {} was freed earlier in concurrent transaction {}",
3267 page_no.get(),
3268 ctx.txn_id
3269 ),
3270 });
3271 }
3272 has_staged_write
3273 };
3274
3275 if has_staged_write {
3276 return self.txn.borrow().get_page(cx, page_no).await;
3277 }
3278 }
3279 self.txn.borrow().get_page(cx, page_no).await
3280 }
3281
3282 fn record_read_witness(&self, _cx: &Cx, key: WitnessKey) {
3283 if let Some(ctx) = self.concurrent_context() {
3284 ctx.handle.lock().record_read_witness(key);
3285 }
3286 }
3287
3288 fn is_dirty(&self, page_no: PageNumber) -> bool {
3289 if let Some(ctx) = self.concurrent_context() {
3290 return ctx.handle.lock().tracks_write_conflict_page(page_no);
3291 }
3292 false
3293 }
3294}
3295
3296impl PageWriter for SharedTxnPageIo {
3297 async fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()> {
3298 let page_size = self.txn.borrow().page_size().as_usize();
3299 let page_data = normalize_owned_page_data(page_size, data)?;
3300 self.write_page_internal(cx, page_no, page_data).await
3301 }
3302
3303 async fn write_page_data(
3304 &mut self,
3305 cx: &Cx,
3306 page_no: PageNumber,
3307 data: PageData,
3308 ) -> Result<()> {
3309 let page_size = self.txn.borrow().page_size().as_usize();
3310 let page_data = normalize_page_data_to_size(page_size, data)?;
3311 self.write_page_internal(cx, page_no, page_data).await
3312 }
3313
3314 fn try_mutate_staged_page_data(
3315 &mut self,
3316 page_no: PageNumber,
3317 f: &mut dyn FnMut(&mut PageData),
3318 ) -> bool {
3319 if let Some(ctx) = self.concurrent_context() {
3320 let has_staged_write = {
3321 let handle = ctx.handle.lock();
3322 let (is_freed, has_staged_write) = concurrent_page_read_status(&handle, page_no);
3323 !is_freed && has_staged_write
3324 };
3325 if !has_staged_write {
3326 return false;
3327 }
3328 }
3329 self.txn
3330 .borrow_mut()
3331 .try_mutate_staged_page_data(page_no, f)
3332 }
3333
3334 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3339 async fn allocate_page(&mut self, cx: &Cx) -> Result<PageNumber> {
3340 let concurrent = self.concurrent_context();
3341 let page_one_tracking_required = self
3342 .concurrent_context()
3343 .as_ref()
3344 .map(|_| {
3345 self.txn
3346 .borrow()
3347 .allocate_page_requires_page_one_conflict_tracking()
3348 })
3349 .transpose()?
3350 .unwrap_or(false);
3351 let page_one_state = concurrent.as_ref().and_then(|ctx| {
3352 let handle = ctx.handle.lock();
3353 if page_one_tracking_required {
3354 if !handle.tracks_write_conflict_page(PageNumber::ONE) {
3355 Some(concurrent_page_state(&handle, PageNumber::ONE))
3356 } else {
3357 None
3358 }
3359 } else {
3360 None
3361 }
3362 });
3363 if let Some(ctx) = concurrent.as_ref()
3364 && page_one_state.is_some()
3365 {
3366 track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "allocate_page")?;
3367 }
3368 let allocate_result = self.txn.borrow_mut().allocate_page(cx).await;
3369 if let Err(allocate_error) = &allocate_result {
3370 if let (Some(ctx), Some(page_one_state)) =
3371 (concurrent.as_ref(), page_one_state.as_ref())
3372 {
3373 let mut handle = ctx.handle.lock();
3374 if let Err(restore_error) = concurrent_restore_page_state(
3375 &mut handle,
3376 &ctx.lock_table,
3377 ctx.session_id,
3378 page_one_state,
3379 ) {
3380 return Err(FrankenError::Internal(format!(
3381 "pager allocate_page failed: {allocate_error}; MVCC state restore failed: {restore_error}"
3382 )));
3383 }
3384 }
3385 }
3386 let page_no = allocate_result?;
3387 self.clear_stale_synthetic_pending_commit_surface(cx, "allocate_page")?;
3388 Ok(page_no)
3389 }
3390
3391 #[allow(clippy::await_holding_refcell_ref, clippy::await_holding_lock)]
3396 async fn free_page(&mut self, cx: &Cx, page_no: PageNumber) -> Result<()> {
3397 let concurrent = self.concurrent_context();
3398 let page_one_tracking_required = self
3399 .concurrent_context()
3400 .as_ref()
3401 .map(|_| {
3402 self.txn
3403 .borrow()
3404 .free_page_requires_page_one_conflict_tracking(page_no)
3405 })
3406 .transpose()?
3407 .unwrap_or(false);
3408 let (prior_page_state, page_one_state) = if let Some(ctx) = concurrent.as_ref() {
3409 let handle = ctx.handle.lock();
3410 let prior_page_state = Some(concurrent_page_state(&handle, page_no));
3411 let page_one_state = if page_one_tracking_required {
3412 if !handle.tracks_write_conflict_page(PageNumber::ONE) {
3413 Some(concurrent_page_state(&handle, PageNumber::ONE))
3414 } else {
3415 None
3416 }
3417 } else {
3418 None
3419 };
3420 (prior_page_state, page_one_state)
3421 } else {
3422 (None, None)
3423 };
3424 if let Some(ctx) = concurrent.as_ref() {
3425 if page_one_state.is_some() {
3426 track_concurrent_conflict_only_page(cx, ctx, PageNumber::ONE, "free_page")?;
3427 }
3428 let concurrent_free_result = (|| -> Result<()> {
3429 let started = Instant::now();
3430 let deadline = Duration::from_millis(ctx.busy_timeout_ms);
3431
3432 loop {
3433 observe_execution_cancellation(cx)?;
3434 let (free_result, conflicting_commit_seq) = {
3435 let mut handle = ctx.handle.lock();
3436 let conflicting_commit_seq = (!handle.holds_page_lock(page_no))
3437 .then(|| ctx.commit_index.latest(page_no))
3438 .flatten()
3439 .filter(|seq| *seq > ctx.snapshot_high);
3440 let free_result = conflicting_commit_seq.is_none().then(|| {
3441 fire_concurrent_page_lock_window_hook();
3442 concurrent_free_page(
3443 &mut handle,
3444 &ctx.lock_table,
3445 ctx.session_id,
3446 page_no,
3447 )
3448 });
3449 (free_result, conflicting_commit_seq)
3450 };
3451 let txn_id = ctx.txn_id;
3452 let snapshot_high = ctx.snapshot_high.get();
3453
3454 if let Some(conflicting_commit_seq) = conflicting_commit_seq {
3455 tracing::warn!(
3456 txn_id,
3457 commit_seq = conflicting_commit_seq.get(),
3458 snapshot_high,
3459 page_id = page_no.get(),
3460 visibility_decision = "free_snapshot_stale",
3461 conflict_reason = "fcw_base_drift",
3462 "mvcc free rejected due to stale snapshot"
3463 );
3464 return Err(FrankenError::BusySnapshot {
3465 conflicting_pages: page_no.get().to_string(),
3466 });
3467 }
3468
3469 match free_result.ok_or_else(|| {
3470 FrankenError::Internal(
3471 "free result must exist when snapshot is valid".to_owned(),
3472 )
3473 })? {
3474 Ok(()) => {
3475 if let Some(conflicting_commit_seq) =
3476 Self::post_acquire_fcw_conflict(ctx, page_no)?
3477 {
3478 tracing::warn!(
3479 txn_id,
3480 commit_seq = conflicting_commit_seq.get(),
3481 snapshot_high,
3482 page_id = page_no.get(),
3483 visibility_decision = "free_post_acquire_snapshot_stale",
3484 conflict_reason = "fcw_base_drift",
3485 "mvcc free rejected after page-lock acquisition"
3486 );
3487 return Err(FrankenError::BusySnapshot {
3488 conflicting_pages: page_no.get().to_string(),
3489 });
3490 }
3491 tracing::debug!(
3492 txn_id,
3493 commit_seq = snapshot_high,
3494 snapshot_high,
3495 page_id = page_no.get(),
3496 visibility_decision = "free_set_recorded",
3497 conflict_reason = "none",
3498 "mvcc free visibility decision"
3499 );
3500 return Ok(());
3501 }
3502 Err(MvccError::Busy) => {
3503 let remaining = deadline.saturating_sub(started.elapsed());
3504 if !wait_for_page_lock_holder_change(cx, ctx, page_no, remaining)? {
3505 tracing::warn!(
3506 txn_id,
3507 commit_seq = snapshot_high,
3508 snapshot_high,
3509 page_id = page_no.get(),
3510 visibility_decision = "free_busy_timeout",
3511 conflict_reason = "page_lock_busy",
3512 retry_policy = "park_wake",
3513 "mvcc free conflict exceeded busy timeout"
3514 );
3515 return Err(FrankenError::Busy);
3516 }
3517
3518 tracing::warn!(
3519 txn_id,
3520 commit_seq = snapshot_high,
3521 snapshot_high,
3522 page_id = page_no.get(),
3523 visibility_decision = "free_retry",
3524 conflict_reason = "page_lock_busy",
3525 retry_policy = "park_wake",
3526 retry_wait_ms = remaining.as_millis(),
3527 "mvcc free conflict detected"
3528 );
3529 }
3530 Err(e) => {
3531 tracing::warn!(
3532 txn_id,
3533 commit_seq = snapshot_high,
3534 snapshot_high,
3535 page_id = page_no.get(),
3536 visibility_decision = "free_abort",
3537 conflict_reason = %e,
3538 "mvcc free failed"
3539 );
3540 return Err(FrankenError::Internal(format!(
3541 "MVCC free_page failed: {e}"
3542 )));
3543 }
3544 }
3545 }
3546 })();
3547 if let Err(error) = concurrent_free_result {
3548 let mut handle = ctx.handle.lock();
3549 if let Some(prior_page_state) = prior_page_state.as_ref()
3550 && let Err(restore_error) = concurrent_restore_page_state(
3551 &mut handle,
3552 &ctx.lock_table,
3553 ctx.session_id,
3554 prior_page_state,
3555 )
3556 {
3557 return Err(FrankenError::Internal(format!(
3558 "{error}; MVCC page restore failed: {restore_error}"
3559 )));
3560 }
3561 if let Some(page_one_state) = page_one_state.as_ref()
3562 && let Err(restore_error) = concurrent_restore_page_state(
3563 &mut handle,
3564 &ctx.lock_table,
3565 ctx.session_id,
3566 page_one_state,
3567 )
3568 {
3569 return Err(FrankenError::Internal(format!(
3570 "{error}; MVCC page1 restore failed: {restore_error}"
3571 )));
3572 }
3573 return Err(error);
3574 }
3575 }
3576 let free_result = self.txn.borrow_mut().free_page(cx, page_no).await;
3577 if let Err(free_error) = free_result {
3578 if let (Some(ctx), Some(prior_page_state)) =
3579 (concurrent.as_ref(), prior_page_state.as_ref())
3580 {
3581 let mut handle = ctx.handle.lock();
3582 if let Err(restore_error) = concurrent_restore_page_state(
3583 &mut handle,
3584 &ctx.lock_table,
3585 ctx.session_id,
3586 prior_page_state,
3587 ) {
3588 return Err(FrankenError::Internal(format!(
3589 "pager free_page failed: {free_error}; MVCC state restore failed: {restore_error}"
3590 )));
3591 }
3592 if let Some(page_one_state) = page_one_state.as_ref() {
3593 if let Err(restore_error) = concurrent_restore_page_state(
3594 &mut handle,
3595 &ctx.lock_table,
3596 ctx.session_id,
3597 page_one_state,
3598 ) {
3599 return Err(FrankenError::Internal(format!(
3600 "pager free_page failed: {free_error}; MVCC page1 restore failed: {restore_error}"
3601 )));
3602 }
3603 }
3604 }
3605 return Err(free_error);
3606 }
3607 self.clear_stale_synthetic_pending_commit_surface(cx, "free_page")?;
3608 Ok(())
3609 }
3610
3611 fn record_write_witness(&mut self, cx: &Cx, key: WitnessKey) {
3612 if let Some(ctx) = self.concurrent_context() {
3613 ctx.handle.lock().record_write_witness(key);
3614 return;
3615 }
3616 self.txn.borrow_mut().record_write_witness(cx, key);
3617 }
3618}
3619
3620#[derive(Clone)]
3640struct TimeTravelPageIo {
3641 inner: SharedTxnPageIo,
3643 version_store: Arc<VersionStore>,
3645 snapshot: TimeTravelSnapshot,
3647}
3648
3649impl std::fmt::Debug for TimeTravelPageIo {
3650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3651 f.debug_struct("TimeTravelPageIo")
3652 .field("inner", &self.inner)
3653 .field("version_store", &"<Arc<VersionStore>>")
3654 .field(
3655 "target_commit_seq",
3656 &self.snapshot.target_commit_seq().get(),
3657 )
3658 .finish()
3659 }
3660}
3661
3662impl PageReader for TimeTravelPageIo {
3663 async fn read_page(&self, cx: &Cx, page_no: PageNumber) -> Result<Vec<u8>> {
3664 let vs = &self.version_store;
3666 if let Some(idx) = self.snapshot.resolve_page(vs, page_no) {
3667 if let Some(version) = vs.get_version(idx) {
3668 tracing::trace!(
3669 page_id = page_no.get(),
3670 commit_seq = self.snapshot.target_commit_seq().get(),
3671 "time-travel: serving historical page from version store"
3672 );
3673 return Ok(version.data.into_vec());
3674 }
3675 }
3676
3677 let store_has_versions = vs.page_count() > 0;
3691
3692 if !store_has_versions {
3693 tracing::warn!(
3694 page_id = page_no.get(),
3695 commit_seq = self.snapshot.target_commit_seq().get(),
3696 "time-travel: VersionStore is empty — cannot serve historical \
3697 page; historical data not available for this commit"
3698 );
3699 return Err(FrankenError::Internal(format!(
3700 "time-travel query failed: historical data not available for \
3701 commit_seq={} (MVCC version store has no historical page \
3702 versions; the database may be running in compatibility mode \
3703 where time-travel is not yet supported)",
3704 self.snapshot.target_commit_seq().get(),
3705 )));
3706 }
3707
3708 tracing::trace!(
3712 page_id = page_no.get(),
3713 commit_seq = self.snapshot.target_commit_seq().get(),
3714 "time-travel: page not in version store (unchanged since target \
3715 commit), falling through to txn"
3716 );
3717 self.inner.read_page(cx, page_no).await
3718 }
3719
3720 async fn read_btree_page_data(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
3721 let vs = &self.version_store;
3722 if let Some(idx) = self.snapshot.resolve_page(vs, page_no)
3723 && let Some(version) = vs.get_version(idx)
3724 {
3725 tracing::trace!(
3726 page_id = page_no.get(),
3727 commit_seq = self.snapshot.target_commit_seq().get(),
3728 "time-travel: serving historical B-tree page from version store"
3729 );
3730 return Ok(version.data);
3731 }
3732
3733 if vs.page_count() == 0 {
3734 tracing::warn!(
3735 page_id = page_no.get(),
3736 commit_seq = self.snapshot.target_commit_seq().get(),
3737 "time-travel: VersionStore is empty — cannot serve historical \
3738 B-tree page; historical data not available for this commit"
3739 );
3740 return Err(FrankenError::Internal(format!(
3741 "time-travel query failed: historical data not available for \
3742 commit_seq={} (MVCC version store has no historical page \
3743 versions; the database may be running in compatibility mode \
3744 where time-travel is not yet supported)",
3745 self.snapshot.target_commit_seq().get(),
3746 )));
3747 }
3748
3749 self.inner.read_btree_page_data(cx, page_no).await
3750 }
3751}
3752
3753impl PageWriter for TimeTravelPageIo {
3756 fn write_page(
3757 &mut self,
3758 _cx: &Cx,
3759 _page_no: PageNumber,
3760 _data: &[u8],
3761 ) -> impl std::future::Future<Output = Result<()>> {
3762 std::future::ready(Err(FrankenError::Internal(
3763 "time-travel cursors are read-only: write_page not permitted".to_owned(),
3764 )))
3765 }
3766
3767 fn allocate_page(&mut self, _cx: &Cx) -> impl std::future::Future<Output = Result<PageNumber>> {
3768 std::future::ready(Err(FrankenError::Internal(
3769 "time-travel cursors are read-only: allocate_page not permitted".to_owned(),
3770 )))
3771 }
3772
3773 fn free_page(
3774 &mut self,
3775 _cx: &Cx,
3776 _page_no: PageNumber,
3777 ) -> impl std::future::Future<Output = Result<()>> {
3778 std::future::ready(Err(FrankenError::Internal(
3779 "time-travel cursors are read-only: free_page not permitted".to_owned(),
3780 )))
3781 }
3782
3783 fn record_write_witness(&mut self, _cx: &Cx, _key: WitnessKey) {}
3784}
3785
3786enum CursorBackend {
3796 Mem(BtCursor<MemPageStore>),
3798 Txn(BtCursor<SharedTxnPageIo>),
3800 TimeTravel(BtCursor<TimeTravelPageIo>),
3802}
3803
3804impl std::fmt::Debug for CursorBackend {
3805 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3806 match self {
3807 Self::Mem(c) => f.debug_tuple("Mem").field(c).finish(),
3808 Self::Txn(c) => f.debug_tuple("Txn").field(c).finish(),
3809 Self::TimeTravel(c) => f.debug_tuple("TimeTravel").field(c).finish(),
3810 }
3811 }
3812}
3813
3814impl CursorBackend {
3815 #[must_use]
3817 fn is_mem(&self) -> bool {
3818 matches!(self, Self::Mem(_))
3819 }
3820
3821 #[must_use]
3823 fn is_txn(&self) -> bool {
3824 matches!(self, Self::Txn(_))
3825 }
3826
3827 #[must_use]
3829 #[allow(dead_code)]
3830 fn is_time_travel(&self) -> bool {
3831 matches!(self, Self::TimeTravel(_))
3832 }
3833
3834 #[must_use]
3836 #[allow(dead_code)]
3837 fn kind_str(&self) -> &'static str {
3838 match self {
3839 Self::Mem(_) => "mem",
3840 Self::Txn(_) => "txn",
3841 Self::TimeTravel(_) => "time_travel",
3842 }
3843 }
3844
3845 #[must_use]
3847 fn is_table_btree(&self) -> bool {
3848 match self {
3849 Self::Mem(c) => c.is_table(),
3850 Self::Txn(c) => c.is_table(),
3851 Self::TimeTravel(c) => c.is_table(),
3852 }
3853 }
3854
3855 #[must_use]
3856 fn usable_size(&self) -> u32 {
3857 match self {
3858 Self::Mem(c) => c.usable_size(),
3859 Self::Txn(c) => c.usable_size(),
3860 Self::TimeTravel(c) => c.usable_size(),
3861 }
3862 }
3863
3864 #[must_use]
3865 fn page_size(&self) -> u32 {
3866 match self {
3867 Self::Mem(c) => c.page_size(),
3868 Self::Txn(c) => c.page_size(),
3869 Self::TimeTravel(c) => c.page_size(),
3870 }
3871 }
3872
3873 #[must_use]
3874 fn index_desc_flags(&self) -> &[bool] {
3875 match self {
3876 Self::Mem(c) => c.index_desc_flags(),
3877 Self::Txn(c) => c.index_desc_flags(),
3878 Self::TimeTravel(c) => c.index_desc_flags(),
3879 }
3880 }
3881
3882 #[must_use]
3883 fn index_collations(&self) -> &[Option<String>] {
3884 match self {
3885 Self::Mem(c) => c.index_collations(),
3886 Self::Txn(c) => c.index_collations(),
3887 Self::TimeTravel(c) => c.index_collations(),
3888 }
3889 }
3890
3891 #[must_use]
3892 fn collation_registry(&self) -> Arc<Mutex<CollationRegistry>> {
3893 match self {
3894 Self::Mem(c) => c.collation_registry(),
3895 Self::Txn(c) => c.collation_registry(),
3896 Self::TimeTravel(c) => c.collation_registry(),
3897 }
3898 }
3899}
3900
3901impl CursorBackend {
3903 async fn first(&mut self, cx: &Cx) -> Result<bool> {
3904 match self {
3905 Self::Mem(c) => c.first(cx).await,
3906 Self::Txn(c) => c.first(cx).await,
3907 Self::TimeTravel(c) => c.first(cx).await,
3908 }
3909 }
3910
3911 async fn last(&mut self, cx: &Cx) -> Result<bool> {
3912 match self {
3913 Self::Mem(c) => c.last(cx).await,
3914 Self::Txn(c) => c.last(cx).await,
3915 Self::TimeTravel(c) => c.last(cx).await,
3916 }
3917 }
3918
3919 async fn next(&mut self, cx: &Cx) -> Result<bool> {
3920 match self {
3921 Self::Mem(c) => c.next(cx).await,
3922 Self::Txn(c) => c.next(cx).await,
3923 Self::TimeTravel(c) => c.next(cx).await,
3924 }
3925 }
3926
3927 async fn count_all_rows(&mut self, cx: &Cx) -> Result<i64> {
3930 match self {
3931 Self::Mem(c) => c.count_all_rows(cx).await,
3932 Self::Txn(c) => c.count_all_rows(cx).await,
3933 Self::TimeTravel(c) => c.count_all_rows(cx).await,
3934 }
3935 }
3936
3937 async fn prev(&mut self, cx: &Cx) -> Result<bool> {
3938 match self {
3939 Self::Mem(c) => c.prev(cx).await,
3940 Self::Txn(c) => c.prev(cx).await,
3941 Self::TimeTravel(c) => c.prev(cx).await,
3942 }
3943 }
3944
3945 fn eof(&self) -> bool {
3946 match self {
3947 Self::Mem(c) => c.eof(),
3948 Self::Txn(c) => c.eof(),
3949 Self::TimeTravel(c) => c.eof(),
3950 }
3951 }
3952
3953 async fn rowid(&self, cx: &Cx) -> Result<i64> {
3954 match self {
3955 Self::Mem(c) => c.rowid(cx).await,
3956 Self::Txn(c) => c.rowid(cx).await,
3957 Self::TimeTravel(c) => c.rowid(cx).await,
3958 }
3959 }
3960
3961 async fn payload(&self, cx: &Cx) -> Result<Vec<u8>> {
3962 match self {
3963 Self::Mem(c) => c.payload(cx).await,
3964 Self::Txn(c) => c.payload(cx).await,
3965 Self::TimeTravel(c) => c.payload(cx).await,
3966 }
3967 }
3968
3969 async fn payload_into(&self, cx: &Cx, buf: &mut Vec<u8>) -> Result<()> {
3970 match self {
3971 Self::Mem(c) => c.payload_into(cx, buf).await,
3972 Self::Txn(c) => c.payload_into(cx, buf).await,
3973 Self::TimeTravel(c) => c.payload_into(cx, buf).await,
3974 }
3975 }
3976
3977 async fn payload_prefix_into(
3978 &self,
3979 cx: &Cx,
3980 max_prefix_bytes: usize,
3981 buf: &mut Vec<u8>,
3982 ) -> Result<()> {
3983 match self {
3984 Self::Mem(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3985 Self::Txn(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3986 Self::TimeTravel(c) => c.payload_prefix_into(cx, max_prefix_bytes, buf).await,
3987 }
3988 }
3989
3990 fn try_probe_current_first_index_key_integer_local(
3991 &self,
3992 probe_value: i64,
3993 ) -> Result<Option<(bool, i64)>> {
3994 match self {
3995 Self::Mem(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3996 Self::Txn(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3997 Self::TimeTravel(c) => c.try_probe_current_first_index_key_integer_local(probe_value),
3998 }
3999 }
4000
4001 async fn count_equal_first_index_key_run_integer_local_segment(
4002 &mut self,
4003 cx: &Cx,
4004 probe_value: i64,
4005 ) -> Result<FirstIndexKeyIntegerLocalRunSegment> {
4006 match self {
4007 Self::Mem(c) => {
4008 c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4009 .await
4010 }
4011 Self::Txn(c) => {
4012 c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4013 .await
4014 }
4015 Self::TimeTravel(c) => {
4016 c.count_equal_first_index_key_run_integer_local_segment(cx, probe_value)
4017 .await
4018 }
4019 }
4020 }
4021
4022 async fn table_move_to(&mut self, cx: &Cx, rowid: i64) -> Result<SeekResult> {
4023 match self {
4024 Self::Mem(c) => c.table_move_to(cx, rowid).await,
4025 Self::Txn(c) => c.table_move_to(cx, rowid).await,
4026 Self::TimeTravel(c) => c.table_move_to(cx, rowid).await,
4027 }
4028 }
4029
4030 async fn table_advance_to(&mut self, cx: &Cx, rowid: i64) -> Result<SeekResult> {
4031 match self {
4032 Self::Mem(c) => c.advance_to(cx, rowid).await,
4033 Self::Txn(c) => c.advance_to(cx, rowid).await,
4034 Self::TimeTravel(c) => c.advance_to(cx, rowid).await,
4035 }
4036 }
4037
4038 async fn table_insert(&mut self, cx: &Cx, rowid: i64, data: &[u8]) -> Result<()> {
4039 match self {
4040 Self::Mem(c) => c.table_insert(cx, rowid, data).await,
4041 Self::Txn(c) => c.table_insert(cx, rowid, data).await,
4042 Self::TimeTravel(_) => Err(FrankenError::Internal(
4043 "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4044 )),
4045 }
4046 }
4047
4048 async fn table_insert_prechecked_absent(
4049 &mut self,
4050 cx: &Cx,
4051 rowid: i64,
4052 data: &[u8],
4053 ) -> Result<()> {
4054 match self {
4055 Self::Mem(c) => c.table_insert_prechecked_absent(cx, rowid, data).await,
4056 Self::Txn(c) => c.table_insert_prechecked_absent(cx, rowid, data).await,
4057 Self::TimeTravel(_) => Err(FrankenError::Internal(
4058 "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4059 )),
4060 }
4061 }
4062
4063 async fn table_refresh_rightmost_leaf_cache_after_insert(
4064 &mut self,
4065 cx: &Cx,
4066 rowid: i64,
4067 ) -> Result<()> {
4068 match self {
4069 Self::Mem(c) => {
4070 c.table_refresh_rightmost_leaf_cache_after_insert(cx, rowid)
4071 .await
4072 }
4073 Self::Txn(c) => {
4074 c.table_refresh_rightmost_leaf_cache_after_insert(cx, rowid)
4075 .await
4076 }
4077 Self::TimeTravel(_) => Err(FrankenError::Internal(
4078 "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4079 )),
4080 }
4081 }
4082
4083 async fn table_append_after_last_position(
4084 &mut self,
4085 cx: &Cx,
4086 rowid: i64,
4087 data: &[u8],
4088 ) -> Result<()> {
4089 match self {
4090 Self::Mem(c) => c.table_append_after_last_position(cx, rowid, data).await,
4091 Self::Txn(c) => c.table_append_after_last_position(cx, rowid, data).await,
4092 Self::TimeTravel(_) => Err(FrankenError::Internal(
4093 "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4094 )),
4095 }
4096 }
4097
4098 async fn table_append_after_last_position_with_writer<W>(
4099 &mut self,
4100 cx: &Cx,
4101 rowid: i64,
4102 payload_len: usize,
4103 writer: W,
4104 ) -> Result<bool>
4105 where
4106 W: FnOnce(&mut [u8]) -> Result<()>,
4107 {
4108 match self {
4109 Self::Mem(c) => {
4110 c.table_append_after_last_position_with_writer(cx, rowid, payload_len, writer)
4111 .await
4112 }
4113 Self::Txn(c) => {
4114 c.table_append_after_last_position_with_writer(cx, rowid, payload_len, writer)
4115 .await
4116 }
4117 Self::TimeTravel(_) => Err(FrankenError::Internal(
4118 "time-travel cursors are read-only: table_insert not permitted".to_owned(),
4119 )),
4120 }
4121 }
4122
4123 async fn delete(&mut self, cx: &Cx) -> Result<()> {
4124 match self {
4125 Self::Mem(c) => c.delete(cx).await,
4126 Self::Txn(c) => c.delete(cx).await,
4127 Self::TimeTravel(_) => Err(FrankenError::Internal(
4128 "time-travel cursors are read-only: delete not permitted".to_owned(),
4129 )),
4130 }
4131 }
4132
4133 async fn index_move_to(&mut self, cx: &Cx, key: &[u8]) -> Result<SeekResult> {
4135 match self {
4136 Self::Mem(c) => c.index_move_to(cx, key).await,
4137 Self::Txn(c) => c.index_move_to(cx, key).await,
4138 Self::TimeTravel(c) => c.index_move_to(cx, key).await,
4139 }
4140 }
4141
4142 async fn index_move_to_upper_bound(&mut self, cx: &Cx, key: &[u8]) -> Result<()> {
4143 match self {
4144 Self::Mem(c) => c.index_move_to_upper_bound(cx, key).await,
4145 Self::Txn(c) => c.index_move_to_upper_bound(cx, key).await,
4146 Self::TimeTravel(c) => c.index_move_to_upper_bound(cx, key).await,
4147 }
4148 }
4149
4150 async fn index_insert(&mut self, cx: &Cx, key: &[u8]) -> Result<()> {
4152 match self {
4153 Self::Mem(c) => c.index_insert(cx, key).await,
4154 Self::Txn(c) => c.index_insert(cx, key).await,
4155 Self::TimeTravel(_) => Err(FrankenError::Internal(
4156 "time-travel cursors are read-only: index_insert not permitted".to_owned(),
4157 )),
4158 }
4159 }
4160
4161 async fn index_insert_unique_with_rightmost_report(
4164 &mut self,
4165 cx: &Cx,
4166 key: &[u8],
4167 n_unique_cols: usize,
4168 columns_label: &str,
4169 ) -> Result<bool> {
4170 match self {
4171 Self::Mem(c) => {
4172 c.index_insert_unique_with_rightmost_report(cx, key, n_unique_cols, columns_label)
4173 .await
4174 }
4175 Self::Txn(c) => {
4176 c.index_insert_unique_with_rightmost_report(cx, key, n_unique_cols, columns_label)
4177 .await
4178 }
4179 Self::TimeTravel(_) => Err(FrankenError::Internal(
4180 "time-travel cursors are read-only: index_insert_unique not permitted".to_owned(),
4181 )),
4182 }
4183 }
4184
4185 async fn index_append_after_current_rightmost_position(
4187 &mut self,
4188 cx: &Cx,
4189 key: &[u8],
4190 ) -> Result<bool> {
4191 match self {
4192 Self::Mem(c) => {
4193 c.index_append_after_current_rightmost_position(cx, key)
4194 .await
4195 }
4196 Self::Txn(c) => {
4197 c.index_append_after_current_rightmost_position(cx, key)
4198 .await
4199 }
4200 Self::TimeTravel(_) => Err(FrankenError::Internal(
4201 "time-travel cursors are read-only: index_insert not permitted".to_owned(),
4202 )),
4203 }
4204 }
4205
4206 fn clear_position(&mut self) {
4211 match self {
4212 Self::Mem(c) => c.invalidate(),
4213 Self::Txn(c) => c.invalidate(),
4214 Self::TimeTravel(c) => c.invalidate(),
4215 }
4216 }
4217
4218 #[must_use]
4219 fn position_stamp(&self) -> Option<CursorPositionStamp> {
4220 match self {
4221 Self::Mem(c) => c.position_stamp(),
4222 Self::Txn(c) => c.position_stamp(),
4223 Self::TimeTravel(c) => c.position_stamp(),
4224 }
4225 }
4226}
4227
4228type RowDecodeScratch = fsqlite_types::record::RecordDecodeScratch;
4239
4240#[derive(Debug)]
4241struct StorageCursor {
4242 cursor: CursorBackend,
4243 cx: Cx,
4244 writable: bool,
4246 root_page: i32,
4248 rowid_mode: RowIdMode,
4250 autoincrement_high_water: i64,
4252 last_alloc_rowid: i64,
4256 payload_buf: Vec<u8>,
4258 target_vals_buf: Vec<SqliteValue>,
4260 cur_vals_buf: Vec<SqliteValue>,
4262 row_decode: RowDecodeScratch,
4264 last_position_stamp: Option<CursorPositionStamp>,
4266 last_successful_insert_rowid: Option<i64>,
4272 last_rightmost_unique_index_prefix: Option<Vec<SqliteValue>>,
4277 last_rightmost_unique_index_position: Option<CursorPositionStamp>,
4279 cached_rowid: Option<i64>,
4281 payload_includes_rowid_alias: Option<bool>,
4283 ipk_col_idx: Option<usize>,
4285 table_column_count: Option<usize>,
4287 first_not_null_non_ipk_col: Option<usize>,
4289}
4290
4291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4298#[must_use]
4299pub struct MemDbVersionToken(usize);
4300
4301#[derive(Debug, Clone)]
4302#[allow(dead_code)] enum MemDbUndoOp {
4304 CreateTable {
4305 root_page: i32,
4306 prev_next_root_page: i32,
4307 },
4308 DestroyTable {
4309 root_page: i32,
4310 table: MemTable,
4311 },
4312 ClearTable {
4313 root_page: i32,
4314 table: MemTable,
4315 },
4316 BumpRowid {
4317 root_page: i32,
4318 prev_next_rowid: i64,
4319 },
4320 UpsertRow {
4321 root_page: i32,
4322 rowid: i64,
4323 prev_next_rowid: i64,
4324 old_values: Option<MemRowValues>,
4325 },
4326 DeleteRow {
4327 root_page: i32,
4328 row: MemRow,
4329 prev_next_rowid: i64,
4330 },
4331}
4332
4333impl MemDbUndoOp {
4334 fn undo(self, db: &mut MemDatabase) {
4335 match self {
4336 Self::CreateTable {
4337 root_page,
4338 prev_next_root_page,
4339 } => {
4340 db.tables.remove(&root_page);
4341 db.next_root_page = prev_next_root_page;
4342 }
4343 Self::DestroyTable { root_page, table } | Self::ClearTable { root_page, table } => {
4344 db.tables.insert(root_page, table);
4345 }
4346 Self::BumpRowid {
4347 root_page,
4348 prev_next_rowid,
4349 } => {
4350 if let Some(table) = db.tables.get_mut(&root_page) {
4351 table.next_rowid = prev_next_rowid;
4352 }
4353 }
4354 Self::UpsertRow {
4355 root_page,
4356 rowid,
4357 prev_next_rowid,
4358 old_values,
4359 } => {
4360 if let Some(table) = db.tables.get_mut(&root_page) {
4361 match old_values {
4362 Some(values) => {
4363 table.insert(rowid, values);
4364 }
4365 None => {
4366 table.delete_by_rowid(rowid);
4367 }
4368 }
4369 table.next_rowid = prev_next_rowid;
4370 }
4371 }
4372 Self::DeleteRow {
4373 root_page,
4374 row,
4375 prev_next_rowid,
4376 } => {
4377 if let Some(table) = db.tables.get_mut(&root_page) {
4378 table.insert(row.rowid, row.values);
4379 table.next_rowid = prev_next_rowid;
4380 }
4381 }
4382 }
4383 }
4384}
4385
4386#[derive(Debug, Clone)]
4391pub struct MemDatabase {
4392 pub tables: SwissIndex<i32, MemTable>,
4394 next_root_page: i32,
4396 undo_enabled: bool,
4398 undo_log: Vec<MemDbUndoOp>,
4400}
4401
4402impl MemDatabase {
4403 pub fn new() -> Self {
4405 Self {
4406 tables: SwissIndex::new(),
4407 next_root_page: 2, undo_enabled: false,
4409 undo_log: Vec::new(),
4410 }
4411 }
4412
4413 pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
4415 for (_, table) in self.tables.iter_mut() {
4416 table.set_collation_registry(Arc::clone(®istry));
4417 }
4418 }
4419
4420 #[must_use]
4422 pub fn table_count(&self) -> i32 {
4423 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
4424 let count = self.tables.len() as i32;
4425 count
4426 }
4427
4428 #[must_use]
4430 pub fn next_root_page(&self) -> i32 {
4431 self.next_root_page
4432 }
4433
4434 pub fn set_next_root_page(&mut self, val: i32) {
4439 self.next_root_page = val;
4440 }
4441
4442 pub fn allocate_root_page(&mut self) -> i32 {
4450 let root_page = self.next_root_page;
4451 self.next_root_page += 1;
4452 root_page
4453 }
4454
4455 pub fn create_table(&mut self, num_columns: usize) -> i32 {
4457 let prev_next_root_page = self.next_root_page;
4458 let root_page = prev_next_root_page;
4459 self.next_root_page += 1;
4460 self.tables.insert(root_page, MemTable::new(num_columns));
4461 self.push_undo(MemDbUndoOp::CreateTable {
4462 root_page,
4463 prev_next_root_page,
4464 });
4465 root_page
4466 }
4467
4468 pub fn create_table_at(&mut self, root_page: i32, num_columns: usize) {
4475 let prev_next_root_page = self.next_root_page;
4476 if root_page >= self.next_root_page {
4477 self.next_root_page = root_page + 1;
4478 }
4479 self.tables.insert(root_page, MemTable::new(num_columns));
4480 self.push_undo(MemDbUndoOp::CreateTable {
4481 root_page,
4482 prev_next_root_page,
4483 });
4484 }
4485
4486 pub fn get_table(&self, root_page: i32) -> Option<&MemTable> {
4488 self.tables.get(&root_page)
4489 }
4490
4491 pub fn get_table_mut(&mut self, root_page: i32) -> Option<&mut MemTable> {
4493 self.tables.get_mut(&root_page)
4494 }
4495
4496 fn push_undo(&mut self, op: MemDbUndoOp) {
4497 if self.undo_enabled {
4498 self.undo_log.push(op);
4499 }
4500 }
4501
4502 pub fn undo_version(&self) -> MemDbVersionToken {
4506 MemDbVersionToken(self.undo_log.len())
4507 }
4508
4509 pub fn begin_undo(&mut self) {
4511 self.undo_enabled = true;
4512 self.undo_log.clear();
4513 }
4514
4515 pub fn commit_undo(&mut self) {
4517 self.undo_enabled = false;
4518 self.undo_log.clear();
4519 }
4520
4521 pub fn rollback_to(&mut self, token: MemDbVersionToken) {
4523 while self.undo_log.len() > token.0 {
4524 if let Some(op) = self.undo_log.pop() {
4525 op.undo(self);
4526 }
4527 }
4528 }
4529
4530 pub fn destroy_table(&mut self, root_page: i32) {
4532 if let Some(table) = self.tables.remove(&root_page) {
4533 self.push_undo(MemDbUndoOp::DestroyTable { root_page, table });
4534 }
4535 }
4536
4537 fn clear_table(&mut self, root_page: i32) {
4538 let prev = self.tables.get(&root_page).cloned();
4539 if let Some(table) = prev {
4540 self.push_undo(MemDbUndoOp::ClearTable { root_page, table });
4541 }
4542 if let Some(table) = self.tables.get_mut(&root_page) {
4543 table.clear();
4544 }
4545 }
4546
4547 fn alloc_rowid(&mut self, root_page: i32) -> i64 {
4548 if let Some(table) = self.tables.get_mut(&root_page) {
4549 let prev_next_rowid = table.next_rowid;
4550 let rowid = table.alloc_rowid();
4551 self.push_undo(MemDbUndoOp::BumpRowid {
4552 root_page,
4553 prev_next_rowid,
4554 });
4555 rowid
4556 } else {
4557 1
4558 }
4559 }
4560
4561 fn alloc_rowid_concurrent(&mut self, root_page: i32) -> i64 {
4567 if let Some(table) = self.tables.get_mut(&root_page) {
4568 let prev_next_rowid = table.next_rowid;
4569 let max_visible = table.max_visible_rowid().unwrap_or(0);
4570 let rowid = max_visible.saturating_add(1);
4571 table.next_rowid = rowid.saturating_add(1);
4572 self.push_undo(MemDbUndoOp::BumpRowid {
4573 root_page,
4574 prev_next_rowid,
4575 });
4576 rowid
4577 } else {
4578 1
4579 }
4580 }
4581
4582 pub fn upsert_row<V>(&mut self, root_page: i32, rowid: i64, values: V)
4583 where
4584 V: Into<MemRowValues>,
4585 {
4586 let values = values.into();
4587 if let Some(table) = self.tables.get_mut(&root_page) {
4588 let prev_next_rowid = table.next_rowid;
4589 let old_values = table
4592 .rows
4593 .binary_search_by_key(&rowid, |r| r.rowid)
4594 .ok()
4595 .map(|idx| table.rows[idx].values.clone());
4596 table.insert(rowid, values);
4597 self.push_undo(MemDbUndoOp::UpsertRow {
4598 root_page,
4599 rowid,
4600 prev_next_rowid,
4601 old_values,
4602 });
4603 }
4604 }
4605
4606 pub fn remove_column_from_rows(&mut self, root_page: i32, removed_slot: usize) -> bool {
4612 let Some(previous) = self.tables.get(&root_page).cloned() else {
4613 return false;
4614 };
4615 if previous
4616 .unique_constraints
4617 .iter()
4618 .any(|constraint| constraint.columns.contains(&removed_slot))
4619 {
4620 return false;
4621 }
4622 self.push_undo(MemDbUndoOp::ClearTable {
4623 root_page,
4624 table: previous,
4625 });
4626 if let Some(table) = self.tables.get_mut(&root_page) {
4627 table.remove_column_from_rows(removed_slot);
4628 }
4629 true
4630 }
4631
4632 #[must_use]
4637 pub fn insert_auto_row<V>(
4638 &mut self,
4639 root_page: i32,
4640 values: V,
4641 rowid_value_column: Option<usize>,
4642 ) -> Option<i64>
4643 where
4644 V: Into<MemRowValues>,
4645 {
4646 let mut values = values.into();
4647 if let Some(table) = self.tables.get_mut(&root_page) {
4648 let prev_next_rowid = table.next_rowid;
4649 let rowid = table.alloc_rowid();
4650 if let Some(column) = rowid_value_column
4651 && let Some(value) = values.get_mut(column)
4652 {
4653 *value = SqliteValue::Integer(rowid);
4654 }
4655 let old_values = table
4656 .rows
4657 .binary_search_by_key(&rowid, |r| r.rowid)
4658 .ok()
4659 .map(|idx| table.rows[idx].values.clone());
4660 table.insert(rowid, values);
4661 self.push_undo(MemDbUndoOp::UpsertRow {
4662 root_page,
4663 rowid,
4664 prev_next_rowid,
4665 old_values,
4666 });
4667 Some(rowid)
4668 } else {
4669 None
4670 }
4671 }
4672
4673 pub fn delete_rowid(&mut self, root_page: i32, rowid: i64) -> bool {
4675 if let Some(table) = self.tables.get_mut(&root_page)
4676 && let Ok(index) = table.rows.binary_search_by_key(&rowid, |r| r.rowid)
4677 {
4678 let prev_next_rowid = table.next_rowid;
4679 let row = table.rows.remove(index);
4680 table.remove_unique_entries(row.rowid, &row.values);
4681 self.push_undo(MemDbUndoOp::DeleteRow {
4682 root_page,
4683 row,
4684 prev_next_rowid,
4685 });
4686 true
4687 } else {
4688 false
4689 }
4690 }
4691
4692 #[allow(dead_code)]
4693 fn delete_at(&mut self, root_page: i32, index: usize) {
4694 if let Some(table) = self.tables.get_mut(&root_page) {
4695 if index < table.rows.len() {
4696 let prev_next_rowid = table.next_rowid;
4697 let row = table.rows.remove(index);
4698 table.remove_unique_entries(row.rowid, &row.values);
4699 self.push_undo(MemDbUndoOp::DeleteRow {
4700 root_page,
4701 row,
4702 prev_next_rowid,
4703 });
4704 }
4705 }
4706 }
4707}
4708
4709impl Default for MemDatabase {
4710 fn default() -> Self {
4711 Self::new()
4712 }
4713}
4714
4715const VDBE_TRACE_ENV: &str = "FSQLITE_VDBE_TRACE_OPCODES";
4720const VDBE_TRACE_LOGGING_STANDARD: &str = "bd-1fpm";
4721
4722const SLOW_QUERY_THRESHOLD_MS: u128 = 100;
4724
4725static FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL: AtomicU64 = AtomicU64::new(0);
4729static FSQLITE_VDBE_STATEMENTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4731static FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL: AtomicU64 = AtomicU64::new(0);
4733static FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS: LazyLock<Box<[AtomicU64]>> = LazyLock::new(|| {
4735 (0..=Opcode::COUNT)
4736 .map(|_| AtomicU64::new(0))
4737 .collect::<Vec<_>>()
4738 .into_boxed_slice()
4739});
4740static FSQLITE_VDBE_TYPE_COERCIONS_TOTAL: AtomicU64 = AtomicU64::new(0);
4742static FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL: AtomicU64 = AtomicU64::new(0);
4744static FSQLITE_VDBE_COLUMN_READS_TOTAL: AtomicU64 = AtomicU64::new(0);
4746static FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4748static FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL: AtomicU64 = AtomicU64::new(0);
4750static FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL: AtomicU64 = AtomicU64::new(0);
4752static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL: AtomicU64 = AtomicU64::new(0);
4754static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL: AtomicU64 = AtomicU64::new(0);
4756static FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL: AtomicU64 = AtomicU64::new(0);
4758static FSQLITE_VDBE_DECODED_VALUES_TOTAL: AtomicU64 = AtomicU64::new(0);
4760static FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4762static FSQLITE_VDBE_RESULT_ROWS_TOTAL: AtomicU64 = AtomicU64::new(0);
4764static FSQLITE_VDBE_RESULT_VALUES_TOTAL: AtomicU64 = AtomicU64::new(0);
4766static FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4768static FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
4770static FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4772static FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4774#[cfg(test)]
4775thread_local! {
4776 static FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL: Cell<u64> = const { Cell::new(0) };
4778}
4779static FSQLITE_VDBE_INSERT_APPEND_COUNT: AtomicU64 = AtomicU64::new(0);
4781static FSQLITE_VDBE_INSERT_SEEK_COUNT: AtomicU64 = AtomicU64::new(0);
4783static FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT: AtomicU64 = AtomicU64::new(0);
4785static FSQLITE_VDBE_DECODED_NULLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4787static FSQLITE_VDBE_DECODED_INTEGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4789static FSQLITE_VDBE_DECODED_REALS_TOTAL: AtomicU64 = AtomicU64::new(0);
4791static FSQLITE_VDBE_DECODED_TEXTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4793static FSQLITE_VDBE_DECODED_BLOBS_TOTAL: AtomicU64 = AtomicU64::new(0);
4795static FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4797static FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4799static FSQLITE_VDBE_RESULT_NULLS_TOTAL: AtomicU64 = AtomicU64::new(0);
4801static FSQLITE_VDBE_RESULT_INTEGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4803static FSQLITE_VDBE_RESULT_REALS_TOTAL: AtomicU64 = AtomicU64::new(0);
4805static FSQLITE_VDBE_RESULT_TEXTS_TOTAL: AtomicU64 = AtomicU64::new(0);
4807static FSQLITE_VDBE_RESULT_BLOBS_TOTAL: AtomicU64 = AtomicU64::new(0);
4809static FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4811static FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
4813static FSQLITE_VDBE_METRICS_ENABLED: AtomicBool = AtomicBool::new(false);
4818static VDBE_PROGRAM_ID_SEQ: AtomicU64 = AtomicU64::new(1);
4820
4821static FSQLITE_JIT_COMPILATIONS_TOTAL: AtomicU64 = AtomicU64::new(0);
4825static FSQLITE_JIT_COMPILE_FAILURES_TOTAL: AtomicU64 = AtomicU64::new(0);
4827static FSQLITE_JIT_TRIGGERS_TOTAL: AtomicU64 = AtomicU64::new(0);
4829static FSQLITE_JIT_CACHE_HITS_TOTAL: AtomicU64 = AtomicU64::new(0);
4831static FSQLITE_JIT_CACHE_MISSES_TOTAL: AtomicU64 = AtomicU64::new(0);
4833
4834static FSQLITE_JIT_ENABLED: std::sync::atomic::AtomicBool =
4841 std::sync::atomic::AtomicBool::new(false);
4842static FSQLITE_JIT_HOT_THRESHOLD: AtomicU64 = AtomicU64::new(8);
4844static FSQLITE_JIT_CACHE_CAPACITY: AtomicU64 = AtomicU64::new(128);
4846
4847#[derive(Debug, Clone)]
4849struct JitCacheEntry {
4850 code_size_bytes: u64,
4851 compiled_program: Arc<CompiledProgram>,
4852}
4853
4854#[derive(Debug, Default)]
4855struct JitRuntimeState {
4856 executions_by_plan: HashMap<u64, u64>,
4857 cache: HashMap<u64, JitCacheEntry>,
4858 lru: VecDeque<u64>,
4859 unsupported_plans: HashSet<u64>,
4860 unsupported_lru: VecDeque<u64>,
4861}
4862
4863impl JitRuntimeState {
4864 fn touch_lru(&mut self, plan_hash: u64) {
4865 self.lru.retain(|candidate| *candidate != plan_hash);
4866 self.lru.push_back(plan_hash);
4867 }
4868
4869 fn insert_cache(
4870 &mut self,
4871 plan_hash: u64,
4872 entry: JitCacheEntry,
4873 cache_capacity: usize,
4874 ) -> Option<u64> {
4875 if cache_capacity == 0 {
4876 return None;
4877 }
4878 let mut evicted = None;
4879 if let hashbrown::hash_map::Entry::Occupied(mut occupied) = self.cache.entry(plan_hash) {
4880 occupied.insert(entry);
4881 self.touch_lru(plan_hash);
4882 return None;
4883 }
4884 if self.cache.len() >= cache_capacity
4885 && let Some(oldest) = self.lru.pop_front()
4886 {
4887 self.cache.remove(&oldest);
4888 evicted = Some(oldest);
4889 }
4890 self.cache.insert(plan_hash, entry);
4891 self.touch_lru(plan_hash);
4892 evicted
4893 }
4894
4895 fn apply_capacity(&mut self, cache_capacity: usize) {
4896 if cache_capacity == 0 {
4897 self.cache.clear();
4898 self.lru.clear();
4899 self.unsupported_plans.clear();
4900 self.unsupported_lru.clear();
4901 return;
4902 }
4903 while self.cache.len() > cache_capacity {
4904 if let Some(oldest) = self.lru.pop_front() {
4905 self.cache.remove(&oldest);
4906 } else {
4907 break;
4908 }
4909 }
4910 while self.unsupported_plans.len() > cache_capacity {
4911 if let Some(oldest) = self.unsupported_lru.pop_front() {
4912 self.unsupported_plans.remove(&oldest);
4913 } else {
4914 break;
4915 }
4916 }
4917 }
4918
4919 fn mark_unsupported_plan(&mut self, plan_hash: u64, cache_capacity: usize) {
4920 if cache_capacity == 0 {
4921 return;
4922 }
4923 if !self.unsupported_plans.insert(plan_hash) {
4924 self.unsupported_lru
4925 .retain(|candidate| *candidate != plan_hash);
4926 self.unsupported_lru.push_back(plan_hash);
4927 return;
4928 }
4929 if self.unsupported_plans.len() > cache_capacity
4930 && let Some(oldest) = self.unsupported_lru.pop_front()
4931 {
4932 self.unsupported_plans.remove(&oldest);
4933 }
4934 self.unsupported_lru.push_back(plan_hash);
4935 }
4936
4937 fn is_unsupported_plan(&self, plan_hash: u64) -> bool {
4938 self.unsupported_plans.contains(&plan_hash)
4939 }
4940}
4941
4942static VDBE_JIT_RUNTIME: std::sync::OnceLock<Mutex<JitRuntimeState>> = std::sync::OnceLock::new();
4943
4944fn jit_runtime() -> &'static Mutex<JitRuntimeState> {
4945 VDBE_JIT_RUNTIME.get_or_init(|| Mutex::new(JitRuntimeState::default()))
4946}
4947
4948fn lock_jit_runtime() -> std::sync::MutexGuard<'static, JitRuntimeState> {
4949 jit_runtime()
4950 .lock()
4951 .unwrap_or_else(std::sync::PoisonError::into_inner)
4952}
4953
4954#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4956pub struct VdbeJitMetricsSnapshot {
4957 pub enabled: bool,
4959 pub hot_threshold: u64,
4961 pub cache_capacity: usize,
4963 pub cache_entries: usize,
4965 pub jit_compilations_total: u64,
4967 pub jit_compile_failures_total: u64,
4969 pub jit_triggers_total: u64,
4971 pub jit_cache_hits_total: u64,
4973 pub jit_cache_misses_total: u64,
4975 pub jit_cache_hit_ratio_percent: u64,
4977}
4978
4979#[must_use]
4981pub fn vdbe_jit_metrics_snapshot() -> VdbeJitMetricsSnapshot {
4982 let runtime = lock_jit_runtime();
4983 let hits = FSQLITE_JIT_CACHE_HITS_TOTAL.load(AtomicOrdering::Relaxed);
4984 let misses = FSQLITE_JIT_CACHE_MISSES_TOTAL.load(AtomicOrdering::Relaxed);
4985 let denom = hits.saturating_add(misses);
4986 let ratio_percent = if denom == 0 {
4987 0
4988 } else {
4989 hits.saturating_mul(100).saturating_add(denom / 2) / denom
4990 };
4991 VdbeJitMetricsSnapshot {
4992 enabled: FSQLITE_JIT_ENABLED.load(AtomicOrdering::Relaxed),
4993 hot_threshold: FSQLITE_JIT_HOT_THRESHOLD.load(AtomicOrdering::Relaxed),
4994 cache_capacity: usize::try_from(FSQLITE_JIT_CACHE_CAPACITY.load(AtomicOrdering::Relaxed))
4995 .unwrap_or(usize::MAX),
4996 cache_entries: runtime.cache.len(),
4997 jit_compilations_total: FSQLITE_JIT_COMPILATIONS_TOTAL.load(AtomicOrdering::Relaxed),
4998 jit_compile_failures_total: FSQLITE_JIT_COMPILE_FAILURES_TOTAL
4999 .load(AtomicOrdering::Relaxed),
5000 jit_triggers_total: FSQLITE_JIT_TRIGGERS_TOTAL.load(AtomicOrdering::Relaxed),
5001 jit_cache_hits_total: hits,
5002 jit_cache_misses_total: misses,
5003 jit_cache_hit_ratio_percent: ratio_percent,
5004 }
5005}
5006
5007pub fn set_vdbe_jit_enabled(enabled: bool) {
5009 FSQLITE_JIT_ENABLED.store(enabled, AtomicOrdering::Relaxed);
5010}
5011
5012#[must_use]
5014pub fn vdbe_jit_enabled() -> bool {
5015 FSQLITE_JIT_ENABLED.load(AtomicOrdering::Relaxed)
5016}
5017
5018#[must_use]
5022pub fn set_vdbe_jit_hot_threshold(threshold: u64) -> u64 {
5023 let clamped = threshold.max(1);
5024 FSQLITE_JIT_HOT_THRESHOLD.store(clamped, AtomicOrdering::Relaxed);
5025 clamped
5026}
5027
5028#[must_use]
5030pub fn vdbe_jit_hot_threshold() -> u64 {
5031 FSQLITE_JIT_HOT_THRESHOLD.load(AtomicOrdering::Relaxed)
5032}
5033
5034#[must_use]
5038pub fn set_vdbe_jit_cache_capacity(capacity: usize) -> usize {
5039 let value_u64 = u64::try_from(capacity).unwrap_or(u64::MAX);
5040 FSQLITE_JIT_CACHE_CAPACITY.store(value_u64, AtomicOrdering::Relaxed);
5041 let mut runtime = lock_jit_runtime();
5042 runtime.apply_capacity(capacity);
5043 capacity
5044}
5045
5046#[must_use]
5048pub fn vdbe_jit_cache_capacity() -> usize {
5049 usize::try_from(FSQLITE_JIT_CACHE_CAPACITY.load(AtomicOrdering::Relaxed)).unwrap_or(usize::MAX)
5050}
5051
5052pub fn reset_vdbe_jit_metrics() {
5054 FSQLITE_JIT_COMPILATIONS_TOTAL.store(0, AtomicOrdering::Relaxed);
5055 FSQLITE_JIT_COMPILE_FAILURES_TOTAL.store(0, AtomicOrdering::Relaxed);
5056 FSQLITE_JIT_TRIGGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5057 FSQLITE_JIT_CACHE_HITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5058 FSQLITE_JIT_CACHE_MISSES_TOTAL.store(0, AtomicOrdering::Relaxed);
5059 let mut runtime = lock_jit_runtime();
5060 runtime.executions_by_plan.clear();
5061 runtime.cache.clear();
5062 runtime.lru.clear();
5063 runtime.unsupported_plans.clear();
5064 runtime.unsupported_lru.clear();
5065}
5066
5067static FSQLITE_SORT_ROWS_TOTAL: AtomicU64 = AtomicU64::new(0);
5071static FSQLITE_SORT_SPILL_PAGES_TOTAL: AtomicU64 = AtomicU64::new(0);
5073static FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5075static FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5077static FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL: AtomicU64 = AtomicU64::new(0);
5079static FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL: AtomicU64 = AtomicU64::new(0);
5081static FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5083static FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5085static FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL: AtomicU64 = AtomicU64::new(0);
5087static FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL: AtomicU64 = AtomicU64::new(0);
5089static FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL: AtomicU64 = AtomicU64::new(0);
5091static FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5093static FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL: AtomicU64 = AtomicU64::new(0);
5095static FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
5097static FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
5099static FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5101static FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL: AtomicU64 = AtomicU64::new(0);
5103static FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL: AtomicU64 = AtomicU64::new(0);
5105static FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL: AtomicU64 = AtomicU64::new(0);
5107static FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL: AtomicU64 = AtomicU64::new(0);
5109static FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
5111static FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
5113
5114#[derive(Debug, Clone, PartialEq, Eq, Default)]
5116pub struct ValueTypeMetricsSnapshot {
5117 pub total_values: u64,
5119 pub nulls: u64,
5121 pub integers: u64,
5123 pub reals: u64,
5125 pub texts: u64,
5127 pub blobs: u64,
5129 pub text_bytes_total: u64,
5131 pub blob_bytes_total: u64,
5133}
5134
5135#[derive(Debug, Clone, PartialEq, Eq)]
5137pub struct OpcodeExecutionCount {
5138 pub opcode: String,
5140 pub total: u64,
5142}
5143
5144#[derive(Debug, Clone, PartialEq, Eq, Default)]
5146pub struct MvccWritePathMetricsSnapshot {
5147 pub tier0_already_owned_writes_total: u64,
5149 pub tier1_first_touch_writes_total: u64,
5151 pub tier2_commit_surface_writes_total: u64,
5153 pub page_lock_waits_total: u64,
5155 pub page_lock_wait_time_ns_total: u64,
5157 pub write_busy_retries_total: u64,
5159 pub write_busy_timeouts_total: u64,
5161 pub stale_snapshot_rejects_total: u64,
5163 pub page_one_conflict_tracks_total: u64,
5165 pub page_one_conflict_track_time_ns_total: u64,
5167 pub pending_commit_surface_clears_total: u64,
5169 pub pending_commit_surface_clear_time_ns_total: u64,
5171}
5172
5173#[derive(Debug, Clone, PartialEq, Eq, Default)]
5175pub struct PageDataMotionMetricsSnapshot {
5176 pub borrowed_write_normalization_calls_total: u64,
5178 pub borrowed_exact_size_copies_total: u64,
5180 pub owned_write_normalization_calls_total: u64,
5182 pub owned_passthrough_total: u64,
5184 pub owned_in_place_zero_extends_total: u64,
5186 pub owned_resized_copies_total: u64,
5188 pub normalized_payload_bytes_total: u64,
5190 pub normalized_zero_fill_bytes_total: u64,
5192}
5193
5194#[derive(Debug, Clone, PartialEq, Eq)]
5196pub struct VdbeMetricsSnapshot {
5197 pub opcodes_executed_total: u64,
5199 pub statements_total: u64,
5201 pub statement_duration_us_total: u64,
5203 pub sort_rows_total: u64,
5205 pub sort_spill_pages_total: u64,
5207 pub opcode_execution_totals: Vec<OpcodeExecutionCount>,
5209 pub type_coercions_total: u64,
5211 pub type_coercion_changes_total: u64,
5213 pub column_reads_total: u64,
5215 pub record_decode_calls_total: u64,
5217 pub decode_cache_hits_total: u64,
5219 pub decode_cache_misses_total: u64,
5221 pub decode_cache_invalidations_position_total: u64,
5223 pub decode_cache_invalidations_write_total: u64,
5225 pub decode_cache_invalidations_pseudo_total: u64,
5227 pub decoded_values_total: u64,
5229 pub decoded_value_heap_bytes_total: u64,
5231 pub result_rows_total: u64,
5233 pub result_values_total: u64,
5235 pub result_value_heap_bytes_total: u64,
5237 pub result_row_materialization_time_ns_total: u64,
5239 pub make_record_calls_total: u64,
5241 pub make_record_blob_bytes_total: u64,
5243 pub insert_append_count: u64,
5245 pub insert_seek_count: u64,
5247 pub insert_append_hint_clear_count: u64,
5249 pub decoded_value_types: ValueTypeMetricsSnapshot,
5251 pub result_value_types: ValueTypeMetricsSnapshot,
5253 pub mvcc_write_path: MvccWritePathMetricsSnapshot,
5255 pub page_data_motion: PageDataMotionMetricsSnapshot,
5257}
5258
5259pub fn set_vdbe_metrics_enabled(enabled: bool) {
5261 FSQLITE_VDBE_METRICS_ENABLED.store(enabled, AtomicOrdering::Relaxed);
5262}
5263
5264#[must_use]
5266pub fn vdbe_metrics_enabled() -> bool {
5267 FSQLITE_VDBE_METRICS_ENABLED.load(AtomicOrdering::Relaxed)
5268}
5269
5270#[must_use]
5272pub fn vdbe_metrics_snapshot() -> VdbeMetricsSnapshot {
5273 let mut opcode_execution_totals: Vec<OpcodeExecutionCount> =
5274 FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS
5275 .iter()
5276 .enumerate()
5277 .skip(1)
5278 .filter_map(|(idx, counter)| {
5279 let total = counter.load(AtomicOrdering::Relaxed);
5280 if total == 0 {
5281 return None;
5282 }
5283 let raw = u8::try_from(idx).ok()?;
5284 let opcode = Opcode::from_byte(raw)?;
5285 Some(OpcodeExecutionCount {
5286 opcode: opcode.name().to_owned(),
5287 total,
5288 })
5289 })
5290 .collect();
5291 opcode_execution_totals.sort_by(|lhs, rhs| {
5292 rhs.total
5293 .cmp(&lhs.total)
5294 .then_with(|| lhs.opcode.cmp(&rhs.opcode))
5295 });
5296 VdbeMetricsSnapshot {
5297 opcodes_executed_total: FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.load(AtomicOrdering::Relaxed),
5298 statements_total: FSQLITE_VDBE_STATEMENTS_TOTAL.load(AtomicOrdering::Relaxed),
5299 statement_duration_us_total: FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
5300 .load(AtomicOrdering::Relaxed),
5301 sort_rows_total: FSQLITE_SORT_ROWS_TOTAL.load(AtomicOrdering::Relaxed),
5302 sort_spill_pages_total: FSQLITE_SORT_SPILL_PAGES_TOTAL.load(AtomicOrdering::Relaxed),
5303 opcode_execution_totals,
5304 type_coercions_total: FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.load(AtomicOrdering::Relaxed),
5305 type_coercion_changes_total: FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL
5306 .load(AtomicOrdering::Relaxed),
5307 column_reads_total: FSQLITE_VDBE_COLUMN_READS_TOTAL.load(AtomicOrdering::Relaxed),
5308 record_decode_calls_total: FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
5309 .load(AtomicOrdering::Relaxed),
5310 decode_cache_hits_total: FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.load(AtomicOrdering::Relaxed),
5311 decode_cache_misses_total: FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL
5312 .load(AtomicOrdering::Relaxed),
5313 decode_cache_invalidations_position_total:
5314 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL.load(AtomicOrdering::Relaxed),
5315 decode_cache_invalidations_write_total: FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL
5316 .load(AtomicOrdering::Relaxed),
5317 decode_cache_invalidations_pseudo_total:
5318 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL.load(AtomicOrdering::Relaxed),
5319 decoded_values_total: FSQLITE_VDBE_DECODED_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5320 decoded_value_heap_bytes_total: FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL
5321 .load(AtomicOrdering::Relaxed),
5322 result_rows_total: FSQLITE_VDBE_RESULT_ROWS_TOTAL.load(AtomicOrdering::Relaxed),
5323 result_values_total: FSQLITE_VDBE_RESULT_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5324 result_value_heap_bytes_total: FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL
5325 .load(AtomicOrdering::Relaxed),
5326 result_row_materialization_time_ns_total:
5327 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.load(AtomicOrdering::Relaxed),
5328 make_record_calls_total: FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.load(AtomicOrdering::Relaxed),
5329 make_record_blob_bytes_total: FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL
5330 .load(AtomicOrdering::Relaxed),
5331 insert_append_count: FSQLITE_VDBE_INSERT_APPEND_COUNT.load(AtomicOrdering::Relaxed),
5332 insert_seek_count: FSQLITE_VDBE_INSERT_SEEK_COUNT.load(AtomicOrdering::Relaxed),
5333 insert_append_hint_clear_count: FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
5334 .load(AtomicOrdering::Relaxed),
5335 decoded_value_types: ValueTypeMetricsSnapshot {
5336 total_values: FSQLITE_VDBE_DECODED_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5337 nulls: FSQLITE_VDBE_DECODED_NULLS_TOTAL.load(AtomicOrdering::Relaxed),
5338 integers: FSQLITE_VDBE_DECODED_INTEGERS_TOTAL.load(AtomicOrdering::Relaxed),
5339 reals: FSQLITE_VDBE_DECODED_REALS_TOTAL.load(AtomicOrdering::Relaxed),
5340 texts: FSQLITE_VDBE_DECODED_TEXTS_TOTAL.load(AtomicOrdering::Relaxed),
5341 blobs: FSQLITE_VDBE_DECODED_BLOBS_TOTAL.load(AtomicOrdering::Relaxed),
5342 text_bytes_total: FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5343 blob_bytes_total: FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5344 },
5345 result_value_types: ValueTypeMetricsSnapshot {
5346 total_values: FSQLITE_VDBE_RESULT_VALUES_TOTAL.load(AtomicOrdering::Relaxed),
5347 nulls: FSQLITE_VDBE_RESULT_NULLS_TOTAL.load(AtomicOrdering::Relaxed),
5348 integers: FSQLITE_VDBE_RESULT_INTEGERS_TOTAL.load(AtomicOrdering::Relaxed),
5349 reals: FSQLITE_VDBE_RESULT_REALS_TOTAL.load(AtomicOrdering::Relaxed),
5350 texts: FSQLITE_VDBE_RESULT_TEXTS_TOTAL.load(AtomicOrdering::Relaxed),
5351 blobs: FSQLITE_VDBE_RESULT_BLOBS_TOTAL.load(AtomicOrdering::Relaxed),
5352 text_bytes_total: FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5353 blob_bytes_total: FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL.load(AtomicOrdering::Relaxed),
5354 },
5355 mvcc_write_path: MvccWritePathMetricsSnapshot {
5356 tier0_already_owned_writes_total: FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL
5357 .load(AtomicOrdering::Relaxed),
5358 tier1_first_touch_writes_total: FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL
5359 .load(AtomicOrdering::Relaxed),
5360 tier2_commit_surface_writes_total: FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL
5361 .load(AtomicOrdering::Relaxed),
5362 page_lock_waits_total: FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL
5363 .load(AtomicOrdering::Relaxed),
5364 page_lock_wait_time_ns_total: FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL
5365 .load(AtomicOrdering::Relaxed),
5366 write_busy_retries_total: FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL
5367 .load(AtomicOrdering::Relaxed),
5368 write_busy_timeouts_total: FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL
5369 .load(AtomicOrdering::Relaxed),
5370 stale_snapshot_rejects_total: FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL
5371 .load(AtomicOrdering::Relaxed),
5372 page_one_conflict_tracks_total: FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL
5373 .load(AtomicOrdering::Relaxed),
5374 page_one_conflict_track_time_ns_total:
5375 FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL
5376 .load(AtomicOrdering::Relaxed),
5377 pending_commit_surface_clears_total: FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL
5378 .load(AtomicOrdering::Relaxed),
5379 pending_commit_surface_clear_time_ns_total:
5380 FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL.load(AtomicOrdering::Relaxed),
5381 },
5382 page_data_motion: PageDataMotionMetricsSnapshot {
5383 borrowed_write_normalization_calls_total:
5384 FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL
5385 .load(AtomicOrdering::Relaxed),
5386 borrowed_exact_size_copies_total:
5387 FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL
5388 .load(AtomicOrdering::Relaxed),
5389 owned_write_normalization_calls_total:
5390 FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL.load(AtomicOrdering::Relaxed),
5391 owned_passthrough_total: FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL
5392 .load(AtomicOrdering::Relaxed),
5393 owned_in_place_zero_extends_total:
5394 FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL
5395 .load(AtomicOrdering::Relaxed),
5396 owned_resized_copies_total: FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL
5397 .load(AtomicOrdering::Relaxed),
5398 normalized_payload_bytes_total: FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL
5399 .load(AtomicOrdering::Relaxed),
5400 normalized_zero_fill_bytes_total:
5401 FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL
5402 .load(AtomicOrdering::Relaxed),
5403 },
5404 }
5405}
5406
5407pub fn reset_vdbe_metrics() {
5409 FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.store(0, AtomicOrdering::Relaxed);
5410 FSQLITE_VDBE_STATEMENTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5411 FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL.store(0, AtomicOrdering::Relaxed);
5412 for counter in FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS.iter() {
5413 counter.store(0, AtomicOrdering::Relaxed);
5414 }
5415 FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.store(0, AtomicOrdering::Relaxed);
5416 FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL.store(0, AtomicOrdering::Relaxed);
5417 FSQLITE_VDBE_COLUMN_READS_TOTAL.store(0, AtomicOrdering::Relaxed);
5418 FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5419 FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5420 FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL.store(0, AtomicOrdering::Relaxed);
5421 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL.store(0, AtomicOrdering::Relaxed);
5422 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL.store(0, AtomicOrdering::Relaxed);
5423 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL.store(0, AtomicOrdering::Relaxed);
5424 FSQLITE_VDBE_DECODED_VALUES_TOTAL.store(0, AtomicOrdering::Relaxed);
5425 FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5426 FSQLITE_VDBE_RESULT_ROWS_TOTAL.store(0, AtomicOrdering::Relaxed);
5427 FSQLITE_VDBE_RESULT_VALUES_TOTAL.store(0, AtomicOrdering::Relaxed);
5428 FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5429 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5430 FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5431 FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5432 FSQLITE_VDBE_INSERT_APPEND_COUNT.store(0, AtomicOrdering::Relaxed);
5433 FSQLITE_VDBE_INSERT_SEEK_COUNT.store(0, AtomicOrdering::Relaxed);
5434 FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT.store(0, AtomicOrdering::Relaxed);
5435 FSQLITE_VDBE_DECODED_NULLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5436 FSQLITE_VDBE_DECODED_INTEGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5437 FSQLITE_VDBE_DECODED_REALS_TOTAL.store(0, AtomicOrdering::Relaxed);
5438 FSQLITE_VDBE_DECODED_TEXTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5439 FSQLITE_VDBE_DECODED_BLOBS_TOTAL.store(0, AtomicOrdering::Relaxed);
5440 FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5441 FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5442 FSQLITE_VDBE_RESULT_NULLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5443 FSQLITE_VDBE_RESULT_INTEGERS_TOTAL.store(0, AtomicOrdering::Relaxed);
5444 FSQLITE_VDBE_RESULT_REALS_TOTAL.store(0, AtomicOrdering::Relaxed);
5445 FSQLITE_VDBE_RESULT_TEXTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5446 FSQLITE_VDBE_RESULT_BLOBS_TOTAL.store(0, AtomicOrdering::Relaxed);
5447 FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5448 FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5449 FSQLITE_SORT_ROWS_TOTAL.store(0, AtomicOrdering::Relaxed);
5450 FSQLITE_SORT_SPILL_PAGES_TOTAL.store(0, AtomicOrdering::Relaxed);
5451 FSQLITE_VDBE_MVCC_TIER0_ALREADY_OWNED_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5452 FSQLITE_VDBE_MVCC_TIER1_FIRST_TOUCH_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5453 FSQLITE_VDBE_MVCC_TIER2_COMMIT_SURFACE_WRITES_TOTAL.store(0, AtomicOrdering::Relaxed);
5454 FSQLITE_VDBE_MVCC_PAGE_LOCK_WAITS_TOTAL.store(0, AtomicOrdering::Relaxed);
5455 FSQLITE_VDBE_MVCC_PAGE_LOCK_WAIT_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5456 FSQLITE_VDBE_MVCC_WRITE_BUSY_RETRIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5457 FSQLITE_VDBE_MVCC_WRITE_BUSY_TIMEOUTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5458 FSQLITE_VDBE_MVCC_STALE_SNAPSHOT_REJECTS_TOTAL.store(0, AtomicOrdering::Relaxed);
5459 FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACKS_TOTAL.store(0, AtomicOrdering::Relaxed);
5460 FSQLITE_VDBE_MVCC_PAGE_ONE_CONFLICT_TRACK_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5461 FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEARS_TOTAL.store(0, AtomicOrdering::Relaxed);
5462 FSQLITE_VDBE_MVCC_PENDING_SURFACE_CLEAR_TIME_NS_TOTAL.store(0, AtomicOrdering::Relaxed);
5463 FSQLITE_VDBE_PAGE_DATA_BORROWED_NORMALIZATION_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5464 FSQLITE_VDBE_PAGE_DATA_BORROWED_EXACT_SIZE_COPIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5465 FSQLITE_VDBE_PAGE_DATA_OWNED_NORMALIZATION_CALLS_TOTAL.store(0, AtomicOrdering::Relaxed);
5466 FSQLITE_VDBE_PAGE_DATA_OWNED_PASSTHROUGH_TOTAL.store(0, AtomicOrdering::Relaxed);
5467 FSQLITE_VDBE_PAGE_DATA_OWNED_IN_PLACE_ZERO_EXTENDS_TOTAL.store(0, AtomicOrdering::Relaxed);
5468 FSQLITE_VDBE_PAGE_DATA_OWNED_RESIZED_COPIES_TOTAL.store(0, AtomicOrdering::Relaxed);
5469 FSQLITE_VDBE_PAGE_DATA_NORMALIZED_PAYLOAD_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5470 FSQLITE_VDBE_PAGE_DATA_NORMALIZED_ZERO_FILL_BYTES_TOTAL.store(0, AtomicOrdering::Relaxed);
5471 reset_vdbe_jit_metrics();
5472}
5473
5474#[cfg(test)]
5475fn reset_vdbe_test_sideband_materialization_count() {
5476 FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(|counter| counter.set(0));
5477}
5478
5479#[cfg(test)]
5480fn vdbe_test_sideband_materialization_count_snapshot() -> u64 {
5481 FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(Cell::get)
5482}
5483
5484fn estimated_value_heap_bytes(value: &SqliteValue) -> u64 {
5485 match value {
5486 SqliteValue::Null => 0,
5487 SqliteValue::Integer(_) | SqliteValue::Float(_) => {
5488 u64::try_from(std::mem::size_of::<SqliteValue>()).unwrap_or(u64::MAX)
5489 }
5490 SqliteValue::Text(text) => {
5491 u64::try_from(std::mem::size_of::<SqliteValue>().saturating_add(text.len()))
5492 .unwrap_or(u64::MAX)
5493 }
5494 SqliteValue::Blob(blob) => {
5495 u64::try_from(std::mem::size_of::<SqliteValue>().saturating_add(blob.len()))
5496 .unwrap_or(u64::MAX)
5497 }
5498 }
5499}
5500
5501struct ValueTypeMetricCounters<'a> {
5502 total: &'a AtomicU64,
5503 nulls: &'a AtomicU64,
5504 integers: &'a AtomicU64,
5505 reals: &'a AtomicU64,
5506 texts: &'a AtomicU64,
5507 blobs: &'a AtomicU64,
5508 text_bytes: &'a AtomicU64,
5509 blob_bytes: &'a AtomicU64,
5510}
5511
5512fn record_value_type_metrics(value: &SqliteValue, counters: &ValueTypeMetricCounters<'_>) {
5513 counters.total.fetch_add(1, AtomicOrdering::Relaxed);
5514 match value {
5515 SqliteValue::Null => {
5516 counters.nulls.fetch_add(1, AtomicOrdering::Relaxed);
5517 }
5518 SqliteValue::Integer(_) => {
5519 counters.integers.fetch_add(1, AtomicOrdering::Relaxed);
5520 }
5521 SqliteValue::Float(_) => {
5522 counters.reals.fetch_add(1, AtomicOrdering::Relaxed);
5523 }
5524 SqliteValue::Text(text) => {
5525 counters.texts.fetch_add(1, AtomicOrdering::Relaxed);
5526 counters.text_bytes.fetch_add(
5527 u64::try_from(text.len()).unwrap_or(u64::MAX),
5528 AtomicOrdering::Relaxed,
5529 );
5530 }
5531 SqliteValue::Blob(blob) => {
5532 counters.blobs.fetch_add(1, AtomicOrdering::Relaxed);
5533 counters.blob_bytes.fetch_add(
5534 u64::try_from(blob.len()).unwrap_or(u64::MAX),
5535 AtomicOrdering::Relaxed,
5536 );
5537 }
5538 }
5539}
5540
5541fn record_decoded_value_metrics(value: &SqliteValue) {
5542 FSQLITE_VDBE_DECODED_VALUE_HEAP_BYTES_TOTAL
5543 .fetch_add(estimated_value_heap_bytes(value), AtomicOrdering::Relaxed);
5544 let counters = ValueTypeMetricCounters {
5545 total: &FSQLITE_VDBE_DECODED_VALUES_TOTAL,
5546 nulls: &FSQLITE_VDBE_DECODED_NULLS_TOTAL,
5547 integers: &FSQLITE_VDBE_DECODED_INTEGERS_TOTAL,
5548 reals: &FSQLITE_VDBE_DECODED_REALS_TOTAL,
5549 texts: &FSQLITE_VDBE_DECODED_TEXTS_TOTAL,
5550 blobs: &FSQLITE_VDBE_DECODED_BLOBS_TOTAL,
5551 text_bytes: &FSQLITE_VDBE_DECODED_TEXT_BYTES_TOTAL,
5552 blob_bytes: &FSQLITE_VDBE_DECODED_BLOB_BYTES_TOTAL,
5553 };
5554 record_value_type_metrics(value, &counters);
5555}
5556
5557fn note_decode_cache_hit(collect_vdbe_metrics: bool) {
5558 if collect_vdbe_metrics {
5559 FSQLITE_VDBE_DECODE_CACHE_HITS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5560 }
5561}
5562
5563fn note_decode_cache_miss(collect_vdbe_metrics: bool) {
5564 if collect_vdbe_metrics {
5565 FSQLITE_VDBE_DECODE_CACHE_MISSES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5566 }
5567}
5568
5569fn note_decode_cache_invalidation(
5570 collect_vdbe_metrics: bool,
5571 reason: DecodeCacheInvalidationReason,
5572) {
5573 if !collect_vdbe_metrics {
5574 return;
5575 }
5576 match reason {
5577 DecodeCacheInvalidationReason::PositionChange => {
5578 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_POSITION_TOTAL
5579 .fetch_add(1, AtomicOrdering::Relaxed);
5580 }
5581 DecodeCacheInvalidationReason::WriteMutation => {
5582 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_WRITE_TOTAL
5583 .fetch_add(1, AtomicOrdering::Relaxed);
5584 }
5585 DecodeCacheInvalidationReason::PseudoRowChange => {
5586 FSQLITE_VDBE_DECODE_CACHE_INVALIDATIONS_PSEUDO_TOTAL
5587 .fetch_add(1, AtomicOrdering::Relaxed);
5588 }
5589 }
5590}
5591
5592fn record_result_row_metrics(row: &[SqliteValue]) {
5593 FSQLITE_VDBE_RESULT_ROWS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5594 let row_heap_bytes = row.iter().fold(0_u64, |acc, value| {
5595 acc.saturating_add(estimated_value_heap_bytes(value))
5596 });
5597 FSQLITE_VDBE_RESULT_VALUE_HEAP_BYTES_TOTAL.fetch_add(row_heap_bytes, AtomicOrdering::Relaxed);
5598 let counters = ValueTypeMetricCounters {
5599 total: &FSQLITE_VDBE_RESULT_VALUES_TOTAL,
5600 nulls: &FSQLITE_VDBE_RESULT_NULLS_TOTAL,
5601 integers: &FSQLITE_VDBE_RESULT_INTEGERS_TOTAL,
5602 reals: &FSQLITE_VDBE_RESULT_REALS_TOTAL,
5603 texts: &FSQLITE_VDBE_RESULT_TEXTS_TOTAL,
5604 blobs: &FSQLITE_VDBE_RESULT_BLOBS_TOTAL,
5605 text_bytes: &FSQLITE_VDBE_RESULT_TEXT_BYTES_TOTAL,
5606 blob_bytes: &FSQLITE_VDBE_RESULT_BLOB_BYTES_TOTAL,
5607 };
5608 for value in row {
5609 record_value_type_metrics(value, &counters);
5610 }
5611}
5612
5613pub fn record_external_result_row_metrics(row: &[SqliteValue]) {
5616 if !vdbe_metrics_enabled() {
5617 return;
5618 }
5619
5620 let materialize_start = Instant::now();
5621 for value in row {
5622 record_decoded_value_metrics(value);
5623 }
5624 record_result_row_metrics(row);
5625 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
5626 u64::try_from(materialize_start.elapsed().as_nanos())
5627 .unwrap_or(u64::MAX)
5628 .max(1),
5629 AtomicOrdering::Relaxed,
5630 );
5631}
5632
5633pub fn record_external_insert_path_metric(append_fast_path: bool) {
5636 if !vdbe_metrics_enabled() {
5637 return;
5638 }
5639
5640 if append_fast_path {
5641 FSQLITE_VDBE_INSERT_APPEND_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5642 } else {
5643 FSQLITE_VDBE_INSERT_SEEK_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5644 }
5645}
5646
5647pub fn record_external_insert_append_hint_clear_metric() {
5649 if !vdbe_metrics_enabled() {
5650 return;
5651 }
5652
5653 FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT.fetch_add(1, AtomicOrdering::Relaxed);
5654}
5655
5656fn record_type_coercion(before: &SqliteValue, after: &SqliteValue) {
5657 FSQLITE_VDBE_TYPE_COERCIONS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5658 if before.storage_class() != after.storage_class() {
5659 FSQLITE_VDBE_TYPE_COERCION_CHANGES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5660 }
5661}
5662
5663#[derive(Debug, Clone)]
5664enum JitDecision {
5665 Disabled,
5666 Warming {
5667 plan_hash: u64,
5668 execution_count: u64,
5669 },
5670 CacheHit {
5671 plan_hash: u64,
5672 code_size_bytes: u64,
5673 compiled_program: Arc<CompiledProgram>,
5674 },
5675 UnsupportedCached {
5676 plan_hash: u64,
5677 },
5678 Compiled {
5679 plan_hash: u64,
5680 compile_time_us: u64,
5681 code_size_bytes: u64,
5682 evicted_plan_hash: Option<u64>,
5683 compiled_program: Arc<CompiledProgram>,
5684 },
5685 CompileFailed {
5686 plan_hash: u64,
5687 compile_time_us: u64,
5688 reason: &'static str,
5689 },
5690}
5691
5692fn hash_program(program: &VdbeProgram) -> u64 {
5693 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
5694 const FNV_PRIME: u64 = 0x100000001b3;
5695
5696 fn mix(hash: &mut u64, bytes: &[u8]) {
5697 for byte in bytes {
5698 *hash ^= u64::from(*byte);
5699 *hash = hash.wrapping_mul(FNV_PRIME);
5700 }
5701 }
5702
5703 fn mix_len(hash: &mut u64, len: usize) {
5704 mix(hash, &u64::try_from(len).unwrap_or(u64::MAX).to_le_bytes());
5705 }
5706
5707 fn mix_p4(hash: &mut u64, p4: &P4) {
5708 match p4 {
5709 P4::None => mix(hash, &[0]),
5710 P4::Int(value) => {
5711 mix(hash, &[1]);
5712 mix(hash, &value.to_le_bytes());
5713 }
5714 P4::Int64(value) => {
5715 mix(hash, &[2]);
5716 mix(hash, &value.to_le_bytes());
5717 }
5718 P4::Real(value) => {
5719 mix(hash, &[3]);
5720 mix(hash, &value.to_bits().to_le_bytes());
5721 }
5722 P4::Str(value) => {
5723 mix(hash, &[4]);
5724 mix_len(hash, value.len());
5725 mix(hash, value.as_bytes());
5726 }
5727 P4::Blob(value) => {
5728 mix(hash, &[5]);
5729 mix_len(hash, value.len());
5730 mix(hash, value);
5731 }
5732 P4::Collation(value) => {
5733 mix(hash, &[6]);
5734 mix_len(hash, value.len());
5735 mix(hash, value.as_bytes());
5736 }
5737 P4::FuncName(value) => {
5738 mix(hash, &[7]);
5739 mix_len(hash, value.len());
5740 mix(hash, value.as_bytes());
5741 }
5742 P4::FuncNameCollated(value, coll) => {
5743 mix(hash, &[13]); mix_len(hash, value.len());
5745 mix(hash, value.as_bytes());
5746 mix_len(hash, coll.len());
5747 mix(hash, coll.as_bytes());
5748 }
5749 P4::Table(value) => {
5750 mix(hash, &[8]);
5751 mix_len(hash, value.len());
5752 mix(hash, value.as_bytes());
5753 }
5754 P4::Index(value) => {
5755 mix(hash, &[9]);
5756 mix_len(hash, value.len());
5757 mix(hash, value.as_bytes());
5758 }
5759 P4::Affinity(value) => {
5760 mix(hash, &[10]);
5761 mix_len(hash, value.len());
5762 mix(hash, value.as_bytes());
5763 }
5764 P4::TimeTravelCommitSeq(value) => {
5765 mix(hash, &[11]);
5766 mix(hash, &value.to_le_bytes());
5767 }
5768 P4::TimeTravelTimestamp(value) => {
5769 mix(hash, &[12]);
5770 mix_len(hash, value.len());
5771 mix(hash, value.as_bytes());
5772 }
5773 P4::PrecomputedHeader(value) => {
5774 mix(hash, &[14]);
5775 mix_len(hash, value.template.len());
5776 mix(hash, &value.template);
5777 mix_len(hash, value.slots.len());
5778 for slot in &value.slots {
5779 let kind = match slot.kind {
5780 PrecomputedSerialTypeKind::NullPlaceholder => 0_u8,
5781 PrecomputedSerialTypeKind::AnyOneByteVarintOrNull => 1_u8,
5782 PrecomputedSerialTypeKind::IntegerOrNull => 2_u8,
5783 PrecomputedSerialTypeKind::RealOrNull => 3_u8,
5784 };
5785 mix(hash, &[kind]);
5786 mix_len(hash, slot.header_offset);
5787 }
5788 }
5789 }
5790 }
5791
5792 let mut hash = FNV_OFFSET;
5793 mix(&mut hash, &program.register_count().to_le_bytes());
5794 for op in program.ops() {
5795 mix(&mut hash, &[op.opcode as u8]);
5796 mix(&mut hash, &op.p1.to_le_bytes());
5797 mix(&mut hash, &op.p2.to_le_bytes());
5798 mix(&mut hash, &op.p3.to_le_bytes());
5799 mix_p4(&mut hash, &op.p4);
5800 mix(&mut hash, &op.p5.to_le_bytes());
5801 }
5802 hash
5803}
5804
5805fn estimate_compiled_program_size(compiled_program: &CompiledProgram) -> u64 {
5806 match compiled_program {
5807 CompiledProgram::ConstantResultRow(template) => {
5808 let value_count = u64::try_from(template.values.len()).unwrap_or(u64::MAX);
5809 value_count.saturating_mul(24).saturating_add(64)
5810 }
5811 CompiledProgram::SimpleInsert(template) => {
5812 let value_count = u64::try_from(template.value_sources.len()).unwrap_or(u64::MAX);
5813 let builder_bytes = match &template.record_builder {
5814 CompiledRecordBuilder::Generic => 0,
5815 CompiledRecordBuilder::PrecomputedHeader(header) => {
5816 u64::try_from(header.template.len() + header.slots.len() * 8)
5817 .unwrap_or(u64::MAX)
5818 }
5819 };
5820 value_count
5821 .saturating_mul(32)
5822 .saturating_add(96)
5823 .saturating_add(builder_bytes)
5824 }
5825 CompiledProgram::RowidLookupSelect(template) => {
5826 let col_count = u64::try_from(template.column_indices.len()).unwrap_or(u64::MAX);
5827 col_count.saturating_mul(24).saturating_add(80)
5828 }
5829 CompiledProgram::FullScanSelect(template) => {
5830 let col_count = u64::try_from(template.column_indices.len()).unwrap_or(u64::MAX);
5831 col_count.saturating_mul(24).saturating_add(64)
5832 }
5833 }
5834}
5835
5836fn compile_jit_program(
5837 program: &VdbeProgram,
5838) -> std::result::Result<(CompiledProgram, u64), &'static str> {
5839 let compiled_program =
5840 try_compile_program(program.ops()).ok_or("unsupported opcode in JIT scaffold compiler")?;
5841 let code_size_bytes = estimate_compiled_program_size(&compiled_program);
5842 Ok((compiled_program, code_size_bytes))
5843}
5844
5845fn maybe_trigger_jit(program: &VdbeProgram) -> JitDecision {
5846 if !vdbe_jit_enabled() {
5847 return JitDecision::Disabled;
5848 }
5849 let plan_hash = hash_program(program);
5850 let hot_threshold = vdbe_jit_hot_threshold();
5851 let cache_capacity = vdbe_jit_cache_capacity();
5852
5853 let mut runtime = lock_jit_runtime();
5854 let execution_count = {
5855 let count = runtime.executions_by_plan.entry(plan_hash).or_insert(0);
5856 *count = count.saturating_add(1);
5857 *count
5858 };
5859 if execution_count < hot_threshold {
5860 return JitDecision::Warming {
5861 plan_hash,
5862 execution_count,
5863 };
5864 }
5865
5866 FSQLITE_JIT_TRIGGERS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5867 if runtime.is_unsupported_plan(plan_hash) {
5868 return JitDecision::UnsupportedCached { plan_hash };
5869 }
5870 if let Some((code_size_bytes, compiled_program)) = runtime
5871 .cache
5872 .get(&plan_hash)
5873 .map(|entry| (entry.code_size_bytes, Arc::clone(&entry.compiled_program)))
5874 {
5875 FSQLITE_JIT_CACHE_HITS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5876 runtime.touch_lru(plan_hash);
5877 return JitDecision::CacheHit {
5878 plan_hash,
5879 code_size_bytes,
5880 compiled_program,
5881 };
5882 }
5883
5884 FSQLITE_JIT_CACHE_MISSES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5885 let compile_started = Instant::now();
5886 match compile_jit_program(program) {
5887 Ok((compiled_program, code_size_bytes)) => {
5888 FSQLITE_JIT_COMPILATIONS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5889 let compile_time_us =
5890 u64::try_from(compile_started.elapsed().as_micros()).unwrap_or(u64::MAX);
5891 let compiled_program = Arc::new(compiled_program);
5892 let evicted_plan_hash = runtime.insert_cache(
5893 plan_hash,
5894 JitCacheEntry {
5895 code_size_bytes,
5896 compiled_program: Arc::clone(&compiled_program),
5897 },
5898 cache_capacity,
5899 );
5900 JitDecision::Compiled {
5901 plan_hash,
5902 compile_time_us,
5903 code_size_bytes,
5904 evicted_plan_hash,
5905 compiled_program,
5906 }
5907 }
5908 Err(reason) => {
5909 FSQLITE_JIT_COMPILE_FAILURES_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
5910 let compile_time_us =
5911 u64::try_from(compile_started.elapsed().as_micros()).unwrap_or(u64::MAX);
5912 runtime.mark_unsupported_plan(plan_hash, cache_capacity.max(1));
5913 JitDecision::CompileFailed {
5914 plan_hash,
5915 compile_time_us,
5916 reason,
5917 }
5918 }
5919 }
5920}
5921
5922#[derive(Debug, Clone, PartialEq, Eq)]
5924pub enum ExecOutcome {
5925 Done,
5927 Error { code: i32, message: String },
5929}
5930
5931#[derive(Debug, Clone, PartialEq, Eq)]
5933pub struct ReplaceVictim {
5934 pub root_page: i32,
5935 pub values: Vec<SqliteValue>,
5936}
5937
5938type RegisterFile = smallvec::SmallVec<[SqliteValue; 32]>;
5944
5945type BindingStorage = smallvec::SmallVec<[SqliteValue; 8]>;
5947
5948type ResultRowStorage = Vec<smallvec::SmallVec<[SqliteValue; 16]>>;
5950
5951#[derive(Debug, Default)]
5952struct MakeRecordStatementLookaside {
5953 buf: Vec<u8>,
5954 sideband_reg: i32,
5955}
5956
5957impl MakeRecordStatementLookaside {
5958 fn prepare_for_statement(&mut self, estimated_capacity: usize) {
5959 if estimated_capacity > self.buf.capacity() {
5960 self.buf.reserve(estimated_capacity - self.buf.capacity());
5961 }
5962 self.reset();
5963 }
5964
5965 fn reset(&mut self) {
5966 self.buf.clear();
5967 self.sideband_reg = 0;
5968 }
5969
5970 fn clear_sideband_for_register(&mut self, register: i32) {
5971 if self.sideband_reg == register {
5972 self.reset();
5973 }
5974 }
5975
5976 #[must_use]
5977 fn sideband_is_armed_for(&self, register: i32) -> bool {
5978 self.sideband_reg == register && !self.buf.is_empty()
5979 }
5980
5981 #[must_use]
5982 fn armed_register(&self) -> Option<i32> {
5983 (self.sideband_reg != 0 && !self.buf.is_empty()).then_some(self.sideband_reg)
5984 }
5985
5986 fn disarm(&mut self) {
5987 self.sideband_reg = 0;
5988 }
5989
5990 fn replace_buf(&mut self, buf: Vec<u8>) {
5991 self.buf = buf;
5992 }
5993
5994 fn replace_cleared_buf(&mut self, mut buf: Vec<u8>) {
5995 buf.clear();
5996 self.buf = buf;
5997 }
5998
5999 fn arm_sideband(&mut self, register: i32) {
6000 self.sideband_reg = register;
6001 }
6002
6003 fn take_buf(&mut self) -> Vec<u8> {
6004 std::mem::take(&mut self.buf)
6005 }
6006
6007 #[cfg(test)]
6008 #[must_use]
6009 fn capacity(&self) -> usize {
6010 self.buf.capacity()
6011 }
6012
6013 #[cfg(test)]
6014 #[must_use]
6015 fn is_empty(&self) -> bool {
6016 self.buf.is_empty()
6017 }
6018
6019 #[must_use]
6020 fn as_slice(&self) -> &[u8] {
6021 self.buf.as_slice()
6022 }
6023}
6024
6025enum CompiledRecordWritePlan<'a> {
6026 PrecomputedHeader {
6027 values: &'a [SqliteValue],
6028 header: &'a fsqlite_types::record::PrecomputedRecordHeader,
6029 exact_size: usize,
6030 },
6031 Generic(Box<fsqlite_types::record::PlannedRecordSerialization<'a>>),
6032}
6033
6034impl CompiledRecordWritePlan<'_> {
6035 #[must_use]
6036 fn exact_size(&self) -> usize {
6037 match self {
6038 Self::PrecomputedHeader { exact_size, .. } => *exact_size,
6039 Self::Generic(plan) => plan.exact_size(),
6040 }
6041 }
6042
6043 #[allow(clippy::result_unit_err)]
6044 fn write_into_slice(self, dst: &mut [u8]) -> std::result::Result<(), ()> {
6045 match self {
6046 Self::PrecomputedHeader { values, header, .. } => {
6047 serialize_record_iter_with_precomputed_header_into_slice(values.iter(), header, dst)
6048 }
6049 Self::Generic(plan) => (*plan).write_into_slice(dst),
6050 }
6051 }
6052}
6053
6054fn build_compiled_record_write_plan<'a>(
6055 values: &'a [SqliteValue],
6056 record_builder: &'a CompiledRecordBuilder,
6057) -> CompiledRecordWritePlan<'a> {
6058 if let CompiledRecordBuilder::PrecomputedHeader(header) = record_builder
6059 && let Some(exact_size) =
6060 record_iter_with_precomputed_header_exact_size(values.iter(), header)
6061 {
6062 return CompiledRecordWritePlan::PrecomputedHeader {
6063 values,
6064 header,
6065 exact_size,
6066 };
6067 }
6068
6069 CompiledRecordWritePlan::Generic(Box::new(
6070 fsqlite_types::record::plan_record_iter_serialization(values.iter()),
6071 ))
6072}
6073
6074fn serialize_compiled_record_into_vec(
6075 values: &[SqliteValue],
6076 record_builder: &CompiledRecordBuilder,
6077 buf: &mut Vec<u8>,
6078) {
6079 if let CompiledRecordBuilder::PrecomputedHeader(header) = record_builder
6080 && serialize_record_iter_with_precomputed_header_into(values.iter(), header, buf)
6081 {
6082 return;
6083 }
6084
6085 fsqlite_types::record::serialize_record_iter_into(values.iter(), buf);
6086}
6087
6088#[allow(clippy::struct_excessive_bools)]
6094pub struct VdbeEngine {
6095 registers: RegisterFile,
6097 bindings: BindingStorage,
6099 execution_cx: Cx,
6101 page_size: PageSize,
6103 trace_opcodes: bool,
6105 collect_vdbe_metrics: bool,
6107 results: ResultRowStorage,
6109 cursors: CursorSlots<MemCursor>,
6112 sorters: CursorSlots<SorterCursor>,
6114 storage_cursors: CursorSlots<StorageCursor>,
6116 pending_next_after_delete: HashSet<i32>,
6119 storage_cursors_enabled: bool,
6121 retain_storage_cursors_on_close: bool,
6128 txn_page_io: Option<SharedTxnPageIo>,
6132 reject_mem_fallback: bool,
6137 db: Option<MemDatabase>,
6139 memdb_rows_loaded: bool,
6142 storage_cursor_memdb_count_shortcuts_safe: bool,
6146 func_registry: Option<Arc<FunctionRegistry>>,
6148 scalar_function_cache: HashMap<usize, ResolvedScalarFunction>,
6150 aggregate_function_cache: HashMap<usize, Arc<ErasedAggregateFunction>>,
6152 collation_registry: Arc<Mutex<CollationRegistry>>,
6154 cold_state: Option<Box<ColdVdbeState>>,
6156 schema_cookie: u32,
6160 last_compare_result: Option<Ordering>,
6162 changes: usize,
6164 replace_victims: Vec<ReplaceVictim>,
6166 last_insert_rowid: i64,
6168 last_insert_rowid_valid: bool,
6170 last_insert_cursor_id: Option<i32>,
6173 fk_counter: i64,
6175 autoincrement_seq_by_root_page: HashMap<i32, i64>,
6178 concurrent_rowid_allocator: Option<Arc<ConcurrentRowIdAllocator>>,
6180 concurrent_rowid_schema_epoch: SchemaEpoch,
6182 rowid_alias_col_by_root_page: Arc<HashMap<i32, usize>>,
6185 table_column_count_by_root_page: Arc<HashMap<i32, usize>>,
6188 first_not_null_non_ipk_col_by_root_page: Arc<HashMap<i32, usize>>,
6190 column_defaults_by_root_page: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
6194 index_desc_flags_by_root_page: Arc<HashMap<i32, Vec<bool>>>,
6196 index_collations_by_root_page: Arc<HashMap<i32, Vec<Option<String>>>>,
6198 cursor_root_pages: HashMap<i32, i32>,
6200 vtab_instances: SwissIndex<i32, Box<dyn VtabInstance>>,
6202 time_travel_cursors: HashMap<i32, TimeTravelMarker>,
6206 version_store: Option<Arc<VersionStore>>,
6210 time_travel_commit_log: Option<Arc<Mutex<CommitLog>>>,
6212 time_travel_gc_horizon: Option<CommitSeq>,
6214 table_index_meta: Arc<TableIndexMetaMap>,
6218 make_record_lookaside: MakeRecordStatementLookaside,
6220 collect_result_rows: bool,
6223 max_collected_result_rows: Option<usize>,
6228 statement_state_clean: bool,
6235 statement_cold_state: StatementColdState,
6238 dirty_root_pages: HashSet<i32>,
6244}
6245
6246type ResultRowCallback<'a> = dyn FnMut(smallvec::SmallVec<[SqliteValue; 16]>) -> Result<()> + 'a;
6247
6248#[derive(Debug, PartialEq, Eq)]
6249pub enum ExactResultRowOutcome {
6250 NoRows,
6251 Row(Box<smallvec::SmallVec<[SqliteValue; 16]>>),
6252 MultipleRows,
6253}
6254
6255#[derive(Debug, Clone)]
6258pub enum TimeTravelMarker {
6259 CommitSeq(u64),
6261 Timestamp(String),
6263}
6264
6265#[allow(dead_code)]
6270trait VtabInstance: Send + Sync {
6271 fn begin(&mut self, cx: &Cx) -> Result<()>;
6272 fn commit(&mut self, cx: &Cx) -> Result<()>;
6273 fn rollback(&mut self, cx: &Cx) -> Result<()>;
6274 fn destroy(&mut self, cx: &Cx) -> Result<()>;
6275 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
6276 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
6277 fn vtab_update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
6278}
6279
6280pub trait ErasedVtabCursor: Send {
6286 fn filter(
6287 &mut self,
6288 cx: &Cx,
6289 idx_num: i32,
6290 idx_str: Option<&str>,
6291 args: &[SqliteValue],
6292 ) -> Result<()>;
6293 fn next(&mut self, cx: &Cx) -> Result<()>;
6294 fn eof(&self) -> bool;
6295 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
6296 fn rowid(&self) -> Result<i64>;
6297}
6298
6299impl<C: fsqlite_func::vtab::VirtualTableCursor + 'static> ErasedVtabCursor for C {
6301 fn filter(
6302 &mut self,
6303 cx: &Cx,
6304 idx_num: i32,
6305 idx_str: Option<&str>,
6306 args: &[SqliteValue],
6307 ) -> Result<()> {
6308 fsqlite_func::vtab::VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
6309 }
6310 fn next(&mut self, cx: &Cx) -> Result<()> {
6311 fsqlite_func::vtab::VirtualTableCursor::next(self, cx)
6312 }
6313 fn eof(&self) -> bool {
6314 fsqlite_func::vtab::VirtualTableCursor::eof(self)
6315 }
6316 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
6317 fsqlite_func::vtab::VirtualTableCursor::column(self, ctx, col)
6318 }
6319 fn rowid(&self) -> Result<i64> {
6320 fsqlite_func::vtab::VirtualTableCursor::rowid(self)
6321 }
6322}
6323
6324struct ErasedVtabBridge(Box<dyn fsqlite_func::vtab::ErasedVtabInstance>);
6326
6327impl VtabInstance for ErasedVtabBridge {
6328 fn begin(&mut self, cx: &Cx) -> Result<()> {
6329 self.0.begin(cx)
6330 }
6331 fn commit(&mut self, cx: &Cx) -> Result<()> {
6332 self.0.commit(cx)
6333 }
6334 fn rollback(&mut self, cx: &Cx) -> Result<()> {
6335 self.0.rollback(cx)
6336 }
6337 fn destroy(&mut self, cx: &Cx) -> Result<()> {
6338 self.0.destroy(cx)
6339 }
6340 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
6341 self.0.rename(cx, new_name)
6342 }
6343 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
6344 let func_cursor = self.0.open_cursor()?;
6345 Ok(Box::new(FuncVtabCursorBridge(func_cursor)))
6346 }
6347 fn vtab_update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
6348 self.0.update(cx, args)
6349 }
6350}
6351
6352struct FuncVtabCursorBridge(Box<dyn fsqlite_func::vtab::ErasedVtabCursor>);
6354
6355impl ErasedVtabCursor for FuncVtabCursorBridge {
6356 fn filter(
6357 &mut self,
6358 cx: &Cx,
6359 idx_num: i32,
6360 idx_str: Option<&str>,
6361 args: &[SqliteValue],
6362 ) -> Result<()> {
6363 self.0.erased_filter(cx, idx_num, idx_str, args)
6364 }
6365 fn next(&mut self, cx: &Cx) -> Result<()> {
6366 self.0.erased_next(cx)
6367 }
6368 fn eof(&self) -> bool {
6369 self.0.erased_eof()
6370 }
6371 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
6372 self.0.erased_column(ctx, col)
6373 }
6374 fn rowid(&self) -> Result<i64> {
6375 self.0.erased_rowid()
6376 }
6377}
6378
6379struct VtabCursorState {
6381 cursor: Box<dyn ErasedVtabCursor>,
6383 null_row: bool,
6385}
6386
6387struct RowSet {
6392 entries: Vec<i64>,
6394 read_pos: usize,
6396}
6397
6398impl RowSet {
6399 fn new() -> Self {
6400 Self {
6401 entries: Vec::new(),
6402 read_pos: 0,
6403 }
6404 }
6405
6406 fn add(&mut self, rowid: i64) {
6408 match self.entries.binary_search(&rowid) {
6409 Ok(_) => {} Err(pos) => self.entries.insert(pos, rowid),
6411 }
6412 }
6413
6414 fn read_next(&mut self) -> Option<i64> {
6416 if self.read_pos < self.entries.len() {
6417 let val = self.entries[self.read_pos];
6418 self.read_pos += 1;
6419 Some(val)
6420 } else {
6421 None
6422 }
6423 }
6424
6425 fn contains(&self, rowid: i64) -> bool {
6427 self.entries.binary_search(&rowid).is_ok()
6428 }
6429}
6430
6431type DistinctKeyBuf = smallvec::SmallVec<[u8; 64]>;
6433
6434struct AggregateContext {
6435 func: Arc<ErasedAggregateFunction>,
6436 state: Box<dyn Any + Send>,
6437 distinct_seen: Option<std::collections::HashSet<DistinctKeyBuf>>,
6439}
6440
6441struct AggStepCall<'a> {
6442 accum_reg: i32,
6443 is_distinct: bool,
6444 func: &'a Arc<ErasedAggregateFunction>,
6445 func_name: &'a str,
6446 agg_collation: Option<&'a str>,
6447 execution_cx: &'a Cx,
6448 args: &'a [SqliteValue],
6449}
6450
6451#[derive(Debug, Clone)]
6454enum PendingUpdateRestore {
6455 Storage {
6456 cursor_id: i32,
6457 rowid: i64,
6458 payload: Vec<u8>,
6459 },
6460 Mem {
6461 root_page: i32,
6462 rowid: i64,
6463 values: MemRowValues,
6464 },
6465}
6466
6467#[derive(Debug, Clone)]
6468struct PendingInsertRollback {
6469 cursor_id: i32,
6470 rowid: i64,
6471 previous_last_insert_rowid: i64,
6472 previous_last_insert_rowid_valid: bool,
6473 update_restore: Option<PendingUpdateRestore>,
6474}
6475
6476struct WindowContext {
6494 func: Arc<ErasedWindowFunction>,
6495 state: Box<dyn Any + Send>,
6496}
6497
6498struct ColdVdbeState {
6499 aggregates: SwissIndex<i32, AggregateContext>,
6501 pending_update_restore: Option<PendingUpdateRestore>,
6504 pending_insert_rollback: Option<PendingInsertRollback>,
6508 conflict_skip_idx: bool,
6512 pending_idx_entries: Vec<(i32, Vec<u8>)>,
6516 rowsets: SwissIndex<i32, RowSet>,
6518 sequence_counters: HashMap<i32, i64>,
6520 vtab_cursors: SwissIndex<i32, VtabCursorState>,
6522 window_contexts: SwissIndex<i32, WindowContext>,
6524 register_subtypes: HashMap<i32, u32>,
6528 has_subtypes: bool,
6530 bloom_filters: HashMap<i32, Vec<u64>>,
6534}
6535
6536impl ColdVdbeState {
6537 fn new() -> Self {
6538 Self {
6539 aggregates: SwissIndex::new(),
6540 pending_update_restore: None,
6541 pending_insert_rollback: None,
6542 conflict_skip_idx: false,
6543 pending_idx_entries: Vec::new(),
6544 rowsets: SwissIndex::new(),
6545 sequence_counters: HashMap::new(),
6546 vtab_cursors: SwissIndex::new(),
6547 window_contexts: SwissIndex::new(),
6548 register_subtypes: HashMap::new(),
6549 has_subtypes: false,
6550 bloom_filters: HashMap::new(),
6551 }
6552 }
6553}
6554
6555fn distinct_key_collated(args: &[SqliteValue], collation: Option<&str>) -> DistinctKeyBuf {
6563 let is_nocase = collation.is_some_and(|c| c.eq_ignore_ascii_case("NOCASE"));
6564 let is_rtrim = collation.is_some_and(|c| c.eq_ignore_ascii_case("RTRIM"));
6565 let mut key = DistinctKeyBuf::new();
6566 for val in args {
6567 match val {
6568 SqliteValue::Null => key.push(0),
6569 SqliteValue::Integer(i) => {
6570 key.push(1);
6571 key.extend_from_slice(&i.to_le_bytes());
6572 }
6573 SqliteValue::Float(f) => {
6574 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(f) {
6575 #[allow(clippy::cast_possible_truncation)]
6576 let i = *f as i64;
6577 #[allow(clippy::cast_precision_loss)]
6578 if (i as f64) == *f {
6579 key.push(1);
6580 key.extend_from_slice(&i.to_le_bytes());
6581 continue;
6582 }
6583 }
6584 key.push(2);
6585 key.extend_from_slice(&f.to_bits().to_le_bytes());
6586 }
6587 SqliteValue::Text(s) => {
6588 key.push(3);
6589 let text_bytes = s.as_bytes_direct();
6590 if is_nocase {
6591 #[allow(clippy::cast_possible_truncation)]
6592 key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
6593 key.extend(text_bytes.iter().map(u8::to_ascii_lowercase));
6594 } else if is_rtrim {
6595 let trimmed = trim_rtrim_collation_text(text_bytes);
6596 #[allow(clippy::cast_possible_truncation)]
6597 key.extend_from_slice(&(trimmed.len() as u64).to_le_bytes());
6598 key.extend_from_slice(trimmed);
6599 } else {
6600 #[allow(clippy::cast_possible_truncation)]
6601 key.extend_from_slice(&(text_bytes.len() as u64).to_le_bytes());
6602 key.extend_from_slice(text_bytes);
6603 }
6604 }
6605 SqliteValue::Blob(b) => {
6606 key.push(4);
6607 #[allow(clippy::cast_possible_truncation)]
6608 key.extend_from_slice(&(b.len() as u64).to_le_bytes());
6609 key.extend_from_slice(b);
6610 }
6611 }
6612 }
6613 key
6614}
6615
6616fn trim_rtrim_collation_text(text: &[u8]) -> &[u8] {
6617 let mut end = text.len();
6618 while end > 0 && text[end - 1] == b' ' {
6619 end -= 1;
6620 }
6621 &text[..end]
6622}
6623
6624fn sqlite_substr_prefix_value(value: &SqliteValue, prefix_len: usize) -> SqliteValue {
6625 match value {
6626 SqliteValue::Null => SqliteValue::Null,
6627 SqliteValue::Blob(bytes) => {
6628 let end = prefix_len.min(bytes.len());
6629 SqliteValue::Blob(Arc::from(&bytes[..end]))
6630 }
6631 SqliteValue::Text(text) => {
6632 sqlite_substr_prefix_text(Cow::Borrowed(text.as_str()), prefix_len)
6633 }
6634 SqliteValue::Integer(_) | SqliteValue::Float(_) => {
6635 sqlite_substr_prefix_text(Cow::Owned(value.to_text()), prefix_len)
6636 }
6637 }
6638}
6639
6640fn sqlite_octet_length_value(value: &SqliteValue) -> SqliteValue {
6641 if value.is_null() {
6642 return SqliteValue::Null;
6643 }
6644 let len = match value {
6645 SqliteValue::Text(text) => text.len(),
6646 SqliteValue::Blob(bytes) => bytes.len(),
6647 SqliteValue::Integer(_) | SqliteValue::Float(_) => value.to_text().len(),
6648 SqliteValue::Null => unreachable!("NULL returned above"),
6649 };
6650 SqliteValue::Integer(i64::try_from(len).unwrap_or(i64::MAX))
6651}
6652
6653fn sqlite_substr_prefix_text(text: Cow<'_, str>, prefix_len: usize) -> SqliteValue {
6654 let text = text.as_ref();
6655 let end = if text.is_ascii() {
6656 prefix_len.min(text.len())
6657 } else if prefix_len == 0 {
6658 0
6659 } else {
6660 text.char_indices()
6661 .nth(prefix_len)
6662 .map_or(text.len(), |(idx, _)| idx)
6663 };
6664 SqliteValue::Text(SmallText::new(&text[..end]))
6665}
6666
6667impl VdbeEngine {
6668 #[cfg(test)]
6674 #[must_use]
6675 pub fn new(register_count: i32) -> Self {
6676 let detached_execution_cx = Cx::new();
6677 Self::new_with_execution_cx(register_count, &detached_execution_cx, PageSize::DEFAULT)
6678 }
6679
6680 #[must_use]
6682 #[allow(clippy::cast_sign_loss)]
6683 pub fn new_with_execution_cx(
6684 register_count: i32,
6685 execution_cx: &Cx,
6686 page_size: PageSize,
6687 ) -> Self {
6688 let count = register_count.max(0) as u32 + 1;
6690 Self {
6691 registers: smallvec::smallvec![SqliteValue::Null; count as usize],
6692 bindings: smallvec::SmallVec::new(),
6693 execution_cx: execution_cx.clone(),
6697 page_size,
6698 trace_opcodes: opcode_trace_enabled(),
6699 collect_vdbe_metrics: false,
6700 results: Vec::with_capacity(64),
6701 cursors: CursorSlots::new(),
6702 sorters: CursorSlots::new(),
6703 storage_cursors: CursorSlots::new(),
6704 pending_next_after_delete: HashSet::new(),
6705 storage_cursors_enabled: true,
6706 retain_storage_cursors_on_close: false,
6707 txn_page_io: None,
6708 reject_mem_fallback: true,
6710 db: None,
6711 memdb_rows_loaded: false,
6712 storage_cursor_memdb_count_shortcuts_safe: false,
6713 func_registry: None,
6714 scalar_function_cache: HashMap::new(),
6715 aggregate_function_cache: HashMap::new(),
6716 collation_registry: Arc::new(Mutex::new(CollationRegistry::new())),
6717 cold_state: None,
6718 schema_cookie: 0,
6719 last_compare_result: None,
6720 changes: 0,
6721 replace_victims: Vec::new(),
6722 last_insert_rowid: 0,
6723 last_insert_rowid_valid: false,
6724 last_insert_cursor_id: None,
6725 fk_counter: 0,
6726 autoincrement_seq_by_root_page: HashMap::new(),
6727 concurrent_rowid_allocator: None,
6728 concurrent_rowid_schema_epoch: SchemaEpoch::ZERO,
6729 rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
6730 table_column_count_by_root_page: Arc::new(HashMap::new()),
6731 first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
6732 column_defaults_by_root_page: Arc::new(HashMap::new()),
6733 index_desc_flags_by_root_page: Arc::new(HashMap::new()),
6734 index_collations_by_root_page: Arc::new(HashMap::new()),
6735 cursor_root_pages: HashMap::new(),
6736 vtab_instances: SwissIndex::new(),
6737 time_travel_cursors: HashMap::new(),
6738 version_store: None,
6739 time_travel_commit_log: None,
6740 time_travel_gc_horizon: None,
6741 table_index_meta: Arc::new(HashMap::new()),
6742 make_record_lookaside: MakeRecordStatementLookaside::default(),
6743 collect_result_rows: true,
6744 max_collected_result_rows: None,
6745 statement_state_clean: true,
6746 statement_cold_state: StatementColdState::empty(),
6747 dirty_root_pages: HashSet::new(),
6748 }
6749 }
6750
6751 #[inline]
6752 fn mark_statement_cold_state(&mut self, state: StatementColdState) {
6753 self.statement_cold_state.insert(state);
6754 }
6755
6756 #[inline]
6757 fn cold_state(&self) -> Option<&ColdVdbeState> {
6758 self.cold_state.as_deref()
6759 }
6760
6761 #[inline]
6762 fn cold_state_mut(&mut self) -> Option<&mut ColdVdbeState> {
6763 self.cold_state.as_deref_mut()
6764 }
6765
6766 #[inline]
6767 fn ensure_cold_state(&mut self) -> &mut ColdVdbeState {
6768 self.cold_state
6769 .get_or_insert_with(|| Box::new(ColdVdbeState::new()))
6770 .as_mut()
6771 }
6772
6773 #[inline]
6774 fn ensure_cold_state_for(&mut self, state: StatementColdState) -> &mut ColdVdbeState {
6775 self.mark_statement_cold_state(state);
6776 self.ensure_cold_state()
6777 }
6778
6779 #[cfg(test)]
6780 #[inline]
6781 fn has_cold_state_allocated(&self) -> bool {
6782 self.cold_state.is_some()
6783 }
6784
6785 #[inline]
6786 fn clear_register_subtype(&mut self, r: i32) {
6787 if let Some(cold_state) = self.cold_state_mut()
6788 && cold_state.has_subtypes
6789 {
6790 cold_state.register_subtypes.remove(&r);
6791 if cold_state.register_subtypes.is_empty() {
6792 cold_state.has_subtypes = false;
6793 }
6794 }
6795 }
6796
6797 #[inline]
6798 fn register_subtype(&self, r: i32) -> Option<u32> {
6799 self.cold_state()
6800 .and_then(|cold_state| cold_state.register_subtypes.get(&r).copied())
6801 }
6802
6803 #[inline]
6804 fn set_register_subtype(&mut self, r: i32, subtype: u32) {
6805 if subtype == 0 {
6806 self.clear_register_subtype(r);
6807 return;
6808 }
6809 let cold_state = self.ensure_cold_state_for(StatementColdState::REGISTER_SUBTYPES);
6810 cold_state.register_subtypes.insert(r, subtype);
6811 cold_state.has_subtypes = true;
6812 }
6813
6814 #[inline]
6815 fn take_pending_update_restore(&mut self) -> Option<PendingUpdateRestore> {
6816 self.cold_state_mut()
6817 .and_then(|cold_state| cold_state.pending_update_restore.take())
6818 }
6819
6820 #[inline]
6821 fn set_pending_update_restore(&mut self, restore: Option<PendingUpdateRestore>) {
6822 if let Some(restore) = restore {
6823 self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6824 .pending_update_restore = Some(restore);
6825 } else if let Some(cold_state) = self.cold_state_mut() {
6826 cold_state.pending_update_restore = None;
6827 }
6828 }
6829
6830 #[inline]
6831 fn take_pending_insert_rollback(&mut self) -> Option<PendingInsertRollback> {
6832 self.cold_state_mut()
6833 .and_then(|cold_state| cold_state.pending_insert_rollback.take())
6834 }
6835
6836 #[inline]
6837 fn set_pending_insert_rollback(&mut self, rollback: Option<PendingInsertRollback>) {
6838 if let Some(rollback) = rollback {
6839 self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6840 .pending_insert_rollback = Some(rollback);
6841 } else if let Some(cold_state) = self.cold_state_mut() {
6842 cold_state.pending_insert_rollback = None;
6843 }
6844 }
6845
6846 #[inline]
6847 fn take_pending_idx_entries(&mut self) -> Vec<(i32, Vec<u8>)> {
6848 self.cold_state_mut().map_or_else(Vec::new, |cold_state| {
6849 std::mem::take(&mut cold_state.pending_idx_entries)
6850 })
6851 }
6852
6853 #[inline]
6854 fn clear_pending_idx_entries(&mut self) {
6855 if let Some(cold_state) = self.cold_state_mut() {
6856 cold_state.pending_idx_entries.clear();
6857 }
6858 }
6859
6860 #[inline]
6861 fn push_pending_idx_entry(&mut self, cursor_id: i32, key_bytes: Vec<u8>) {
6862 self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6863 .pending_idx_entries
6864 .push((cursor_id, key_bytes));
6865 }
6866
6867 #[inline]
6868 fn conflict_skip_idx(&self) -> bool {
6869 self.cold_state()
6870 .is_some_and(|cold_state| cold_state.conflict_skip_idx)
6871 }
6872
6873 #[inline]
6874 fn set_conflict_skip_idx(&mut self, skip: bool) {
6875 if skip {
6876 self.ensure_cold_state_for(StatementColdState::CONFLICT_TRACKING)
6877 .conflict_skip_idx = true;
6878 } else if let Some(cold_state) = self.cold_state_mut() {
6879 cold_state.conflict_skip_idx = false;
6880 }
6881 }
6882
6883 fn clear_statement_cold_state(&mut self) {
6884 if self.statement_cold_state.is_empty() && self.cold_state.is_none() {
6885 return;
6886 }
6887 self.cold_state = None;
6888 self.statement_cold_state.clear();
6889 }
6890
6891 fn reset_for_reuse_impl(
6892 &mut self,
6893 register_count: i32,
6894 execution_cx: &Cx,
6895 page_size: PageSize,
6896 retain_cursors: bool,
6897 preserve_runtime_setup: bool,
6898 ) {
6899 let count = register_count.max(0) as u32 + 1;
6900 self.registers.clear();
6902 #[allow(clippy::cast_possible_truncation)]
6903 self.registers.resize(count as usize, SqliteValue::Null);
6904 self.bindings.clear();
6905 self.execution_cx = execution_cx.clone();
6906 self.page_size = page_size;
6907 self.collect_vdbe_metrics = false;
6909 self.results.clear();
6910 if retain_cursors {
6911 self.clear_retained_storage_cursor_statement_state();
6916 self.sorters.clear();
6917 self.pending_next_after_delete.clear();
6918 } else {
6919 self.cursors.clear();
6920 self.sorters.clear();
6921 self.storage_cursors.clear();
6922 self.pending_next_after_delete.clear();
6923 self.storage_cursors_enabled = true;
6924 self.txn_page_io = None;
6925 }
6926 self.retain_storage_cursors_on_close = retain_cursors;
6927 if !retain_cursors {
6928 self.db = None;
6929 self.cursor_root_pages.clear();
6930 self.vtab_instances.clear();
6931 self.time_travel_cursors.clear();
6932 self.time_travel_commit_log = None;
6933 self.time_travel_gc_horizon = None;
6934 }
6935 if !preserve_runtime_setup {
6936 self.func_registry = None;
6937 }
6938 self.scalar_function_cache.clear();
6945 self.aggregate_function_cache.clear();
6946 self.clear_statement_cold_state();
6948 if !preserve_runtime_setup {
6949 self.schema_cookie = 0;
6950 }
6951 self.last_compare_result = None;
6952 self.changes = 0;
6953 self.replace_victims.clear();
6954 self.last_insert_rowid = 0;
6955 self.last_insert_rowid_valid = false;
6956 self.last_insert_cursor_id = None;
6957 self.fk_counter = 0;
6958 if !preserve_runtime_setup {
6959 self.autoincrement_seq_by_root_page.clear();
6960 }
6961 if !retain_cursors {
6962 self.concurrent_rowid_allocator = None;
6963 self.concurrent_rowid_schema_epoch = SchemaEpoch::ZERO;
6964 }
6965 self.make_record_lookaside.reset();
6968 if !preserve_runtime_setup {
6969 self.reject_mem_fallback = true;
6970 self.memdb_rows_loaded = false;
6971 self.storage_cursor_memdb_count_shortcuts_safe = false;
6972 self.version_store = None;
6973 self.collect_result_rows = true;
6974 self.max_collected_result_rows = None;
6975 }
6976 self.statement_state_clean = true;
6977 }
6978
6979 pub fn reset_for_reuse_ex(
6995 &mut self,
6996 register_count: i32,
6997 execution_cx: &Cx,
6998 page_size: PageSize,
6999 retain_cursors: bool,
7000 ) {
7001 self.reset_for_reuse_impl(
7002 register_count,
7003 execution_cx,
7004 page_size,
7005 retain_cursors,
7006 false,
7007 );
7008 }
7009
7010 pub fn reset_for_reuse_preserving_runtime_setup(
7016 &mut self,
7017 register_count: i32,
7018 execution_cx: &Cx,
7019 page_size: PageSize,
7020 retain_cursors: bool,
7021 ) {
7022 self.reset_for_reuse_impl(
7023 register_count,
7024 execution_cx,
7025 page_size,
7026 retain_cursors,
7027 true,
7028 );
7029 }
7030
7031 pub fn reset_for_reuse(&mut self, register_count: i32, execution_cx: &Cx, page_size: PageSize) {
7033 self.reset_for_reuse_ex(register_count, execution_cx, page_size, false);
7034 }
7035
7036 pub fn set_retain_storage_cursors_on_close(&mut self, retain: bool) {
7038 self.retain_storage_cursors_on_close = retain;
7039 }
7040
7041 fn clear_storage_cursor_statement_state(sc: &mut StorageCursor) {
7042 sc.last_alloc_rowid = 0;
7047 }
7048
7049 fn clear_retained_storage_cursor_statement_state(&mut self) {
7050 for sc in self.storage_cursors.values_mut() {
7051 Self::clear_storage_cursor_statement_state(sc);
7052 }
7053 }
7054
7055 pub fn apply_reusable_table_execution_state(
7056 &mut self,
7057 state: ReusableTableExecutionState,
7058 ) -> ReusableTableExecutionStateOutcome {
7059 let ReusableTableExecutionState {
7060 func_registry,
7061 collation_registry,
7062 schema_cookie,
7063 autoincrement_seq_by_root_page,
7064 rowid_alias_col_by_root_page,
7065 table_column_count_by_root_page,
7066 first_not_null_non_ipk_col_by_root_page,
7067 column_defaults_by_root_page,
7068 index_desc_flags_by_root_page,
7069 index_collations_by_root_page,
7070 reject_mem_fallback,
7071 memdb_rows_loaded,
7072 storage_cursor_memdb_count_shortcuts_safe,
7073 version_store,
7074 collect_result_rows,
7075 max_collected_result_rows,
7076 } = state;
7077
7078 let mut outcome = ReusableTableExecutionStateOutcome::default();
7079 let mut note_rebind = |changed: bool| {
7080 if changed {
7081 outcome.metadata_rebind_count += 1;
7082 }
7083 };
7084
7085 let func_registry_changed = self
7086 .func_registry
7087 .as_ref()
7088 .is_none_or(|current| !Arc::ptr_eq(current, &func_registry));
7089 if func_registry_changed {
7090 self.func_registry = Some(func_registry);
7091 self.scalar_function_cache.clear();
7095 self.aggregate_function_cache.clear();
7096 outcome.function_cache_cleared = true;
7097 }
7098 note_rebind(func_registry_changed);
7099
7100 let collation_registry_changed =
7101 !Arc::ptr_eq(&self.collation_registry, &collation_registry);
7102 if collation_registry_changed {
7103 self.set_collation_registry(collation_registry);
7104 }
7105 note_rebind(collation_registry_changed);
7106
7107 let schema_cookie_changed = self.schema_cookie != schema_cookie;
7108 if schema_cookie_changed {
7109 self.schema_cookie = schema_cookie;
7110 }
7111 note_rebind(schema_cookie_changed);
7112
7113 let autoincrement_seq_changed =
7114 self.autoincrement_seq_by_root_page != autoincrement_seq_by_root_page;
7115 if autoincrement_seq_changed {
7116 self.autoincrement_seq_by_root_page = autoincrement_seq_by_root_page;
7117 }
7118 note_rebind(autoincrement_seq_changed);
7119
7120 let rowid_alias_changed = !Arc::ptr_eq(
7121 &self.rowid_alias_col_by_root_page,
7122 &rowid_alias_col_by_root_page,
7123 );
7124 if rowid_alias_changed {
7125 self.rowid_alias_col_by_root_page = rowid_alias_col_by_root_page;
7126 }
7127 note_rebind(rowid_alias_changed);
7128
7129 let table_column_count_changed = !Arc::ptr_eq(
7130 &self.table_column_count_by_root_page,
7131 &table_column_count_by_root_page,
7132 );
7133 if table_column_count_changed {
7134 self.table_column_count_by_root_page = table_column_count_by_root_page;
7135 }
7136 note_rebind(table_column_count_changed);
7137
7138 let first_not_null_changed = !Arc::ptr_eq(
7139 &self.first_not_null_non_ipk_col_by_root_page,
7140 &first_not_null_non_ipk_col_by_root_page,
7141 );
7142 if first_not_null_changed {
7143 self.first_not_null_non_ipk_col_by_root_page = first_not_null_non_ipk_col_by_root_page;
7144 }
7145 note_rebind(first_not_null_changed);
7146
7147 let column_defaults_changed = !Arc::ptr_eq(
7148 &self.column_defaults_by_root_page,
7149 &column_defaults_by_root_page,
7150 );
7151 if column_defaults_changed {
7152 self.column_defaults_by_root_page = column_defaults_by_root_page;
7153 }
7154 note_rebind(column_defaults_changed);
7155
7156 let index_desc_flags_changed = !Arc::ptr_eq(
7157 &self.index_desc_flags_by_root_page,
7158 &index_desc_flags_by_root_page,
7159 );
7160 if index_desc_flags_changed {
7161 self.index_desc_flags_by_root_page = index_desc_flags_by_root_page;
7162 }
7163 note_rebind(index_desc_flags_changed);
7164
7165 let index_collations_changed = !Arc::ptr_eq(
7166 &self.index_collations_by_root_page,
7167 &index_collations_by_root_page,
7168 );
7169 if index_collations_changed {
7170 self.index_collations_by_root_page = index_collations_by_root_page;
7171 }
7172 note_rebind(index_collations_changed);
7173
7174 let reject_mem_fallback_changed = self.reject_mem_fallback != reject_mem_fallback;
7175 if reject_mem_fallback_changed {
7176 self.reject_mem_fallback = reject_mem_fallback;
7177 }
7178 note_rebind(reject_mem_fallback_changed);
7179
7180 let memdb_rows_loaded_changed = self.memdb_rows_loaded != memdb_rows_loaded;
7181 if memdb_rows_loaded_changed {
7182 self.memdb_rows_loaded = memdb_rows_loaded;
7183 }
7184 note_rebind(memdb_rows_loaded_changed);
7185
7186 let storage_shortcuts_changed = self.storage_cursor_memdb_count_shortcuts_safe
7187 != storage_cursor_memdb_count_shortcuts_safe;
7188 if storage_shortcuts_changed {
7189 self.storage_cursor_memdb_count_shortcuts_safe =
7190 storage_cursor_memdb_count_shortcuts_safe;
7191 }
7192 note_rebind(storage_shortcuts_changed);
7193
7194 let version_store_changed = match (&self.version_store, &version_store) {
7195 (Some(current), Some(next)) => !Arc::ptr_eq(current, next),
7196 (None, None) => false,
7197 _ => true,
7198 };
7199 if version_store_changed {
7200 self.version_store = version_store;
7201 }
7202 note_rebind(version_store_changed);
7203
7204 let collect_result_rows_changed = self.collect_result_rows != collect_result_rows;
7205 if collect_result_rows_changed {
7206 self.collect_result_rows = collect_result_rows;
7207 }
7208 note_rebind(collect_result_rows_changed);
7209
7210 let max_collected_result_rows_changed =
7211 self.max_collected_result_rows != max_collected_result_rows;
7212 if max_collected_result_rows_changed {
7213 self.max_collected_result_rows = max_collected_result_rows;
7214 }
7215 note_rebind(max_collected_result_rows_changed);
7216
7217 outcome
7218 }
7219
7220 pub fn changes(&self) -> usize {
7222 self.changes
7223 }
7224
7225 pub fn take_replace_victims(&mut self) -> Vec<ReplaceVictim> {
7228 std::mem::take(&mut self.replace_victims)
7229 }
7230
7231 pub fn last_insert_rowid(&self) -> Option<i64> {
7234 self.last_insert_rowid_valid
7235 .then_some(self.last_insert_rowid)
7236 }
7237
7238 pub fn set_collect_result_rows(&mut self, collect_result_rows: bool) {
7240 self.collect_result_rows = collect_result_rows;
7241 }
7242
7243 pub fn set_max_collected_result_rows(&mut self, max_collected_result_rows: Option<usize>) {
7245 self.max_collected_result_rows = max_collected_result_rows;
7246 }
7247
7248 pub fn time_travel_marker(&self, cursor_id: i32) -> Option<&TimeTravelMarker> {
7250 self.time_travel_cursors.get(&cursor_id)
7251 }
7252
7253 pub fn time_travel_cursors(&self) -> &HashMap<i32, TimeTravelMarker> {
7255 &self.time_travel_cursors
7256 }
7257
7258 pub fn set_version_store(&mut self, vs: Arc<VersionStore>) {
7264 self.version_store = Some(vs);
7265 }
7266
7267 pub fn set_time_travel_commit_log(&mut self, log: Arc<Mutex<CommitLog>>) {
7269 self.time_travel_commit_log = Some(log);
7270 }
7271
7272 pub fn set_time_travel_gc_horizon(&mut self, horizon: CommitSeq) {
7274 self.time_travel_gc_horizon = Some(horizon);
7275 }
7276
7277 pub fn set_execution_cx(&mut self, cx: Cx) {
7279 self.execution_cx = cx;
7280 }
7281
7282 fn derive_execution_cx(&self) -> Cx {
7283 self.execution_cx.clone()
7284 }
7285
7286 #[allow(clippy::too_many_arguments)]
7287 fn log_open_storage_cursor_fallback_decision(
7288 trace_id: u64,
7289 certifying_mode: bool,
7290 cursor_id: i32,
7291 root_page: i32,
7292 writable: bool,
7293 backend_kind: &'static str,
7294 decision_reason: &'static str,
7295 decision_outcome: &'static str,
7296 detail: &str,
7297 ) {
7298 let rejected = decision_outcome == "vdbe_mempage_fallback_rejected";
7299 let trace_enabled = if rejected {
7300 tracing::enabled!(target: "fsqlite.fallback_decision", tracing::Level::WARN)
7301 } else {
7302 tracing::enabled!(target: "fsqlite.fallback_decision", tracing::Level::DEBUG)
7303 };
7304 if !trace_enabled {
7305 return;
7306 }
7307
7308 let mode = if certifying_mode {
7309 "parity_cert"
7310 } else {
7311 "fallback_allowed"
7312 };
7313 let statement_fingerprint =
7314 format!("vdbe-open-storage-cursor:{cursor_id}:{root_page}:{decision_reason}");
7315 let statement_kind = "vdbe_open_cursor";
7316 let backend_identity = format!("{backend_kind}:{mode}");
7317 let fallback_boundary = format!("vdbe.open_cursor.{decision_reason}");
7318 let source_touchpoint = "VdbeEngine::open_storage_cursor";
7319 let first_failure_diag = format!(
7320 "statement_kind={statement_kind}; fallback_boundary={fallback_boundary}; \
7321 source_touchpoint={source_touchpoint}; decision_reason={decision_reason}; \
7322 cursor_id={cursor_id}; root_page={root_page}; writable={writable}; detail={detail}"
7323 );
7324 let run_id = "(none)";
7325 let scenario_id = "(none)";
7326 let strict_mode = false;
7327 if rejected {
7328 tracing::warn!(
7329 target: "fsqlite.fallback_decision",
7330 trace_id,
7331 run_id,
7332 scenario_id,
7333 statement_fingerprint = %statement_fingerprint,
7334 statement_kind,
7335 backend_identity = %backend_identity,
7336 fallback_boundary = %fallback_boundary,
7337 decision_reason,
7338 certifying_mode,
7339 strict_mode,
7340 decision_outcome,
7341 source_touchpoint,
7342 first_failure_diag = %first_failure_diag,
7343 "fallback decision telemetry"
7344 );
7345 } else {
7346 tracing::debug!(
7347 target: "fsqlite.fallback_decision",
7348 trace_id,
7349 run_id,
7350 scenario_id,
7351 statement_fingerprint = %statement_fingerprint,
7352 statement_kind,
7353 backend_identity = %backend_identity,
7354 fallback_boundary = %fallback_boundary,
7355 decision_reason,
7356 certifying_mode,
7357 strict_mode,
7358 decision_outcome,
7359 source_touchpoint,
7360 first_failure_diag = %first_failure_diag,
7361 "fallback decision telemetry"
7362 );
7363 }
7364 }
7365
7366 fn index_desc_flags_for_root(&self, root_page: i32) -> Vec<bool> {
7367 self.index_desc_flags_by_root_page
7368 .get(&root_page)
7369 .cloned()
7370 .unwrap_or_default()
7371 }
7372
7373 fn index_collations_for_root(&self, root_page: i32) -> Vec<Option<String>> {
7374 self.index_collations_by_root_page
7375 .get(&root_page)
7376 .cloned()
7377 .unwrap_or_default()
7378 }
7379
7380 fn index_desc_flags_for_cursor(&self, cursor_id: i32) -> Vec<bool> {
7381 if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
7382 return cursor.cursor.index_desc_flags().to_vec();
7383 }
7384 let root_page = self
7385 .cursor_root_pages
7386 .get(&cursor_id)
7387 .copied()
7388 .unwrap_or_default();
7389 self.index_desc_flags_for_root(root_page)
7390 }
7391
7392 fn index_collations_for_cursor(&self, cursor_id: i32) -> Vec<Option<String>> {
7393 if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
7394 return cursor.cursor.index_collations().to_vec();
7395 }
7396 let root_page = self
7397 .cursor_root_pages
7398 .get(&cursor_id)
7399 .copied()
7400 .unwrap_or_default();
7401 self.index_collations_for_root(root_page)
7402 }
7403
7404 async fn storage_cursor_find_exact_index_key(
7405 &mut self,
7406 cursor_id: i32,
7407 key_bytes: &[u8],
7408 missing_registry_detail: &'static str,
7409 ) -> Result<bool> {
7410 let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
7411 let index_collations = self.index_collations_for_cursor(cursor_id);
7412 let uses_collated_probe = index_collations.iter().any(|collation| {
7413 collation
7414 .as_deref()
7415 .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
7416 });
7417 let collated_probe_registry = uses_collated_probe.then(|| {
7418 self.collation_registry
7419 .lock()
7420 .unwrap_or_else(|err| err.into_inner())
7421 .clone()
7422 });
7423
7424 let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
7425 return Ok(false);
7426 };
7427
7428 if uses_collated_probe {
7429 let Some(collation_registry) = collated_probe_registry.as_ref() else {
7430 return Err(FrankenError::internal(missing_registry_detail));
7431 };
7432 storage_cursor_exact_match_collated(
7433 cursor_id,
7434 cursor,
7435 key_bytes,
7436 &index_desc_flags,
7437 &index_collations,
7438 collation_registry,
7439 )
7440 .await
7441 } else {
7442 cursor
7443 .cursor
7444 .index_move_to(&cursor.cx, key_bytes)
7445 .await
7446 .map(|seek| seek.is_found())
7447 }
7448 }
7449
7450 fn table_root_page_for_cursor(&self, cursor_id: i32) -> Option<i32> {
7451 self.cursor_root_pages.get(&cursor_id).copied().or_else(|| {
7452 self.storage_cursors
7453 .get(&cursor_id)
7454 .map(|cursor| cursor.root_page)
7455 })
7456 }
7457
7458 fn logical_table_payload_value(
7459 &self,
7460 table_root_page: Option<i32>,
7461 payload_values: &[SqliteValue],
7462 rowid: i64,
7463 column_index: usize,
7464 ) -> SqliteValue {
7465 let rowid_alias_col_idx = table_root_page
7466 .and_then(|root_page| self.rowid_alias_col_by_root_page.get(&root_page))
7467 .copied();
7468 if rowid_alias_col_idx == Some(column_index) {
7469 return SqliteValue::Integer(rowid);
7470 }
7471
7472 let payload_includes_rowid_alias = rowid_alias_col_idx.is_some_and(|ipk_idx| {
7473 payload_includes_rowid_alias(
7474 payload_values,
7475 rowid,
7476 ipk_idx,
7477 table_root_page.and_then(|root_page| {
7478 self.table_column_count_by_root_page
7479 .get(&root_page)
7480 .copied()
7481 }),
7482 table_root_page.and_then(|root_page| {
7483 self.first_not_null_non_ipk_col_by_root_page
7484 .get(&root_page)
7485 .copied()
7486 }),
7487 )
7488 });
7489
7490 let payload_index = if let Some(ipk_idx) = rowid_alias_col_idx
7491 && column_index > ipk_idx
7492 && !payload_includes_rowid_alias
7493 {
7494 column_index - 1
7495 } else {
7496 column_index
7497 };
7498
7499 payload_values
7500 .get(payload_index)
7501 .cloned()
7502 .unwrap_or_else(|| {
7503 table_root_page
7504 .and_then(|root_page| self.column_defaults_by_root_page.get(&root_page))
7505 .and_then(|defaults| defaults.get(column_index))
7506 .and_then(|default| default.as_ref())
7507 .cloned()
7508 .unwrap_or(SqliteValue::Null)
7509 })
7510 }
7511
7512 fn index_key_values_from_table_payload(
7513 &self,
7514 table_root_page: Option<i32>,
7515 payload_values: &[SqliteValue],
7516 rowid: i64,
7517 column_indices: &[usize],
7518 ) -> Vec<SqliteValue> {
7519 let mut key_values = Vec::with_capacity(column_indices.len().saturating_add(1));
7520 for &col_idx in column_indices {
7521 key_values.push(self.logical_table_payload_value(
7522 table_root_page,
7523 payload_values,
7524 rowid,
7525 col_idx,
7526 ));
7527 }
7528 key_values.push(SqliteValue::Integer(rowid));
7529 key_values
7530 }
7531
7532 async fn delete_index_entry_for_rowid(&mut self, cursor_id: i32, rowid: i64) -> Result<()> {
7533 let Some(sc) = self.storage_cursors.get_mut(&cursor_id) else {
7534 return Ok(());
7535 };
7536 if !sc.writable || !sc.cursor.first(&sc.cx).await? {
7537 return Ok(());
7538 }
7539
7540 loop {
7541 let key = sc.cursor.payload(&sc.cx).await?;
7542 let values = parse_record(&key).ok_or_else(|| FrankenError::DatabaseCorrupt {
7543 detail: "REPLACE cleanup encountered a malformed secondary-index record".to_owned(),
7544 })?;
7545 let entry_rowid = values
7546 .last()
7547 .and_then(SqliteValue::as_integer)
7548 .ok_or_else(|| FrankenError::DatabaseCorrupt {
7549 detail:
7550 "REPLACE cleanup encountered a secondary-index record without a rowid suffix"
7551 .to_owned(),
7552 })?;
7553 if entry_rowid == rowid {
7554 sc.cursor.delete(&sc.cx).await?;
7555 invalidate_storage_cursor_row_cache_with_reason(
7556 sc,
7557 self.collect_vdbe_metrics,
7558 DecodeCacheInvalidationReason::WriteMutation,
7559 );
7560 return Ok(());
7561 }
7562 if !sc.cursor.next(&sc.cx).await? {
7563 return Ok(());
7564 }
7565 }
7566 }
7567
7568 fn logical_replace_victim(
7569 &self,
7570 root_page: i32,
7571 stored_values: &[SqliteValue],
7572 rowid: Option<i64>,
7573 ) -> ReplaceVictim {
7574 let column_count = self
7575 .table_column_count_by_root_page
7576 .get(&root_page)
7577 .copied()
7578 .unwrap_or(stored_values.len());
7579 let values = (0..column_count)
7580 .map(|column_index| {
7581 rowid.map_or_else(
7582 || {
7583 stored_values
7584 .get(column_index)
7585 .cloned()
7586 .or_else(|| {
7587 self.column_defaults_by_root_page
7588 .get(&root_page)
7589 .and_then(|defaults| defaults.get(column_index))
7590 .and_then(Clone::clone)
7591 })
7592 .unwrap_or(SqliteValue::Null)
7593 },
7594 |rowid| {
7595 self.logical_table_payload_value(
7596 Some(root_page),
7597 stored_values,
7598 rowid,
7599 column_index,
7600 )
7601 },
7602 )
7603 })
7604 .collect();
7605 ReplaceVictim { root_page, values }
7606 }
7607
7608 async fn native_replace_row(&mut self, tbl_cursor_id: i32, conflict_rowid: i64) -> Result<()> {
7611 let old_payload = if let Some(tsc) = self.storage_cursors.get_mut(&tbl_cursor_id) {
7612 if tsc
7613 .cursor
7614 .table_move_to(&tsc.cx, conflict_rowid)
7615 .await?
7616 .is_found()
7617 {
7618 Some(tsc.cursor.payload(&tsc.cx).await?)
7619 } else {
7620 None
7621 }
7622 } else {
7623 None
7624 };
7625
7626 let Some(payload) = old_payload else {
7627 return Ok(());
7628 };
7629
7630 let old_row = parse_record(&payload).ok_or_else(|| {
7632 FrankenError::internal("delete_secondary_index_entries: malformed table record")
7633 })?;
7634 let table_root_page = self.table_root_page_for_cursor(tbl_cursor_id);
7635 let replace_victim = table_root_page.map(|root_page| {
7636 self.logical_replace_victim(root_page, &old_row, Some(conflict_rowid))
7637 });
7638
7639 let table_index_meta = Arc::clone(&self.table_index_meta);
7643 if let Some(index_metas) = table_index_meta.get(&tbl_cursor_id) {
7644 for meta in index_metas.iter() {
7645 if meta.column_indices.is_empty() {
7646 self.delete_index_entry_for_rowid(meta.cursor_id, conflict_rowid)
7651 .await?;
7652 continue;
7653 }
7654 let key_values = self.index_key_values_from_table_payload(
7655 table_root_page,
7656 &old_row,
7657 conflict_rowid,
7658 &meta.column_indices,
7659 );
7660 let key_bytes = encode_record(&key_values);
7661
7662 let found =
7664 self.storage_cursor_find_exact_index_key(
7665 meta.cursor_id,
7666 &key_bytes,
7667 "delete_secondary_index_entries: missing collation registry for collated exact probe",
7668 )
7669 .await?;
7670 if found
7671 && let Some(sc) = self.storage_cursors.get_mut(&meta.cursor_id)
7672 && sc.writable
7673 {
7674 sc.cursor.delete(&sc.cx).await?;
7675 invalidate_storage_cursor_row_cache_with_reason(
7676 sc,
7677 self.collect_vdbe_metrics,
7678 DecodeCacheInvalidationReason::WriteMutation,
7679 );
7680 }
7681 }
7682 }
7683
7684 if let Some(tsc) = self.storage_cursors.get_mut(&tbl_cursor_id) {
7686 tsc.cursor.table_move_to(&tsc.cx, conflict_rowid).await?;
7687 tsc.cursor.delete(&tsc.cx).await?;
7688 invalidate_storage_cursor_row_cache_with_reason(
7689 tsc,
7690 self.collect_vdbe_metrics,
7691 DecodeCacheInvalidationReason::WriteMutation,
7692 );
7693 }
7694 self.sync_storage_table_delete_into_memdb_mirror(tbl_cursor_id, conflict_rowid);
7695 if let Some(replace_victim) = replace_victim {
7696 self.replace_victims.push(replace_victim);
7697 }
7698
7699 Ok(())
7700 }
7701
7702 async fn rollback_pending_insert_after_index_conflict(
7703 &mut self,
7704 require_pending_table_insert: bool,
7705 ) -> Result<()> {
7706 let entries = self.take_pending_idx_entries();
7707 for (idx_cid, idx_key) in entries {
7708 if self.storage_cursor_find_exact_index_key(
7709 idx_cid,
7710 &idx_key,
7711 "rollback_pending_insert_after_index_conflict: missing collation registry for collated exact probe",
7712 )
7713 .await?
7714 && let Some(isc) = self.storage_cursors.get_mut(&idx_cid)
7715 {
7716 isc.cursor.delete(&isc.cx).await?;
7717 invalidate_storage_cursor_row_cache_with_reason(
7718 isc,
7719 self.collect_vdbe_metrics,
7720 DecodeCacheInvalidationReason::WriteMutation,
7721 );
7722 }
7723 }
7724
7725 let Some(rollback) = self.take_pending_insert_rollback() else {
7726 if require_pending_table_insert {
7727 return Err(FrankenError::internal(
7728 "secondary-index conflict without pending table insert",
7729 ));
7730 }
7731 return Ok(());
7732 };
7733 let tsc = self
7734 .storage_cursors
7735 .get_mut(&rollback.cursor_id)
7736 .ok_or_else(|| {
7737 FrankenError::internal("table cursor missing during secondary-index rollback")
7738 })?;
7739 if !tsc
7740 .cursor
7741 .table_move_to(&tsc.cx, rollback.rowid)
7742 .await?
7743 .is_found()
7744 {
7745 return Err(FrankenError::internal(
7746 "failed to locate provisional table row during secondary-index rollback",
7747 ));
7748 }
7749 tsc.cursor.delete(&tsc.cx).await?;
7750 invalidate_storage_cursor_row_cache_with_reason(
7751 tsc,
7752 self.collect_vdbe_metrics,
7753 DecodeCacheInvalidationReason::WriteMutation,
7754 );
7755 let rollback_cursor_id = rollback.cursor_id;
7756 let rollback_rowid = rollback.rowid;
7757 self.changes = self.changes.checked_sub(1).ok_or_else(|| {
7758 FrankenError::internal("secondary-index rollback underflowed change counter")
7759 })?;
7760 self.sync_storage_table_delete_into_memdb_mirror(rollback_cursor_id, rollback_rowid);
7761 if let Some(update_restore) = rollback.update_restore {
7762 self.restore_pending_update_after_conflict(update_restore)
7763 .await?;
7764 }
7765 self.last_insert_rowid = rollback.previous_last_insert_rowid;
7766 self.last_insert_rowid_valid = rollback.previous_last_insert_rowid_valid;
7767 self.last_insert_cursor_id = None;
7768 Ok(())
7769 }
7770
7771 async fn restore_pending_update_after_conflict(
7772 &mut self,
7773 restore: PendingUpdateRestore,
7774 ) -> Result<()> {
7775 match restore {
7776 PendingUpdateRestore::Storage {
7777 cursor_id,
7778 rowid,
7779 payload,
7780 } => {
7781 let tsc = self.storage_cursors.get_mut(&cursor_id).ok_or_else(|| {
7782 FrankenError::internal("table cursor missing during UPDATE conflict restore")
7783 })?;
7784 tsc.cursor.table_insert(&tsc.cx, rowid, &payload).await?;
7785 invalidate_storage_cursor_row_cache_with_reason(
7786 tsc,
7787 self.collect_vdbe_metrics,
7788 DecodeCacheInvalidationReason::WriteMutation,
7789 );
7790
7791 let table_index_meta = Arc::clone(&self.table_index_meta);
7792 let table_root_page = self.table_root_page_for_cursor(cursor_id);
7793 if let Some(index_metas) = table_index_meta.get(&cursor_id) {
7794 let old_row = parse_record(&payload).ok_or_else(|| {
7795 FrankenError::internal(
7796 "UPDATE conflict restore could not decode original row payload",
7797 )
7798 })?;
7799 for meta in index_metas.iter() {
7800 if meta.column_indices.is_empty() {
7806 continue;
7807 }
7808 let key_values = self.index_key_values_from_table_payload(
7809 table_root_page,
7810 &old_row,
7811 rowid,
7812 &meta.column_indices,
7813 );
7814 let key_bytes = encode_record(&key_values);
7815 if let Some(sc) = self.storage_cursors.get_mut(&meta.cursor_id)
7816 && sc.writable
7817 {
7818 sc.cursor.index_insert(&sc.cx, &key_bytes).await?;
7819 invalidate_storage_cursor_row_cache_with_reason(
7820 sc,
7821 self.collect_vdbe_metrics,
7822 DecodeCacheInvalidationReason::WriteMutation,
7823 );
7824 }
7825 }
7826 }
7827 self.sync_storage_table_restore_into_memdb_mirror(cursor_id, rowid, &payload);
7828 }
7829 PendingUpdateRestore::Mem {
7830 root_page,
7831 rowid,
7832 values,
7833 } => {
7834 let db = self.db.as_mut().ok_or_else(|| {
7835 FrankenError::internal("MemDatabase missing during UPDATE conflict restore")
7836 })?;
7837 db.upsert_row(root_page, rowid, values);
7838 }
7839 }
7840 Ok(())
7841 }
7842
7843 pub fn set_database(&mut self, mut db: MemDatabase) {
7845 db.set_collation_registry(Arc::clone(&self.collation_registry));
7846 self.db = Some(db);
7847 }
7848
7849 pub fn set_memdb_rows_loaded(&mut self, loaded: bool) {
7852 self.memdb_rows_loaded = loaded;
7853 }
7854
7855 pub fn set_storage_cursor_memdb_count_shortcuts_safe(&mut self, safe: bool) {
7858 self.storage_cursor_memdb_count_shortcuts_safe = safe;
7859 }
7860
7861 pub fn storage_cursor_memdb_count_shortcuts_safe(&self) -> bool {
7864 self.storage_cursor_memdb_count_shortcuts_safe
7865 }
7866
7867 #[inline]
7871 fn mark_root_page_dirty(&mut self, root_page: i32) {
7872 self.dirty_root_pages.insert(root_page);
7873 }
7874
7875 #[inline]
7878 fn mark_storage_root_page_dirty(&mut self, root_page: i32) {
7879 self.mark_root_page_dirty(root_page);
7880 self.storage_cursor_memdb_count_shortcuts_safe = false;
7883 }
7884
7885 fn mark_storage_table_dirty(&mut self, cursor_id: i32) {
7890 if let Some(root_page) = self.cursor_root_pages.get(&cursor_id).copied() {
7891 self.mark_storage_root_page_dirty(root_page);
7892 }
7893 }
7894
7895 pub fn has_dirty_root_pages(&self) -> bool {
7897 !self.dirty_root_pages.is_empty()
7898 }
7899
7900 pub fn dirty_root_pages(&self) -> &HashSet<i32> {
7902 &self.dirty_root_pages
7903 }
7904
7905 pub fn clear_dirty_root_pages(&mut self) {
7907 self.dirty_root_pages.clear();
7908 }
7909
7910 fn sync_storage_table_delete_into_memdb_mirror(&mut self, cursor_id: i32, _rowid: i64) {
7911 self.mark_storage_table_dirty(cursor_id);
7913 }
7914
7915 fn sync_storage_table_restore_into_memdb_mirror(
7916 &mut self,
7917 cursor_id: i32,
7918 _rowid: i64,
7919 _payload: &[u8],
7920 ) {
7921 self.mark_storage_table_dirty(cursor_id);
7923 }
7924
7925 pub fn take_database(&mut self) -> Option<MemDatabase> {
7927 self.db.take()
7928 }
7929
7930 pub fn register_vtab_cursor(&mut self, cursor_id: i32, cursor: Box<dyn ErasedVtabCursor>) {
7932 self.ensure_cold_state_for(StatementColdState::VTAB_CURSORS)
7933 .vtab_cursors
7934 .insert(
7935 cursor_id,
7936 VtabCursorState {
7937 cursor,
7938 null_row: false,
7939 },
7940 );
7941 }
7942
7943 pub fn register_vtab_instance(
7945 &mut self,
7946 cursor_id: i32,
7947 instance: Box<dyn fsqlite_func::vtab::ErasedVtabInstance>,
7948 ) {
7949 self.vtab_instances
7950 .insert(cursor_id, Box::new(ErasedVtabBridge(instance)));
7951 }
7952
7953 pub fn enable_storage_cursors(&mut self, enabled: bool) {
7955 self.storage_cursors_enabled = enabled;
7956 }
7957
7958 pub fn enable_storage_read_cursors(&mut self, enabled: bool) {
7960 self.enable_storage_cursors(enabled);
7961 }
7962
7963 pub fn set_reject_mem_fallback(&mut self, reject: bool) {
7970 self.reject_mem_fallback = reject;
7971 }
7972
7973 #[must_use]
7979 pub fn all_cursors_are_txn_backed(&self) -> bool {
7980 self.storage_cursors.values().all(|sc| sc.cursor.is_txn())
7981 }
7982
7983 #[must_use]
7985 pub fn has_mem_cursor(&self) -> bool {
7986 self.storage_cursors.values().any(|sc| sc.cursor.is_mem())
7987 }
7988
7989 pub fn validate_parity_cert_invariant(&self) -> std::result::Result<(), String> {
7995 if !self.reject_mem_fallback {
7996 return Ok(());
7997 }
7998 let mem_cursors: Vec<i32> = self
7999 .storage_cursors
8000 .iter()
8001 .filter(|(_, sc)| sc.cursor.is_mem())
8002 .map(|(id, _)| id)
8003 .collect();
8004 if mem_cursors.is_empty() {
8005 Ok(())
8006 } else {
8007 Err(format!(
8008 "bd-2ttd8.4: parity-cert violation — {} cursor(s) routed through MemPageStore: {:?}",
8009 mem_cursors.len(),
8010 mem_cursors
8011 ))
8012 }
8013 }
8014
8015 fn attach_transaction_state(
8016 &mut self,
8017 txn: TransactionKind,
8018 concurrent: Option<ConcurrentContext>,
8019 ) {
8020 if let Some(txn_page_io) = self.txn_page_io.as_ref() {
8021 txn_page_io.refill(txn, concurrent);
8022 } else {
8023 self.txn_page_io = Some(SharedTxnPageIo::from_parts(txn, concurrent));
8024 }
8025 self.storage_cursors_enabled = true;
8026 }
8027
8028 pub fn set_transaction(&mut self, txn: impl Into<TransactionKind>) {
8034 self.attach_transaction_state(txn.into(), None);
8035 }
8036
8037 pub fn set_transaction_concurrent(
8045 &mut self,
8046 txn: impl Into<TransactionKind>,
8047 session_id: u64,
8048 handle: SharedConcurrentHandle,
8049 lock_table: Arc<InProcessPageLockTable>,
8050 commit_index: Arc<CommitIndex>,
8051 busy_timeout_ms: u64,
8052 ) {
8053 self.attach_transaction_state(
8054 txn.into(),
8055 Some(ConcurrentContext::new(
8056 session_id,
8057 handle,
8058 lock_table,
8059 commit_index,
8060 busy_timeout_ms,
8061 )),
8062 );
8063 }
8064
8065 pub fn set_concurrent_rowid_allocator(
8067 &mut self,
8068 allocator: Arc<ConcurrentRowIdAllocator>,
8069 schema_epoch: SchemaEpoch,
8070 ) {
8071 self.concurrent_rowid_allocator = Some(allocator);
8072 self.concurrent_rowid_schema_epoch = schema_epoch;
8073 }
8074
8075 fn storage_cursor_runtime_meta(&self, root_page: i32) -> (RowIdMode, i64) {
8076 match self.autoincrement_seq_by_root_page.get(&root_page).copied() {
8077 Some(high_water) => (RowIdMode::AutoIncrement, high_water),
8078 None => (RowIdMode::Normal, 0),
8079 }
8080 }
8081
8082 async fn storage_cursor_visible_max_rowid(sc: &mut StorageCursor) -> Result<i64> {
8083 if sc.cursor.last(&sc.cx).await? {
8084 sc.cursor.rowid(&sc.cx).await
8085 } else {
8086 Ok(0)
8087 }
8088 }
8089
8090 fn concurrent_rowid_key(schema_epoch: SchemaEpoch, root_page: i32) -> Result<AllocatorKey> {
8091 let root_page_u32 = u32::try_from(root_page).map_err(|_| {
8092 FrankenError::internal(format!(
8093 "invalid concurrent rowid root page {root_page} for allocator"
8094 ))
8095 })?;
8096 Ok(AllocatorKey {
8097 schema_epoch,
8098 table_id: TableId::new(root_page_u32),
8099 })
8100 }
8101
8102 fn map_rowid_allocator_error(
8103 err: impl std::fmt::Display,
8104 detail: &'static str,
8105 ) -> FrankenError {
8106 FrankenError::VdbeExecutionError {
8107 detail: format!("{detail}: {err}"),
8108 }
8109 }
8110
8111 async fn allocate_serialized_storage_rowid(
8112 sc: &mut StorageCursor,
8113 autoinc_max: i64,
8114 overflow_detail: &'static str,
8115 ) -> Result<i64> {
8116 let base = if sc.last_alloc_rowid > 0 {
8117 sc.last_alloc_rowid.max(autoinc_max)
8118 } else {
8119 Self::storage_cursor_visible_max_rowid(sc)
8120 .await?
8121 .max(autoinc_max)
8122 };
8123 let rowid = base
8124 .checked_add(1)
8125 .ok_or_else(|| FrankenError::VdbeExecutionError {
8126 detail: overflow_detail.into(),
8127 })?;
8128 sc.last_alloc_rowid = rowid;
8129 Ok(rowid)
8130 }
8131
8132 async fn allocate_concurrent_storage_rowid(
8133 allocator: &ConcurrentRowIdAllocator,
8134 schema_epoch: SchemaEpoch,
8135 root_page: i32,
8136 mode: RowIdMode,
8137 autoinc_max: i64,
8138 sc: &mut StorageCursor,
8139 overflow_detail: &'static str,
8140 ) -> Result<i64> {
8141 let visible_max = if sc.last_alloc_rowid > 0 {
8142 sc.last_alloc_rowid.max(autoinc_max)
8143 } else {
8144 Self::storage_cursor_visible_max_rowid(sc)
8145 .await?
8146 .max(autoinc_max)
8147 };
8148 let key = Self::concurrent_rowid_key(schema_epoch, root_page)?;
8149 allocator.ensure_table_floor(
8150 key,
8151 (visible_max > 0).then(|| RowId::new(visible_max)),
8152 autoinc_max,
8153 mode,
8154 );
8155 let rowid = allocator
8156 .allocate_one(key)
8157 .map_err(|err| Self::map_rowid_allocator_error(err, overflow_detail))?
8158 .get();
8159 sc.last_alloc_rowid = rowid;
8160 Ok(rowid)
8161 }
8162
8163 async fn bump_concurrent_storage_rowid_floor(
8164 allocator: &ConcurrentRowIdAllocator,
8165 schema_epoch: SchemaEpoch,
8166 root_page: i32,
8167 mode: RowIdMode,
8168 autoinc_max: i64,
8169 sc: &mut StorageCursor,
8170 rowid: i64,
8171 ) -> Result<()> {
8172 let visible_max = Self::storage_cursor_visible_max_rowid(sc)
8173 .await?
8174 .max(autoinc_max)
8175 .max(rowid);
8176 let key = Self::concurrent_rowid_key(schema_epoch, root_page)?;
8177 allocator.ensure_table_floor(
8178 key,
8179 (visible_max > 0).then(|| RowId::new(visible_max)),
8180 autoinc_max,
8181 mode,
8182 );
8183 allocator
8184 .bump_explicit(key, RowId::new(rowid))
8185 .map_err(|err| Self::map_rowid_allocator_error(err, "explicit rowid bump failed"))?;
8186 Ok(())
8187 }
8188
8189 pub fn take_transaction(&mut self) -> Result<Option<TransactionKind>> {
8194 self.storage_cursors.clear();
8196 match self.txn_page_io.take() {
8197 Some(txn_page_io) => Ok(Some(txn_page_io.into_inner()?)),
8198 None => Ok(None),
8199 }
8200 }
8201
8202 pub fn refill_transaction(
8206 &mut self,
8207 txn: impl Into<TransactionKind>,
8208 ) -> Option<TransactionKind> {
8209 self.txn_page_io
8210 .as_ref()
8211 .map(|io| io.refill(txn.into(), None))
8212 }
8213
8214 pub fn drain_transaction(&mut self) -> Option<TransactionKind> {
8218 self.txn_page_io.as_ref().map(|io| io.drain())
8219 }
8220
8221 pub fn has_txn_page_io(&self) -> bool {
8223 self.txn_page_io.is_some()
8224 }
8225
8226 pub fn storage_cursors_empty(&self) -> bool {
8228 self.storage_cursors.is_empty()
8229 }
8230
8231 pub fn set_function_registry(&mut self, registry: Arc<FunctionRegistry>) {
8233 self.func_registry = Some(registry);
8234 }
8235
8236 pub fn set_collation_registry(&mut self, registry: Arc<Mutex<CollationRegistry>>) {
8238 self.collation_registry = Arc::clone(®istry);
8239 if let Some(db) = self.db.as_mut() {
8240 db.set_collation_registry(registry);
8241 }
8242 }
8243
8244 #[inline]
8248 fn lock_collation(&self) -> std::sync::MutexGuard<'_, CollationRegistry> {
8249 self.collation_registry
8250 .lock()
8251 .unwrap_or_else(|e| e.into_inner())
8252 }
8253
8254 pub fn set_bindings(&mut self, bindings: Vec<SqliteValue>) {
8258 self.bindings = bindings.into_iter().collect();
8259 }
8260
8261 pub fn set_bindings_slice(&mut self, bindings: &[SqliteValue]) {
8263 self.bindings.clear();
8264 self.bindings.extend(bindings.iter().cloned());
8265 }
8266
8267 pub fn set_schema_cookie(&mut self, cookie: u32) {
8270 self.schema_cookie = cookie;
8271 }
8272
8273 pub fn schema_cookie(&self) -> u32 {
8275 self.schema_cookie
8276 }
8277
8278 pub fn set_autoincrement_sequence_by_root_page(&mut self, map: HashMap<i32, i64>) {
8282 self.autoincrement_seq_by_root_page = map;
8283 }
8284
8285 pub fn set_rowid_alias_column_by_root_page(&mut self, map: HashMap<i32, usize>) {
8287 self.rowid_alias_col_by_root_page = Arc::new(map);
8288 }
8289
8290 pub fn set_shared_rowid_alias_column_by_root_page(&mut self, map: Arc<HashMap<i32, usize>>) {
8292 self.rowid_alias_col_by_root_page = map;
8293 }
8294
8295 pub fn set_table_column_count_by_root_page(&mut self, map: HashMap<i32, usize>) {
8297 self.table_column_count_by_root_page = Arc::new(map);
8298 }
8299
8300 pub fn set_shared_table_column_count_by_root_page(&mut self, map: Arc<HashMap<i32, usize>>) {
8302 self.table_column_count_by_root_page = map;
8303 }
8304
8305 pub fn set_first_not_null_non_ipk_col_by_root_page(&mut self, map: HashMap<i32, usize>) {
8307 self.first_not_null_non_ipk_col_by_root_page = Arc::new(map);
8308 }
8309
8310 pub fn set_shared_first_not_null_non_ipk_col_by_root_page(
8312 &mut self,
8313 map: Arc<HashMap<i32, usize>>,
8314 ) {
8315 self.first_not_null_non_ipk_col_by_root_page = map;
8316 }
8317
8318 pub fn set_column_defaults_by_root_page(
8321 &mut self,
8322 map: HashMap<i32, Vec<Option<SqliteValue>>>,
8323 ) {
8324 self.column_defaults_by_root_page = Arc::new(map);
8325 }
8326
8327 pub fn set_shared_column_defaults_by_root_page(
8329 &mut self,
8330 map: Arc<HashMap<i32, Vec<Option<SqliteValue>>>>,
8331 ) {
8332 self.column_defaults_by_root_page = map;
8333 }
8334
8335 pub fn set_index_desc_flags_by_root_page(&mut self, map: HashMap<i32, Vec<bool>>) {
8337 self.index_desc_flags_by_root_page = Arc::new(map);
8338 }
8339
8340 pub fn set_shared_index_desc_flags_by_root_page(&mut self, map: Arc<HashMap<i32, Vec<bool>>>) {
8342 self.index_desc_flags_by_root_page = map;
8343 }
8344
8345 pub fn set_index_collations_by_root_page(&mut self, map: HashMap<i32, Vec<Option<String>>>) {
8347 self.index_collations_by_root_page = Arc::new(map);
8348 }
8349
8350 pub fn set_shared_index_collations_by_root_page(
8352 &mut self,
8353 map: Arc<HashMap<i32, Vec<Option<String>>>>,
8354 ) {
8355 self.index_collations_by_root_page = map;
8356 }
8357
8358 #[allow(
8363 clippy::too_many_lines,
8364 clippy::match_same_arms,
8365 clippy::cast_sign_loss,
8366 clippy::cast_possible_truncation,
8367 clippy::cast_possible_wrap
8368 )]
8369 pub async fn execute(&mut self, program: &VdbeProgram) -> Result<ExecOutcome> {
8370 self.execute_with_borrowed_bindings_internal(program, None, None)
8371 .await
8372 }
8373
8374 #[allow(
8379 clippy::too_many_lines,
8380 clippy::match_same_arms,
8381 clippy::cast_sign_loss,
8382 clippy::cast_possible_truncation,
8383 clippy::cast_possible_wrap
8384 )]
8385 pub async fn execute_with_borrowed_bindings(
8386 &mut self,
8387 program: &VdbeProgram,
8388 borrowed_bindings: Option<&[SqliteValue]>,
8389 ) -> Result<ExecOutcome> {
8390 self.execute_with_borrowed_bindings_internal(program, borrowed_bindings, None)
8391 .await
8392 }
8393
8394 pub async fn execute_with_borrowed_bindings_and_row_handler(
8400 &mut self,
8401 program: &VdbeProgram,
8402 borrowed_bindings: Option<&[SqliteValue]>,
8403 row_handler: &mut ResultRowCallback<'_>,
8404 ) -> Result<ExecOutcome> {
8405 self.execute_with_borrowed_bindings_internal(program, borrowed_bindings, Some(row_handler))
8406 .await
8407 }
8408
8409 async fn execute_with_borrowed_bindings_internal(
8410 &mut self,
8411 program: &VdbeProgram,
8412 borrowed_bindings: Option<&[SqliteValue]>,
8413 mut row_handler: Option<&mut ResultRowCallback<'_>>,
8414 ) -> Result<ExecOutcome> {
8415 let _record_profile_scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
8416 let _pipeline_profile_stage = enter_vdbe_execute_profile_stage();
8417 let estimated_make_record_capacity =
8418 estimate_make_record_buffer_capacity(program, self.page_size);
8419 self.make_record_lookaside
8420 .prepare_for_statement(estimated_make_record_capacity);
8421 if !self.statement_state_clean {
8422 self.clear_statement_cold_state();
8423 self.results.clear();
8424 self.last_compare_result = None;
8425 self.changes = 0;
8426 self.replace_victims.clear();
8427 self.last_insert_rowid = 0;
8428 self.last_insert_rowid_valid = false;
8429 self.last_insert_cursor_id = None;
8430 self.fk_counter = 0;
8431 self.cursor_root_pages.clear();
8432 }
8433 self.statement_state_clean = false;
8434 self.table_index_meta = Arc::clone(program.shared_table_index_meta());
8435
8436 let ops = program.ops();
8437 if ops.is_empty() {
8438 return Ok(ExecOutcome::Done);
8439 }
8440
8441 let reg_count = usize::try_from(program.register_count()).unwrap_or(0);
8445 if self.registers.len() < reg_count {
8446 self.registers.resize(reg_count, SqliteValue::Null);
8447 }
8448
8449 let statement_debug_enabled =
8450 tracing::enabled!(target: "fsqlite_vdbe::statement", tracing::Level::DEBUG);
8451 let jit_enabled = vdbe_jit_enabled();
8452 let jit_debug_enabled =
8453 jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::DEBUG);
8454 let jit_info_enabled =
8455 jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::INFO);
8456 let jit_warn_enabled =
8457 jit_enabled && tracing::enabled!(target: "fsqlite_vdbe::jit", tracing::Level::WARN);
8458 let exec_info_enabled = tracing::enabled!(target: "fsqlite_vdbe", tracing::Level::INFO);
8459 let slow_query_info_enabled =
8460 tracing::enabled!(target: "fsqlite_vdbe::slow_query", tracing::Level::INFO);
8461 let collect_vdbe_metrics = vdbe_metrics_enabled();
8462 self.collect_vdbe_metrics = collect_vdbe_metrics;
8463 let needs_statement_timing = collect_vdbe_metrics
8464 || statement_debug_enabled
8465 || exec_info_enabled
8466 || slow_query_info_enabled;
8467 let program_id = if statement_debug_enabled
8468 || jit_debug_enabled
8469 || jit_info_enabled
8470 || jit_warn_enabled
8471 || exec_info_enabled
8472 || slow_query_info_enabled
8473 {
8474 VDBE_PROGRAM_ID_SEQ.fetch_add(1, AtomicOrdering::Relaxed)
8475 } else {
8476 0
8477 };
8478 let start_time = needs_statement_timing.then(Instant::now);
8479 let mut opcode_count: u64 = 0;
8480 let mut local_opcode_execution_totals =
8481 collect_vdbe_metrics.then(|| vec![0_u64; Opcode::COUNT + 1].into_boxed_slice());
8482
8483 if statement_debug_enabled {
8484 tracing::debug!(
8485 target: "fsqlite_vdbe::statement",
8486 program_id,
8487 num_ops = ops.len(),
8488 "vdbe statement begin",
8489 );
8490 }
8491
8492 let mut compiled_program = None;
8493 if jit_enabled {
8494 match maybe_trigger_jit(program) {
8495 JitDecision::Disabled => {}
8496 JitDecision::Warming {
8497 plan_hash,
8498 execution_count,
8499 } => {
8500 if jit_debug_enabled {
8501 tracing::debug!(
8502 target: "fsqlite_vdbe::jit",
8503 program_id,
8504 plan_hash = format_args!("{plan_hash:016x}"),
8505 execution_count,
8506 hot_threshold = vdbe_jit_hot_threshold(),
8507 "jit warmup (interpreter tier)"
8508 );
8509 }
8510 }
8511 JitDecision::CacheHit {
8512 plan_hash,
8513 code_size_bytes,
8514 compiled_program: cached_program,
8515 } => {
8516 if jit_info_enabled {
8517 tracing::info!(
8518 target: "fsqlite_vdbe::jit",
8519 program_id,
8520 plan_hash = format_args!("{plan_hash:016x}"),
8521 code_size_bytes,
8522 "jit trigger cache hit"
8523 );
8524 }
8525 compiled_program = Some(cached_program);
8526 }
8527 JitDecision::UnsupportedCached { plan_hash } => {
8528 if jit_debug_enabled {
8529 tracing::debug!(
8530 target: "fsqlite_vdbe::jit",
8531 program_id,
8532 plan_hash = format_args!("{plan_hash:016x}"),
8533 "jit unsupported-plan cache hit (skipping recompilation)"
8534 );
8535 }
8536 }
8537 JitDecision::Compiled {
8538 plan_hash,
8539 compile_time_us,
8540 code_size_bytes,
8541 evicted_plan_hash,
8542 compiled_program: newly_compiled_program,
8543 } => {
8544 if jit_info_enabled {
8545 let plan_hash_hex = format!("{plan_hash:016x}");
8546 let span = tracing::info_span!(
8547 target: "fsqlite_vdbe::jit",
8548 "jit_compile",
8549 plan_hash = %plan_hash_hex,
8550 compile_time_us,
8551 code_size_bytes,
8552 );
8553 let _compile_guard = span.enter();
8554 tracing::info!(
8555 target: "fsqlite_vdbe::jit",
8556 program_id,
8557 plan_hash = %plan_hash_hex,
8558 compile_time_us,
8559 code_size_bytes,
8560 evicted_plan_hash = evicted_plan_hash.map(|value| format!("{value:016x}")),
8561 "jit trigger compile completed"
8562 );
8563 }
8564 compiled_program = Some(newly_compiled_program);
8565 }
8566 JitDecision::CompileFailed {
8567 plan_hash,
8568 compile_time_us,
8569 reason,
8570 } => {
8571 if jit_warn_enabled {
8572 let plan_hash_hex = format!("{plan_hash:016x}");
8573 let span = tracing::info_span!(
8574 target: "fsqlite_vdbe::jit",
8575 "jit_compile",
8576 plan_hash = %plan_hash_hex,
8577 compile_time_us,
8578 code_size_bytes = 0_u64,
8579 );
8580 let _compile_guard = span.enter();
8581 tracing::warn!(
8582 target: "fsqlite_vdbe::jit",
8583 program_id,
8584 plan_hash = %plan_hash_hex,
8585 compile_time_us,
8586 reason,
8587 "jit compilation failed; falling back to interpreter"
8588 );
8589 }
8590 }
8591 }
8592 }
8593
8594 if let Some(compiled_program) = compiled_program {
8595 let outcome = self
8596 .execute_compiled_program(
8597 compiled_program.as_ref(),
8598 borrowed_bindings,
8599 collect_vdbe_metrics,
8600 &mut row_handler,
8601 )
8602 .await?;
8603
8604 if !needs_statement_timing {
8605 return Ok(outcome);
8606 }
8607
8608 let elapsed = start_time
8609 .expect("statement timing state exists when post-execution bookkeeping is enabled")
8610 .elapsed();
8611 let elapsed_us = elapsed.as_micros();
8612 let result_rows = self.results.len();
8613
8614 if collect_vdbe_metrics {
8615 FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.fetch_add(0, AtomicOrdering::Relaxed);
8616 FSQLITE_VDBE_STATEMENTS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
8617 #[allow(clippy::cast_possible_truncation)]
8618 FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
8619 .fetch_add(elapsed_us as u64, AtomicOrdering::Relaxed);
8620 }
8621
8622 let log_statement_done = || {
8623 if statement_debug_enabled {
8624 tracing::debug!(
8625 target: "fsqlite_vdbe::statement",
8626 program_id,
8627 opcode_count = 0_u64,
8628 result_rows,
8629 elapsed_us = elapsed_us as u64,
8630 outcome = ?outcome,
8631 "vdbe statement done",
8632 );
8633 }
8634
8635 if slow_query_info_enabled && elapsed.as_millis() >= SLOW_QUERY_THRESHOLD_MS {
8636 #[allow(clippy::cast_possible_truncation)]
8637 let millis = elapsed.as_millis() as u64;
8638 tracing::info!(
8639 target: "fsqlite_vdbe::slow_query",
8640 program_id,
8641 opcode_count = 0_u64,
8642 result_rows,
8643 elapsed_ms = millis,
8644 "slow vdbe statement",
8645 );
8646 }
8647 };
8648
8649 if exec_info_enabled {
8650 let span = tracing::info_span!(
8651 target: "fsqlite_vdbe",
8652 "vdbe_exec",
8653 opcode_count = 0_u64,
8654 program_id,
8655 result_rows,
8656 elapsed_us = elapsed_us as u64,
8657 );
8658 let _guard = span.enter();
8659 log_statement_done();
8660 } else {
8661 log_statement_done();
8662 }
8663
8664 return Ok(outcome);
8665 }
8666
8667 let mut pc: usize = 0;
8668 let n_ops = ops.len();
8670 let mut once_stack = [0u64; 4]; let mut once_heap: Vec<u64> = if n_ops > 256 {
8672 vec![0u64; n_ops.div_ceil(64)]
8673 } else {
8674 Vec::new()
8675 };
8676 let once_bits: &mut [u64] = if n_ops > 256 {
8677 &mut once_heap
8678 } else {
8679 &mut once_stack
8680 };
8681
8682 let outcome = loop {
8686 debug_assert!(
8691 pc < ops.len(),
8692 "VDBE pc={pc} out of bounds (len={})",
8693 ops.len()
8694 );
8695 if opcode_count & (VDBE_EXECUTION_CHECKPOINT_INTERVAL - 1) == 0 {
8696 observe_execution_cancellation(&self.execution_cx)?;
8697 }
8698
8699 let op = &ops[pc];
8700 opcode_count += 1;
8701 if collect_vdbe_metrics {
8702 if let Some(local_opcode_execution_totals) = local_opcode_execution_totals.as_mut()
8703 {
8704 let opcode_idx = usize::from(op.opcode as u8);
8705 local_opcode_execution_totals[opcode_idx] =
8706 local_opcode_execution_totals[opcode_idx].saturating_add(1);
8707 }
8708 }
8709 if self.trace_opcodes {
8710 self.trace_opcode(pc, op);
8711 }
8712 if self
8713 .try_execute_hot_opcode(
8714 op,
8715 &mut pc,
8716 collect_vdbe_metrics,
8717 &mut row_handler,
8718 borrowed_bindings,
8719 )
8720 .await?
8721 {
8722 continue;
8723 }
8724 #[allow(unreachable_patterns)]
8725 match op.opcode {
8726 Opcode::Init => {
8728 let target = op.p2 as usize;
8733 if op.p2 > 0 && target < ops.len() {
8734 pc = target;
8735 continue;
8736 }
8737 pc += 1;
8738 }
8739
8740 Opcode::Goto => {
8741 pc = op.p2 as usize;
8742 }
8743
8744 Opcode::Halt => {
8745 if op.p1 != 0 {
8746 if op.p1 == ErrorCode::Constraint as i32 && op.p5 == OPFLAG_HALT_UNIQUE {
8747 let columns = match &op.p4 {
8748 P4::Str(columns) => columns.clone(),
8749 _ => String::new(),
8750 };
8751 return Err(FrankenError::UniqueViolation { columns });
8752 }
8753 let msg = match &op.p4 {
8754 P4::Str(s) => s.clone(),
8755 _ => format!("halt with error code {}", op.p1),
8756 };
8757 break ExecOutcome::Error {
8758 code: op.p1,
8759 message: msg,
8760 };
8761 }
8762 break ExecOutcome::Done;
8763 }
8764
8765 Opcode::Noop => {
8766 pc += 1;
8767 }
8768
8769 Opcode::SetSnapshot => {
8770 self.execute_set_snapshot_cold(op)?;
8771 pc += 1;
8772 }
8773
8774 Opcode::Integer => {
8776 self.set_reg_int(op.p2, i64::from(op.p1));
8778 pc += 1;
8779 }
8780
8781 Opcode::Int64 => {
8782 let val = match &op.p4 {
8783 P4::Int64(v) => *v,
8784 _ => 0,
8785 };
8786 self.set_reg_int(op.p2, val);
8787 pc += 1;
8788 }
8789
8790 Opcode::Real => {
8791 let val = match &op.p4 {
8792 P4::Real(v) => *v,
8793 _ => 0.0,
8794 };
8795 self.set_reg_real(op.p2, val);
8796 pc += 1;
8797 }
8798
8799 Opcode::String8 => {
8800 match &op.p4 {
8803 P4::Str(s) => self.write_text_to_reg(op.p2, s),
8804 _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
8805 }
8806 pc += 1;
8807 }
8808
8809 Opcode::String => {
8810 match &op.p4 {
8812 P4::Str(s) => self.write_text_to_reg(op.p2, s),
8813 _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
8814 }
8815 pc += 1;
8816 }
8817
8818 Opcode::Null => {
8819 let start = op.p2;
8823 let end = if op.p3 > 0 { op.p3 } else { start };
8824 for r in start..=end {
8825 self.set_reg_null(r);
8826 }
8827 pc += 1;
8828 }
8829
8830 Opcode::SoftNull => {
8831 self.set_reg_null(op.p1);
8832 pc += 1;
8833 }
8834
8835 Opcode::Blob => {
8836 match &op.p4 {
8839 P4::Blob(b) => self.write_blob_to_reg(op.p2, b),
8840 _ => self.set_reg(op.p2, SqliteValue::Blob(Arc::from([] as [u8; 0]))),
8841 }
8842 pc += 1;
8843 }
8844
8845 Opcode::Move => {
8847 let count = usize::try_from(op.p3).unwrap_or(0);
8848 self.move_reg_range(op.p1, op.p2, count);
8849 pc += 1;
8850 }
8851
8852 Opcode::Copy => {
8853 self.copy_reg_range(op.p1, op.p2, op.p3);
8856 pc += 1;
8857 }
8858
8859 Opcode::SCopy => {
8860 self.copy_single_reg(op.p1, op.p2);
8862 pc += 1;
8863 }
8864
8865 Opcode::IntCopy => {
8866 let val = self.get_reg(op.p1).to_integer();
8867 self.set_reg_int(op.p2, val);
8868 pc += 1;
8869 }
8870
8871 Opcode::ResultRow => {
8873 self.execute_result_row_hot(op, collect_vdbe_metrics, &mut row_handler)?;
8874 pc += 1;
8875 }
8876
8877 Opcode::Add => {
8879 let a = self.get_reg(op.p2);
8881 let b = self.get_reg(op.p1);
8882 let result = a.sql_add(b);
8883 self.set_reg_fast(op.p3, result);
8884 pc += 1;
8885 }
8886
8887 Opcode::Subtract => {
8888 let a = self.get_reg(op.p2);
8890 let b = self.get_reg(op.p1);
8891 let result = a.sql_sub(b);
8892 self.set_reg_fast(op.p3, result);
8893 pc += 1;
8894 }
8895
8896 Opcode::Multiply => {
8897 let a = self.get_reg(op.p2);
8899 let b = self.get_reg(op.p1);
8900 let result = a.sql_mul(b);
8901 self.set_reg_fast(op.p3, result);
8902 pc += 1;
8903 }
8904
8905 Opcode::Divide => {
8906 let divisor = self.get_reg(op.p1);
8908 let dividend = self.get_reg(op.p2);
8909 let result = sql_div(dividend, divisor);
8910 self.set_reg_fast(op.p3, result);
8911 pc += 1;
8912 }
8913
8914 Opcode::Remainder => {
8915 let divisor = self.get_reg(op.p1);
8917 let dividend = self.get_reg(op.p2);
8918 let result = sql_rem(dividend, divisor);
8919 self.set_reg_fast(op.p3, result);
8920 pc += 1;
8921 }
8922
8923 Opcode::Concat => {
8925 let a = self.get_reg(op.p1);
8928 let b = self.get_reg(op.p2);
8929 let result = if a.is_null() || b.is_null() {
8930 SqliteValue::Null
8931 } else {
8932 match (b, a) {
8935 (SqliteValue::Text(bs), SqliteValue::Text(as_)) => {
8936 let mut s = String::with_capacity(bs.len() + as_.len());
8937 s.push_str(bs);
8938 s.push_str(as_);
8939 SqliteValue::Text(s.into())
8940 }
8941 (SqliteValue::Text(bs), a_val) => {
8942 let a_text = a_val.to_text();
8943 let mut s = String::with_capacity(bs.len() + a_text.len());
8944 s.push_str(bs);
8945 s.push_str(&a_text);
8946 SqliteValue::Text(s.into())
8947 }
8948 (b_val, SqliteValue::Text(as_)) => {
8949 let mut s = b_val.to_text();
8950 s.push_str(as_);
8951 SqliteValue::Text(s.into())
8952 }
8953 _ => {
8954 let mut s = b.to_text();
8955 s.push_str(&a.to_text());
8956 SqliteValue::Text(s.into())
8957 }
8958 }
8959 };
8960 self.set_reg_fast(op.p3, result);
8961 pc += 1;
8962 }
8963
8964 Opcode::BitAnd => {
8966 let a = self.get_reg(op.p1);
8967 let b = self.get_reg(op.p2);
8968 if a.is_null() || b.is_null() {
8969 self.set_reg_fast(op.p3, SqliteValue::Null);
8970 } else {
8971 self.set_reg_int(op.p3, a.to_integer() & b.to_integer());
8972 }
8973 pc += 1;
8974 }
8975
8976 Opcode::BitOr => {
8977 let a = self.get_reg(op.p1);
8978 let b = self.get_reg(op.p2);
8979 if a.is_null() || b.is_null() {
8980 self.set_reg_fast(op.p3, SqliteValue::Null);
8981 } else {
8982 self.set_reg_int(op.p3, a.to_integer() | b.to_integer());
8983 }
8984 pc += 1;
8985 }
8986
8987 Opcode::ShiftLeft => {
8988 let a = self.get_reg(op.p1);
8989 let b = self.get_reg(op.p2);
8990 if a.is_null() || b.is_null() {
8991 self.set_reg_fast(op.p3, SqliteValue::Null);
8992 } else {
8993 let result = sql_shift_left(b.to_integer(), a.to_integer());
8994 self.set_reg_fast(op.p3, result);
8995 }
8996 pc += 1;
8997 }
8998
8999 Opcode::ShiftRight => {
9000 let a = self.get_reg(op.p1);
9001 let b = self.get_reg(op.p2);
9002 if a.is_null() || b.is_null() {
9003 self.set_reg_fast(op.p3, SqliteValue::Null);
9004 } else {
9005 let result = sql_shift_right(b.to_integer(), a.to_integer());
9006 self.set_reg_fast(op.p3, result);
9007 }
9008 pc += 1;
9009 }
9010
9011 Opcode::BitNot => {
9012 let a = self.get_reg(op.p1);
9014 if a.is_null() {
9015 self.set_reg_fast(op.p2, SqliteValue::Null);
9016 } else {
9017 self.set_reg_int(op.p2, !a.to_integer());
9018 }
9019 pc += 1;
9020 }
9021
9022 Opcode::AddImm => {
9024 let val = self
9026 .get_reg(op.p1)
9027 .to_integer()
9028 .wrapping_add(i64::from(op.p2));
9029 self.set_reg_int(op.p1, val);
9030 pc += 1;
9031 }
9032
9033 Opcode::Cast => {
9034 let val = self.take_reg(op.p1);
9036 if collect_vdbe_metrics {
9037 let casted = sql_cast(val.clone(), op.p2);
9038 record_type_coercion(&val, &casted);
9039 self.set_reg_fast(op.p1, casted);
9040 } else {
9041 let casted = sql_cast(val, op.p2);
9042 self.set_reg_fast(op.p1, casted);
9043 }
9044 pc += 1;
9045 }
9046
9047 Opcode::MustBeInt => {
9048 let val = self.take_reg(op.p1);
9049 let coerced = val.apply_affinity(fsqlite_types::TypeAffinity::Integer);
9050 let is_int = coerced.as_integer().is_some();
9051 self.set_reg_fast(op.p1, coerced);
9052 if is_int {
9053 pc += 1;
9054 } else {
9055 if op.p2 > 0 {
9056 pc = op.p2 as usize;
9057 continue;
9058 }
9059 return Err(FrankenError::TypeMismatch {
9060 expected: "integer".to_owned(),
9061 actual: self.get_reg(op.p1).typeof_str().to_owned(),
9062 });
9063 }
9064 }
9065
9066 #[allow(clippy::cast_precision_loss)]
9067 Opcode::RealAffinity => {
9068 if let SqliteValue::Integer(i) = self.get_reg(op.p1) {
9069 let i_val = *i;
9070 let f = i_val as f64;
9071 if collect_vdbe_metrics {
9072 record_type_coercion(
9073 &SqliteValue::Integer(i_val),
9074 &SqliteValue::Float(f),
9075 );
9076 }
9077 self.set_reg_fast(op.p1, SqliteValue::Float(f));
9078 }
9079 pc += 1;
9080 }
9081
9082 Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
9084 let lhs = self.get_reg(op.p3);
9085 let rhs = self.get_reg(op.p1);
9086 let store_p2 = (op.p5 & 0x20) != 0; if lhs.is_null() || rhs.is_null() {
9089 let null_eq = (op.p5 & 0x80) != 0;
9090 if null_eq {
9091 let both_null = lhs.is_null() && rhs.is_null();
9093 let should_jump = match op.opcode {
9094 Opcode::Eq => both_null,
9095 Opcode::Ne => !both_null,
9096 _ => false,
9097 };
9098 if store_p2 {
9099 self.set_reg_int(op.p2, i64::from(should_jump));
9100 pc += 1;
9101 } else if should_jump {
9102 pc = op.p2 as usize;
9103 } else {
9104 pc += 1;
9105 }
9106 } else if store_p2 {
9107 self.set_reg_fast(op.p2, SqliteValue::Null);
9109 pc += 1;
9110 } else {
9111 let jump_if_null = (op.p5 & 0x10) != 0;
9113 if jump_if_null {
9114 pc = op.p2 as usize;
9115 } else {
9116 pc += 1;
9117 }
9118 }
9119 } else {
9120 let cmp = if let Some(cmp) =
9125 fast_compare_same_storage_class(lhs, rhs, &op.p4, op.p5)
9126 {
9127 cmp
9128 } else {
9129 let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, op.p5);
9131 if let P4::Collation(ref coll_name) = op.p4 {
9132 let coll = self.lock_collation();
9133 collate_compare(&cmp_lhs, &cmp_rhs, coll_name, &coll)
9134 } else {
9135 cmp_lhs.partial_cmp(&cmp_rhs)
9136 }
9137 };
9138 let should_jump = matches!(
9139 (op.opcode, cmp),
9140 (Opcode::Eq, Some(std::cmp::Ordering::Equal))
9141 | (Opcode::Lt, Some(std::cmp::Ordering::Less))
9142 | (
9143 Opcode::Le,
9144 Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
9145 )
9146 | (Opcode::Gt, Some(std::cmp::Ordering::Greater))
9147 | (
9148 Opcode::Ge,
9149 Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
9150 )
9151 ) || matches!(
9152 (op.opcode, cmp),
9153 (Opcode::Ne, Some(ord)) if ord != std::cmp::Ordering::Equal
9154 );
9155
9156 if store_p2 {
9157 if cmp.is_none() {
9158 self.set_reg_fast(op.p2, SqliteValue::Null);
9160 } else {
9161 self.set_reg_int(op.p2, i64::from(should_jump));
9162 }
9163 pc += 1;
9164 } else if should_jump {
9165 pc = op.p2 as usize;
9166 } else {
9167 pc += 1;
9168 }
9169 }
9170 }
9171
9172 Opcode::And => {
9174 let a = self.get_reg(op.p1);
9176 let b = self.get_reg(op.p2);
9177 let result = sql_and(a, b);
9178 self.set_reg_fast(op.p3, result);
9179 pc += 1;
9180 }
9181
9182 Opcode::Or => {
9183 let a = self.get_reg(op.p1);
9185 let b = self.get_reg(op.p2);
9186 let result = sql_or(a, b);
9187 self.set_reg_fast(op.p3, result);
9188 pc += 1;
9189 }
9190
9191 Opcode::Not => {
9192 let a = self.get_reg(op.p1);
9194 if a.is_null() {
9195 self.set_reg_fast(op.p2, SqliteValue::Null);
9196 } else {
9197 self.set_reg_int(op.p2, i64::from(!vdbe_real_is_truthy(a)));
9198 }
9199 pc += 1;
9200 }
9201
9202 Opcode::If => {
9204 let val = self.get_reg(op.p1);
9207 let should_jump = if val.is_null() {
9208 op.p3 != 0
9209 } else {
9210 vdbe_real_is_truthy(val)
9211 };
9212 if should_jump {
9213 pc = op.p2 as usize;
9214 } else {
9215 pc += 1;
9216 }
9217 }
9218
9219 Opcode::IfNot => {
9220 let val = self.get_reg(op.p1);
9223 let should_jump = if val.is_null() {
9224 op.p3 != 0
9225 } else {
9226 !vdbe_real_is_truthy(val)
9227 };
9228 if should_jump {
9229 pc = op.p2 as usize;
9230 } else {
9231 pc += 1;
9232 }
9233 }
9234
9235 Opcode::IsNull => {
9236 if self.get_reg(op.p1).is_null() {
9238 pc = op.p2 as usize;
9239 } else {
9240 pc += 1;
9241 }
9242 }
9243
9244 Opcode::NotNull => {
9245 if self.get_reg(op.p1).is_null() {
9247 pc += 1;
9248 } else {
9249 pc = op.p2 as usize;
9250 }
9251 }
9252
9253 Opcode::Once => {
9254 let word = pc / 64;
9258 let bit = 1u64 << (pc % 64);
9259 if once_bits[word] & bit != 0 {
9260 pc = op.p2 as usize;
9262 } else {
9263 once_bits[word] |= bit;
9265 pc += 1;
9266 }
9267 }
9268
9269 Opcode::Gosub => {
9271 let return_addr = (pc + 1) as i32;
9273 self.set_reg(op.p1, SqliteValue::Integer(i64::from(return_addr)));
9274 pc = op.p2 as usize;
9275 }
9276
9277 Opcode::Return => {
9278 let addr = self.get_reg(op.p1).to_integer();
9280 if addr < 0 || addr as usize >= ops.len() {
9281 return Err(FrankenError::Internal(format!(
9282 "Return address {} out of bounds",
9283 addr
9284 )));
9285 }
9286 pc = addr as usize;
9287 }
9288
9289 Opcode::Transaction => {
9302 let _db_number = op.p1;
9303 let is_write = op.p2 != 0;
9304 if is_write && self.txn_page_io.is_some() {
9305 tracing::trace!(
9310 target: "fsqlite.vdbe",
9311 opcode = "Transaction",
9312 db = _db_number,
9313 write = is_write,
9314 "write transaction intent acknowledged"
9315 );
9316 }
9317 pc += 1;
9318 }
9319 Opcode::AutoCommit => {
9320 pc += 1;
9325 }
9326 Opcode::TableLock => {
9327 pc += 1;
9332 }
9333
9334 Opcode::ReadCookie => {
9340 let dest_reg = op.p2;
9341 let cookie_num = op.p3;
9342 let value = match cookie_num {
9343 1 => i64::from(self.schema_cookie),
9345 _ => 0,
9347 };
9348 self.set_reg(dest_reg, SqliteValue::Integer(value));
9349 pc += 1;
9350 }
9351 Opcode::SetCookie => {
9352 let cookie_num = op.p2;
9353 let new_value = op.p3;
9354 if cookie_num == 1 {
9355 #[allow(clippy::cast_sign_loss)]
9356 {
9357 self.schema_cookie = new_value as u32;
9358 }
9359 }
9360 pc += 1;
9362 }
9363
9364 Opcode::OpenRead => {
9366 if op.p3 > 1 {
9369 return Err(FrankenError::NotImplemented(format!(
9376 "ATTACH DATABASE (db number {}) not yet supported — \
9377 use the main database or :memory:",
9378 op.p3
9379 )));
9380 }
9381 let cursor_id = op.p1;
9382 let root_page = op.p2;
9383 self.pending_next_after_delete.remove(&cursor_id);
9384 if op.p3 == 1 {
9385 let has_temp_root = self
9386 .db
9387 .as_ref()
9388 .is_some_and(|db| db.get_table(root_page).is_some());
9389 if !has_temp_root {
9390 return Err(FrankenError::Internal(format!(
9391 "OpenRead failed: TEMP root page {root_page} is not attached"
9392 )));
9393 }
9394 self.storage_cursors.remove(&cursor_id);
9395 self.cursors
9396 .insert(cursor_id, MemCursor::new(root_page, false));
9397 self.cursor_root_pages.insert(cursor_id, root_page);
9398 pc += 1;
9399 continue;
9400 }
9401 if !self.open_storage_cursor(cursor_id, root_page, false).await {
9402 return Err(FrankenError::Internal(format!(
9403 "OpenRead failed: could not open storage cursor on root page {root_page}"
9404 )));
9405 }
9406 self.cursor_root_pages.insert(cursor_id, root_page);
9407 self.cursors.remove(&cursor_id);
9408 pc += 1;
9409 }
9410 Opcode::OpenWrite => {
9411 if op.p3 > 1 {
9414 return Err(FrankenError::NotImplemented(format!(
9421 "ATTACH DATABASE (db number {}) not yet supported — \
9422 use the main database or :memory:",
9423 op.p3
9424 )));
9425 }
9426 let cursor_id = op.p1;
9427 let root_page = op.p2;
9428 self.pending_next_after_delete.remove(&cursor_id);
9429 if op.p3 == 1 {
9430 let has_temp_root = self
9431 .db
9432 .as_ref()
9433 .is_some_and(|db| db.get_table(root_page).is_some());
9434 if !has_temp_root {
9435 return Err(FrankenError::Internal(format!(
9436 "OpenWrite failed: TEMP root page {root_page} is not attached"
9437 )));
9438 }
9439 self.storage_cursors.remove(&cursor_id);
9440 self.cursors
9441 .insert(cursor_id, MemCursor::new(root_page, true));
9442 self.cursor_root_pages.insert(cursor_id, root_page);
9443 pc += 1;
9444 continue;
9445 }
9446 if !self.open_storage_cursor(cursor_id, root_page, true).await {
9447 return Err(FrankenError::Internal(format!(
9448 "OpenWrite failed: could not open storage cursor on root page {root_page}"
9449 )));
9450 }
9451 self.cursor_root_pages.insert(cursor_id, root_page);
9452 self.cursors.remove(&cursor_id);
9453 pc += 1;
9454 }
9455
9456 Opcode::OpenEphemeral => {
9457 let cursor_id = op.p1;
9459 self.pending_next_after_delete.remove(&cursor_id);
9460 let num_cols = op.p2.max(1);
9461 if let Some(db) = self.db.as_mut() {
9462 let root_page = db.create_table(num_cols as usize);
9463 self.storage_cursors.remove(&cursor_id);
9464 self.cursors
9465 .insert(cursor_id, MemCursor::new(root_page, true));
9466 }
9467 pc += 1;
9468 }
9469
9470 Opcode::OpenAutoindex => {
9471 let autoindex_page_size = self.page_size.get();
9477 let cursor_id = op.p1;
9478 self.pending_next_after_delete.remove(&cursor_id);
9479 let root_pgno = if let Some(db) = self.db.as_mut() {
9480 let rp = db.allocate_root_page();
9481 PageNumber::new(rp as u32)
9482 } else {
9483 PageNumber::new(2)
9484 };
9485 if let Some(root_pgno) = root_pgno {
9486 let store = MemPageStore::with_empty_index(root_pgno, autoindex_page_size);
9487 let cx = self.derive_execution_cx();
9488 let mut cursor =
9489 BtCursor::new(store, root_pgno, autoindex_page_size, false);
9490 let autoindex_collations = extract_collation_names_owned(&op.p4);
9491 if !autoindex_collations.is_empty() {
9492 cursor.set_index_collation_context(
9493 autoindex_collations,
9494 Arc::clone(&self.collation_registry),
9495 );
9496 }
9497 self.cursor_root_pages
9498 .insert(cursor_id, root_pgno.get() as i32);
9499 self.cursors.remove(&cursor_id);
9500 self.storage_cursors.insert(
9501 cursor_id,
9502 StorageCursor {
9503 cursor: CursorBackend::Mem(cursor),
9504 cx,
9505 writable: true,
9506 root_page: root_pgno.get() as i32,
9507 rowid_mode: RowIdMode::Normal,
9508 autoincrement_high_water: 0,
9509 last_alloc_rowid: 0,
9510 payload_buf: Vec::new(),
9511 target_vals_buf: Vec::new(),
9512 cur_vals_buf: Vec::new(),
9513 row_decode: RowDecodeScratch::default(),
9514 last_position_stamp: None,
9515 last_successful_insert_rowid: None,
9516 last_rightmost_unique_index_prefix: None,
9517 last_rightmost_unique_index_position: None,
9518 cached_rowid: None,
9519 payload_includes_rowid_alias: None,
9520 ipk_col_idx: None,
9521 table_column_count: None,
9522 first_not_null_non_ipk_col: None,
9523 },
9524 );
9525 }
9526 pc += 1;
9527 }
9528
9529 Opcode::OpenPseudo => {
9530 let cursor_id = op.p1;
9531 self.pending_next_after_delete.remove(&cursor_id);
9532 self.storage_cursors.remove(&cursor_id);
9533 self.cursors.insert(cursor_id, MemCursor::new_pseudo(op.p2));
9534 pc += 1;
9535 }
9536
9537 Opcode::OpenDup | Opcode::ReopenIdx => {
9538 pc += 1;
9540 }
9541
9542 Opcode::SorterOpen => {
9543 let cursor_id = op.p1;
9544 self.pending_next_after_delete.remove(&cursor_id);
9545 let key_columns = usize::try_from(op.p2.max(1)).unwrap_or(1);
9546 let top_n_from_register = (op.p5 & SORTER_OPEN_TOP_N_REGISTER) != 0;
9547 let top_n_bound = if top_n_from_register {
9548 self.get_reg(op.p3).to_integer()
9549 } else {
9550 i64::from(op.p3)
9551 };
9552 let top_n_limit = usize::try_from(top_n_bound)
9553 .ok()
9554 .filter(|limit| top_n_from_register || *limit > 0);
9555 let (order_str, collation_str) = match &op.p4 {
9559 P4::Str(s) => {
9560 if let Some((orders, colls)) = s.split_once('|') {
9561 (orders.to_owned(), Some(colls.to_owned()))
9562 } else {
9563 (s.clone(), None)
9564 }
9565 }
9566 _ => (String::new(), None),
9567 };
9568 let sort_key_orders: Vec<SortKeyOrder> = order_str
9569 .chars()
9570 .take(key_columns)
9571 .map(|ch| match ch {
9572 '-' => SortKeyOrder::Desc,
9573 '>' => SortKeyOrder::AscNullsLast,
9574 '<' => SortKeyOrder::DescNullsFirst,
9575 _ => SortKeyOrder::Asc,
9576 })
9577 .collect();
9578 let collations: Vec<Option<String>> = if let Some(cs) = collation_str {
9579 cs.split(',')
9580 .take(key_columns)
9581 .map(|c| {
9582 if c.is_empty() {
9583 None
9584 } else {
9585 Some(c.to_owned())
9586 }
9587 })
9588 .collect()
9589 } else {
9590 Vec::new()
9591 };
9592 self.sorters.insert(
9593 cursor_id,
9594 SorterCursor::with_collation_registry(
9595 key_columns,
9596 sort_key_orders,
9597 collations,
9598 Arc::clone(&self.collation_registry),
9599 top_n_limit,
9600 ),
9601 );
9602 self.cursors.remove(&cursor_id);
9604 self.storage_cursors.remove(&cursor_id);
9605 pc += 1;
9606 }
9607
9608 Opcode::Close => {
9609 self.cursors.remove(&op.p1);
9610 if self.retain_storage_cursors_on_close {
9611 if let Some(sc) = self.storage_cursors.get_mut(&op.p1) {
9612 Self::clear_storage_cursor_statement_state(sc);
9613 }
9614 } else {
9615 self.storage_cursors.remove(&op.p1);
9616 }
9617 self.sorters.remove(&op.p1);
9618 if let Some(cold_state) = self.cold_state_mut() {
9619 cold_state.vtab_cursors.remove(&op.p1);
9620 }
9621 self.pending_next_after_delete.remove(&op.p1);
9622 pc += 1;
9623 }
9624
9625 Opcode::ColumnsUsed => {
9626 pc += 1;
9627 }
9628
9629 Opcode::Rewind | Opcode::Sort | Opcode::SorterSort => {
9630 let cursor_id = op.p1;
9632 self.pending_next_after_delete.remove(&cursor_id);
9634 let is_empty = if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
9635 if matches!(op.opcode, Opcode::Sort | Opcode::SorterSort) {
9636 sorter.sort()?;
9637 let rows = sorter.rows_sorted_total;
9639 let spill_pages = sorter.spill_pages_total;
9640 let merge_runs = sorter.spill_runs.len() as u64;
9641 if collect_vdbe_metrics {
9642 FSQLITE_SORT_ROWS_TOTAL.fetch_add(rows, AtomicOrdering::Relaxed);
9643 FSQLITE_SORT_SPILL_PAGES_TOTAL
9644 .fetch_add(spill_pages, AtomicOrdering::Relaxed);
9645 }
9646 sorter.rows_sorted_total = 0;
9647 sorter.spill_pages_total = 0;
9648 let _span = tracing::debug_span!(
9650 "sort",
9651 rows_sorted = rows,
9652 spill_pages = spill_pages,
9653 merge_runs = merge_runs,
9654 )
9655 .entered();
9656 tracing::debug!(
9657 rows_sorted = rows,
9658 spill_pages = spill_pages,
9659 merge_runs = merge_runs,
9660 "sort completed"
9661 );
9662 }
9663 if sorter.rows.is_empty() {
9664 sorter.position = None;
9665 true
9666 } else {
9667 sorter.position = Some(0);
9668 false
9669 }
9670 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9671 if cursor.is_pseudo {
9672 cursor.pseudo_row.is_none()
9673 } else if let Some(db) = self.db.as_ref() {
9674 if let Some(table) = db.get_table(cursor.root_page) {
9675 if table.rows.is_empty() {
9676 true
9677 } else {
9678 cursor.position = Some(0);
9679 false
9680 }
9681 } else {
9682 true
9683 }
9684 } else {
9685 true
9686 }
9687 } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9688 !cursor.cursor.first(&cursor.cx).await?
9689 } else {
9690 true
9691 };
9692 if is_empty {
9693 pc = op.p2 as usize;
9694 } else {
9695 pc += 1;
9696 }
9697 }
9698
9699 Opcode::Last => {
9700 let cursor_id = op.p1;
9702 self.pending_next_after_delete.remove(&cursor_id);
9704 let is_empty = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9705 !cursor.cursor.last(&cursor.cx).await?
9706 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9707 if cursor.is_pseudo {
9708 cursor.pseudo_row.is_none()
9709 } else if let Some(db) = self.db.as_ref() {
9710 if let Some(table) = db.get_table(cursor.root_page) {
9711 if table.rows.is_empty() {
9712 true
9713 } else {
9714 cursor.position = Some(table.rows.len() - 1);
9715 false
9716 }
9717 } else {
9718 true
9719 }
9720 } else {
9721 true
9722 }
9723 } else {
9724 true
9725 };
9726 if is_empty {
9727 pc = op.p2 as usize;
9728 } else {
9729 pc += 1;
9730 }
9731 }
9732
9733 Opcode::Next | Opcode::SorterNext => {
9734 let cursor_id = op.p1;
9736 let has_next = if !self.pending_next_after_delete.is_empty()
9737 && self.pending_next_after_delete.remove(&cursor_id)
9738 {
9739 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9740 !cursor.cursor.eof()
9741 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9742 if cursor.is_pseudo {
9743 false
9744 } else if let Some(pos) = cursor.position {
9745 if let Some(table) = self
9746 .db
9747 .as_ref()
9748 .and_then(|db| db.get_table(cursor.root_page))
9749 {
9750 if pos < table.rows.len() {
9751 true
9752 } else {
9753 cursor.position = None;
9754 false
9755 }
9756 } else {
9757 false
9758 }
9759 } else {
9760 false
9761 }
9762 } else {
9763 false
9764 }
9765 } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9766 cursor.cursor.next(&cursor.cx).await?
9767 } else if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
9768 if let Some(pos) = sorter.position {
9769 let next = pos + 1;
9770 if next < sorter.rows.len() {
9771 sorter.position = Some(next);
9772 true
9773 } else {
9774 sorter.position = None;
9775 false
9776 }
9777 } else {
9778 false
9779 }
9780 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9781 if cursor.is_pseudo {
9782 false
9783 } else if let Some(db) = self.db.as_ref() {
9784 if let Some(table) = db.get_table(cursor.root_page) {
9785 if let Some(pos) = cursor.position {
9786 let next = pos + 1;
9787 if next < table.rows.len() {
9788 cursor.position = Some(next);
9789 true
9790 } else {
9791 cursor.position = None;
9792 false
9793 }
9794 } else {
9795 false
9796 }
9797 } else {
9798 false
9799 }
9800 } else {
9801 false
9802 }
9803 } else {
9804 false
9805 };
9806 if has_next {
9807 pc = op.p2 as usize;
9808 } else {
9809 pc += 1;
9810 }
9811 }
9812
9813 Opcode::Prev => {
9814 let cursor_id = op.p1;
9816 if !self.pending_next_after_delete.is_empty() {
9819 self.pending_next_after_delete.remove(&cursor_id);
9820 }
9821 let has_prev = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9822 cursor.cursor.prev(&cursor.cx).await?
9823 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9824 if let Some(pos) = cursor.position {
9825 if pos > 0 {
9826 cursor.position = Some(pos - 1);
9827 true
9828 } else {
9829 cursor.position = None;
9830 false
9831 }
9832 } else {
9833 false
9834 }
9835 } else {
9836 false
9837 };
9838 if has_prev {
9839 pc = op.p2 as usize;
9840 } else {
9841 pc += 1;
9842 }
9843 }
9844
9845 Opcode::Column => {
9846 let cursor_id = op.p1;
9855 let col_idx = op.p2 as usize;
9856 let target = op.p3;
9857 if !self
9858 .column_to_reg_direct(cursor_id, col_idx, target)
9859 .await?
9860 {
9861 let val = self.cursor_column(cursor_id, col_idx).await?;
9862 self.set_reg_fast(target, val);
9863 }
9864 pc += 1;
9865 }
9866
9867 Opcode::Rowid => {
9868 let cursor_id = op.p1;
9870 let target = op.p2;
9871 let val = self.cursor_rowid(cursor_id).await?;
9872 self.set_reg_fast(target, val);
9873 pc += 1;
9874 }
9875
9876 Opcode::RowData => {
9877 let cursor_id = op.p1;
9879 let target = op.p2;
9880 if let Some(cursor) = self.storage_cursors.get(&cursor_id) {
9881 if cursor.cursor.eof() {
9882 self.set_reg_fast(target, SqliteValue::Null);
9883 } else {
9884 let payload = cursor.cursor.payload(&cursor.cx).await?;
9885 self.set_reg_fast(target, SqliteValue::Blob(payload.into()));
9886 }
9887 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
9888 if cursor.is_pseudo {
9889 if let Some(reg) = cursor.pseudo_reg {
9890 let blob = self.get_reg(reg).clone();
9891 self.set_reg_fast(target, blob);
9892 } else {
9893 self.set_reg_fast(target, SqliteValue::Null);
9894 }
9895 } else {
9896 self.set_reg_fast(target, SqliteValue::Null);
9897 }
9898 } else {
9899 self.set_reg_fast(target, SqliteValue::Null);
9900 }
9901 pc += 1;
9902 }
9903
9904 Opcode::NullRow => {
9905 if let Some(cursor) = self.storage_cursors.get_mut(&op.p1) {
9910 cursor.cursor.clear_position();
9913 }
9914 if let Some(cursor) = self.cursors.get_mut(&op.p1) {
9915 cursor.position = None;
9916 }
9917 if let Some(state) = self
9918 .cold_state_mut()
9919 .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&op.p1))
9920 {
9921 state.null_row = true;
9922 }
9923 pc += 1;
9924 }
9925
9926 Opcode::Offset => {
9927 self.set_reg_fast(op.p3, SqliteValue::Null);
9928 pc += 1;
9929 }
9930
9931 Opcode::SeekRowid => {
9933 let cursor_id = op.p1;
9936 self.pending_next_after_delete.remove(&cursor_id);
9938 let key = self.get_reg(op.p3);
9939 if key.is_null() {
9940 pc = op.p2 as usize;
9941 continue;
9942 }
9943 let rowid_val = key.to_integer();
9944 let use_rowset_advance = pc > 0
9945 && ops.get(pc - 1).is_some_and(|prev| {
9946 prev.opcode == Opcode::RowSetRead && prev.p3 == op.p3
9947 });
9948 let found = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
9949 let seek_result = if use_rowset_advance {
9950 cursor
9951 .cursor
9952 .table_advance_to(&cursor.cx, rowid_val)
9953 .await?
9954 } else {
9955 cursor.cursor.table_move_to(&cursor.cx, rowid_val).await?
9956 };
9957 seek_result.is_found()
9958 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
9959 if let Some(db) = self.db.as_ref() {
9960 if let Some(table) = db.get_table(cursor.root_page) {
9961 if let Some(idx) = table.find_by_rowid(rowid_val) {
9962 cursor.position = Some(idx);
9963 true
9964 } else {
9965 false
9966 }
9967 } else {
9968 false
9969 }
9970 } else {
9971 false
9972 }
9973 } else {
9974 false
9975 };
9976 if found {
9977 pc += 1;
9978 } else {
9979 pc = op.p2 as usize;
9980 }
9981 }
9982
9983 Opcode::SeekGE | Opcode::SeekGT | Opcode::SeekLE | Opcode::SeekLT => {
9984 let cursor_id = op.p1;
9994 self.pending_next_after_delete.remove(&cursor_id);
9996 let key_val = self.clone_reg_materialized(op.p3);
9997 if key_val.is_null() {
9998 pc = op.p2 as usize;
9999 continue;
10000 }
10001
10002 let found = if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10008 if cursor.cursor.is_table_btree() {
10009 let key = key_val.to_integer();
10011 let seek_result = cursor.cursor.table_move_to(&cursor.cx, key).await?;
10012
10013 match op.opcode {
10014 Opcode::SeekGE => {
10015 !cursor.cursor.eof()
10019 }
10020 Opcode::SeekGT => {
10021 if seek_result.is_found() {
10025 cursor.cursor.next(&cursor.cx).await?
10026 } else {
10027 !cursor.cursor.eof()
10028 }
10029 }
10030 Opcode::SeekLE => {
10031 if seek_result.is_found() {
10035 true
10036 } else if cursor.cursor.eof() {
10037 cursor.cursor.last(&cursor.cx).await?
10039 } else {
10040 cursor.cursor.prev(&cursor.cx).await?
10042 }
10043 }
10044 Opcode::SeekLT => {
10045 if cursor.cursor.eof() {
10049 cursor.cursor.last(&cursor.cx).await?
10051 } else {
10052 cursor.cursor.prev(&cursor.cx).await?
10054 }
10055 }
10056 _ => unreachable!(),
10057 }
10058 } else {
10059 let key_bytes = record_blob_bytes(&key_val);
10061 match op.opcode {
10062 Opcode::SeekGE => {
10063 cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
10064 !cursor.cursor.eof()
10065 }
10066 Opcode::SeekGT => {
10067 cursor
10068 .cursor
10069 .index_move_to_upper_bound(&cursor.cx, key_bytes)
10070 .await?;
10071 !cursor.cursor.eof()
10072 }
10073 Opcode::SeekLE => {
10074 cursor
10075 .cursor
10076 .index_move_to_upper_bound(&cursor.cx, key_bytes)
10077 .await?;
10078 if cursor.cursor.eof() {
10079 cursor.cursor.last(&cursor.cx).await?
10080 } else {
10081 cursor.cursor.prev(&cursor.cx).await?
10082 }
10083 }
10084 Opcode::SeekLT => {
10085 cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
10086 if cursor.cursor.eof() {
10087 cursor.cursor.last(&cursor.cx).await?
10088 } else {
10089 cursor.cursor.prev(&cursor.cx).await?
10090 }
10091 }
10092 _ => unreachable!(),
10093 }
10094 }
10095 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
10096 let key = key_val.to_integer();
10098 if let Some(db) = self.db.as_ref() {
10099 if let Some(table) = db.get_table(cursor.root_page) {
10100 if table.rows.is_empty() {
10101 false
10102 } else {
10103 match op.opcode {
10104 Opcode::SeekGE => {
10105 let pos = table
10106 .rows
10107 .binary_search_by_key(&key, |r| r.rowid)
10108 .unwrap_or_else(|e| e);
10109 if pos < table.rows.len() {
10110 cursor.position = Some(pos);
10111 true
10112 } else {
10113 false
10114 }
10115 }
10116 Opcode::SeekGT => {
10117 let pos = match table
10118 .rows
10119 .binary_search_by_key(&key, |r| r.rowid)
10120 {
10121 Ok(idx) => idx + 1,
10122 Err(idx) => idx,
10123 };
10124 if pos < table.rows.len() {
10125 cursor.position = Some(pos);
10126 true
10127 } else {
10128 false
10129 }
10130 }
10131 Opcode::SeekLE => {
10132 let pos = match table
10133 .rows
10134 .binary_search_by_key(&key, |r| r.rowid)
10135 {
10136 Ok(idx) => Some(idx),
10137 Err(idx) => idx.checked_sub(1),
10138 };
10139 if let Some(idx) = pos {
10140 cursor.position = Some(idx);
10141 true
10142 } else {
10143 false
10144 }
10145 }
10146 Opcode::SeekLT => {
10147 let pos = table
10148 .rows
10149 .binary_search_by_key(&key, |r| r.rowid)
10150 .unwrap_or_else(|e| e)
10151 .checked_sub(1);
10152 if let Some(idx) = pos {
10153 cursor.position = Some(idx);
10154 true
10155 } else {
10156 false
10157 }
10158 }
10159 _ => unreachable!(),
10160 }
10161 }
10162 } else {
10163 false
10164 }
10165 } else {
10166 false
10167 }
10168 } else {
10169 false
10170 };
10171 if found {
10172 pc += 1;
10173 } else {
10174 pc = op.p2 as usize;
10175 }
10176 }
10177
10178 Opcode::SeekScan | Opcode::SeekEnd | Opcode::SeekHit => {
10179 pc += 1;
10180 }
10181
10182 Opcode::NotFound | Opcode::NotExists | Opcode::IfNoHope => {
10183 let cursor_id = op.p1;
10187 if self.storage_cursors.contains_key(&cursor_id) {
10191 self.pending_next_after_delete.remove(&cursor_id);
10192 }
10193 let key_val = self.clone_reg_materialized(op.p3);
10194 if key_val.is_null() {
10195 pc = op.p2 as usize;
10196 continue;
10197 }
10198 let exists = if matches!(key_val, SqliteValue::Blob(_)) {
10199 let key_bytes = record_blob_bytes(&key_val);
10202 self.storage_cursor_find_exact_index_key(
10203 cursor_id,
10204 key_bytes,
10205 "NotFound: missing collation registry for collated probe",
10206 )
10207 .await?
10208 } else {
10209 let rowid_val = key_val.to_integer();
10211 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10212 cursor
10213 .cursor
10214 .table_move_to(&cursor.cx, rowid_val)
10215 .await?
10216 .is_found()
10217 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10218 if let Some(db) = self.db.as_ref() {
10219 if let Some(table) = db.get_table(cursor.root_page) {
10220 table.find_by_rowid(rowid_val).is_some()
10221 } else {
10222 false
10223 }
10224 } else {
10225 false
10226 }
10227 } else {
10228 false
10229 }
10230 };
10231 if exists {
10232 pc += 1; } else {
10234 pc = op.p2 as usize; }
10236 }
10237
10238 Opcode::Found => {
10239 let cursor_id = op.p1;
10242 if self.storage_cursors.contains_key(&cursor_id) {
10243 self.pending_next_after_delete.remove(&cursor_id);
10244 }
10245 let key_val = self.clone_reg_materialized(op.p3);
10246 if key_val.is_null() {
10247 pc += 1;
10248 continue;
10249 }
10250 let exists = if matches!(key_val, SqliteValue::Blob(_)) {
10251 let key_bytes = record_blob_bytes(&key_val);
10252 self.storage_cursor_find_exact_index_key(
10253 cursor_id,
10254 key_bytes,
10255 "Found: missing collation registry for collated probe",
10256 )
10257 .await?
10258 } else {
10259 let rowid_val = key_val.to_integer();
10260 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10261 cursor
10262 .cursor
10263 .table_move_to(&cursor.cx, rowid_val)
10264 .await?
10265 .is_found()
10266 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10267 if let Some(db) = self.db.as_ref() {
10268 if let Some(table) = db.get_table(cursor.root_page) {
10269 table.find_by_rowid(rowid_val).is_some()
10270 } else {
10271 false
10272 }
10273 } else {
10274 false
10275 }
10276 } else {
10277 false
10278 }
10279 };
10280 if exists {
10281 pc = op.p2 as usize;
10282 } else {
10283 pc += 1;
10284 }
10285 }
10286
10287 Opcode::NoConflict => {
10288 let cursor_id = op.p1;
10293 if self.storage_cursors.contains_key(&cursor_id) {
10294 self.pending_next_after_delete.remove(&cursor_id);
10295 }
10296 let key_val = self.clone_reg_materialized(op.p3);
10297 let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
10298 let index_collations = self.index_collations_for_cursor(cursor_id);
10299
10300 if key_val.is_null() {
10302 pc = op.p2 as usize;
10303 continue;
10304 }
10305
10306 let uses_collated_probe = if let SqliteValue::Blob(ref bytes) = key_val {
10307 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10308 if try_decode_storage_cursor_target_index_record(cursor, bytes)
10309 && cursor.target_vals_buf.iter().any(SqliteValue::is_null)
10310 {
10311 pc = op.p2 as usize;
10312 continue;
10313 }
10314 cursor.target_vals_buf.iter().enumerate().any(|(idx, _)| {
10315 index_collations
10316 .get(idx)
10317 .and_then(|collation| collation.as_deref())
10318 .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
10319 })
10320 } else {
10321 false
10322 }
10323 } else {
10324 false
10325 };
10326 let collated_probe_registry = uses_collated_probe.then(|| {
10327 self.collation_registry
10328 .lock()
10329 .unwrap_or_else(|err| err.into_inner())
10330 .clone()
10331 });
10332
10333 let conflict = if let SqliteValue::Blob(ref bytes) = key_val {
10339 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
10340 if uses_collated_probe {
10341 let Some(collation_registry) = collated_probe_registry.as_ref()
10342 else {
10343 return Err(FrankenError::internal(
10344 "NoConflict: missing collation registry for collated probe",
10345 ));
10346 };
10347 storage_cursor_no_conflict_prefix_match_collated(
10348 cursor_id,
10349 cursor,
10350 bytes,
10351 &index_desc_flags,
10352 &index_collations,
10353 collation_registry,
10354 )
10355 .await?
10356 } else {
10357 storage_cursor_no_conflict_prefix_match(cursor_id, cursor, bytes)
10358 .await?
10359 }
10360 } else {
10361 false
10362 }
10363 } else {
10364 false
10365 };
10366
10367 if conflict {
10368 pc += 1; } else {
10370 pc = op.p2 as usize; }
10372 }
10373
10374 Opcode::NewRowid => {
10376 let cursor_id = op.p1;
10383 let target = op.p2;
10384 let concurrent_mode = op.p3 != 0;
10385 let concurrent_allocator = if concurrent_mode {
10386 self.concurrent_rowid_allocator.clone()
10387 } else {
10388 None
10389 };
10390 let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
10391 let rowid = if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10392 let root_page = sc.root_page;
10393 let autoinc_max = sc.autoincrement_high_water;
10394 let rowid_mode = sc.rowid_mode;
10395 self.pending_next_after_delete.remove(&cursor_id);
10400 if concurrent_mode {
10401 if let Some(allocator) = concurrent_allocator.as_ref() {
10402 Self::allocate_concurrent_storage_rowid(
10403 allocator,
10404 concurrent_schema_epoch,
10405 root_page,
10406 rowid_mode,
10407 autoinc_max,
10408 sc,
10409 "rowid overflow: maximum rowid reached",
10410 )
10411 .await?
10412 } else {
10413 Self::allocate_serialized_storage_rowid(
10414 sc,
10415 autoinc_max,
10416 "rowid overflow: maximum rowid reached",
10417 )
10418 .await?
10419 }
10420 } else {
10421 Self::allocate_serialized_storage_rowid(
10422 sc,
10423 autoinc_max,
10424 "rowid overflow: maximum rowid reached",
10425 )
10426 .await?
10427 }
10428 } else {
10429 let root = self.cursors.get(&cursor_id).map(|c| c.root_page);
10431 if let Some(root) = root {
10432 if let Some(db) = self.db.as_mut() {
10433 if concurrent_mode {
10434 db.alloc_rowid_concurrent(root)
10435 } else {
10436 db.alloc_rowid(root)
10437 }
10438 } else {
10439 1
10440 }
10441 } else {
10442 1
10443 }
10444 };
10445 self.set_reg(target, SqliteValue::Integer(rowid));
10446 pc += 1;
10447 }
10448
10449 Opcode::Insert => {
10450 let cursor_id = op.p1;
10457 let record_reg = op.p2;
10458 let rowid_reg = op.p3;
10459 let oe_flag = op.p5 & 0x0F; let is_update = (op.p5 & OPFLAG_ISUPDATE) != 0;
10461 let rowid = self.get_reg(rowid_reg).to_integer();
10462 let concurrent_allocator = self.concurrent_rowid_allocator.clone();
10463 let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
10464 let previous_last_insert_rowid = self.last_insert_rowid;
10465 let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
10466 let pending_update_restore = if is_update {
10467 self.take_pending_update_restore()
10468 } else {
10469 self.set_pending_update_restore(None);
10470 None
10471 };
10472
10473 let preformatted_record = match &op.p4 {
10481 P4::Blob(bytes) => Some(bytes.as_slice()),
10482 _ => None,
10483 };
10484 let sideband_active = preformatted_record.is_none()
10485 && self.make_record_lookaside.sideband_is_armed_for(record_reg);
10486 let record_val = if preformatted_record.is_some() {
10491 self.clear_register_subtype(record_reg);
10492 self.set_reg(record_reg, SqliteValue::Null);
10493 SqliteValue::Null
10494 } else if sideband_active {
10495 self.clear_register_subtype(record_reg);
10496 SqliteValue::Null
10497 } else {
10498 self.take_reg(record_reg)
10502 };
10503 let mut sideband_buf = if sideband_active {
10504 self.make_record_lookaside.disarm();
10505 self.make_record_lookaside.take_buf()
10506 } else {
10507 Vec::new()
10508 };
10509 let mut actually_inserted = false;
10510 let mut inserted_via_storage = false;
10511 let mut inserted_root_page = None;
10512 let insert_result: Result<Option<ExecOutcome>> = async {
10513 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10514 if sc.writable {
10515 let root_page = sc.root_page;
10516 let autoinc_max = sc.autoincrement_high_water;
10517 let rowid_mode = sc.rowid_mode;
10518 let blob = if let Some(record) = preformatted_record {
10519 record
10520 } else if sideband_active {
10521 &sideband_buf[..]
10522 } else {
10523 record_blob_bytes(&record_val)
10524 };
10525 let append_eligible = !is_update
10537 && sc
10538 .last_successful_insert_rowid
10539 .is_some_and(|last| rowid > last);
10540 let mut insert_seek_result = None;
10541 let exists = if append_eligible {
10542 FSQLITE_VDBE_INSERT_APPEND_COUNT
10543 .fetch_add(1, AtomicOrdering::Relaxed);
10544 false } else {
10546 FSQLITE_VDBE_INSERT_SEEK_COUNT
10547 .fetch_add(1, AtomicOrdering::Relaxed);
10548 let seek_result =
10549 sc.cursor.table_move_to(&sc.cx, rowid).await?;
10550 insert_seek_result = Some(seek_result);
10551 seek_result.is_found()
10552 };
10553
10554 if exists {
10555 if sc.last_successful_insert_rowid.take().is_some() {
10556 FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10557 .fetch_add(1, AtomicOrdering::Relaxed);
10558 }
10559 if oe_flag == 5 {
10564 self.native_replace_row(cursor_id, rowid).await?;
10566 let sc2 = self
10567 .storage_cursors
10568 .get_mut(&cursor_id)
10569 .ok_or_else(|| {
10570 FrankenError::internal(
10571 "cursor disappeared during REPLACE",
10572 )
10573 })?;
10574 sc2.cursor.table_insert(&sc2.cx, rowid, blob).await?;
10575 invalidate_storage_cursor_row_cache_with_reason(
10576 sc2,
10577 self.collect_vdbe_metrics,
10578 DecodeCacheInvalidationReason::WriteMutation,
10579 );
10580 if let Some(allocator) = concurrent_allocator.as_ref() {
10581 Self::bump_concurrent_storage_rowid_floor(
10582 allocator,
10583 concurrent_schema_epoch,
10584 root_page,
10585 rowid_mode,
10586 autoinc_max,
10587 sc2,
10588 rowid,
10589 )
10590 .await?;
10591 }
10592 inserted_via_storage = true;
10593 inserted_root_page = Some(root_page);
10594 actually_inserted = true;
10595 } else if oe_flag == 4 {
10596 if let Some(update_restore) = pending_update_restore.clone()
10598 {
10599 self.restore_pending_update_after_conflict(
10600 update_restore,
10601 )
10602 .await?;
10603 }
10604 } else {
10605 if let Some(update_restore) = pending_update_restore.clone()
10607 {
10608 self.restore_pending_update_after_conflict(
10609 update_restore,
10610 )
10611 .await?;
10612 }
10613 return Ok(Some(ExecOutcome::Error {
10614 code: ErrorCode::Constraint as i32,
10615 message: "PRIMARY KEY constraint failed".to_owned(),
10616 }));
10617 }
10618 } else {
10619 if insert_seek_result == Some(SeekResult::NotFound) {
10622 let rightmost_insert = sc.cursor.eof();
10623 sc.cursor
10624 .table_insert_prechecked_absent(&sc.cx, rowid, blob)
10625 .await?;
10626 if rightmost_insert {
10627 sc.cursor
10628 .table_refresh_rightmost_leaf_cache_after_insert(
10629 &sc.cx, rowid,
10630 )
10631 .await?;
10632 sc.last_successful_insert_rowid = Some(rowid);
10633 } else if sc.last_successful_insert_rowid.take().is_some() {
10634 FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10635 .fetch_add(1, AtomicOrdering::Relaxed);
10636 }
10637 } else {
10638 sc.cursor.table_insert(&sc.cx, rowid, blob).await?;
10639 if append_eligible {
10640 sc.last_successful_insert_rowid = Some(rowid);
10641 } else if sc.last_successful_insert_rowid.take().is_some() {
10642 FSQLITE_VDBE_INSERT_APPEND_HINT_CLEAR_COUNT
10643 .fetch_add(1, AtomicOrdering::Relaxed);
10644 }
10645 }
10646 invalidate_storage_cursor_row_cache_with_reason(
10647 sc,
10648 self.collect_vdbe_metrics,
10649 DecodeCacheInvalidationReason::WriteMutation,
10650 );
10651 if let Some(allocator) = concurrent_allocator.as_ref() {
10652 Self::bump_concurrent_storage_rowid_floor(
10653 allocator,
10654 concurrent_schema_epoch,
10655 root_page,
10656 rowid_mode,
10657 autoinc_max,
10658 sc,
10659 rowid,
10660 )
10661 .await?;
10662 }
10663 inserted_via_storage = true;
10664 inserted_root_page = Some(root_page);
10665 actually_inserted = true;
10666 }
10667 }
10668 } else if let Some(root) = self.cursors.get(&cursor_id).map(|c| c.root_page)
10669 {
10670 let values = if let Some(record) = preformatted_record {
10672 let values = parse_record(record).ok_or_else(|| {
10673 FrankenError::internal("malformed SQLite record blob")
10674 })?;
10675 if self.collect_vdbe_metrics {
10676 FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
10677 .fetch_add(1, AtomicOrdering::Relaxed);
10678 for value in &values {
10679 record_decoded_value_metrics(value);
10680 }
10681 }
10682 values
10683 } else if sideband_active {
10684 let values = parse_record(&sideband_buf).ok_or_else(|| {
10685 FrankenError::internal("malformed SQLite record blob")
10686 })?;
10687 if self.collect_vdbe_metrics {
10688 FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL
10689 .fetch_add(1, AtomicOrdering::Relaxed);
10690 for value in &values {
10691 record_decoded_value_metrics(value);
10692 }
10693 }
10694 values
10695 } else {
10696 decode_record_with_metrics(&record_val, self.collect_vdbe_metrics)?
10697 };
10698 let mut mem_replace_victims: Vec<(i64, Vec<SqliteValue>)> = Vec::new();
10703 if let Some(db) = self.db.as_mut() {
10704 let rowid_conflict = db
10706 .get_table(root)
10707 .and_then(|t| t.find_by_rowid(rowid))
10708 .is_some();
10709
10710 let unique_conflicts = db
10715 .get_table(root)
10716 .map(|t| t.find_unique_conflicts(&values))
10717 .unwrap_or_default();
10718
10719 let has_conflict = rowid_conflict || !unique_conflicts.is_empty();
10720
10721 if has_conflict {
10722 match oe_flag {
10723 4 => {
10724 if let Some(update_restore) =
10726 pending_update_restore.clone()
10727 {
10728 self.restore_pending_update_after_conflict(
10729 update_restore,
10730 )
10731 .await?;
10732 }
10733 }
10734 5 => {
10735 for conflict_rid in unique_conflicts {
10738 if conflict_rid != rowid {
10741 if let Some(old_values) =
10742 db.get_table(root).and_then(|t| {
10743 t.row_values_by_rowid(conflict_rid)
10744 })
10745 {
10746 mem_replace_victims.push((
10747 conflict_rid,
10748 old_values.to_vec(),
10749 ));
10750 }
10751 db.delete_rowid(root, conflict_rid);
10752 }
10753 }
10754 if rowid_conflict
10755 && let Some(old_values) = db
10756 .get_table(root)
10757 .and_then(|t| t.row_values_by_rowid(rowid))
10758 {
10759 mem_replace_victims
10760 .push((rowid, old_values.to_vec()));
10761 }
10762 db.upsert_row(root, rowid, values);
10763 actually_inserted = true;
10764 }
10765 _ => {
10766 if let Some(update_restore) =
10768 pending_update_restore.clone()
10769 {
10770 self.restore_pending_update_after_conflict(
10771 update_restore,
10772 )
10773 .await?;
10774 }
10775 return Ok(Some(ExecOutcome::Error {
10776 code: ErrorCode::Constraint as i32,
10777 message: "PRIMARY KEY constraint failed".to_owned(),
10778 }));
10779 }
10780 }
10781 } else {
10782 db.upsert_row(root, rowid, values);
10784 actually_inserted = true;
10785 }
10786 }
10787 for (victim_rowid, old_values) in mem_replace_victims {
10788 let victim = self.logical_replace_victim(
10789 root,
10790 &old_values,
10791 Some(victim_rowid),
10792 );
10793 self.replace_victims.push(victim);
10794 }
10795 }
10796 Ok(None)
10797 }
10798 .await;
10799 if sideband_active {
10802 sideband_buf.clear();
10803 self.make_record_lookaside.replace_buf(sideband_buf);
10804 }
10805 if let Some(outcome) = insert_result? {
10806 break outcome;
10807 }
10808 if inserted_via_storage && let Some(root_page) = inserted_root_page {
10809 self.mark_storage_root_page_dirty(root_page);
10813 }
10814
10815 if actually_inserted {
10818 self.changes += 1;
10819 if !is_update {
10820 self.last_insert_rowid = rowid;
10821 self.last_insert_rowid_valid = true;
10822 }
10823 self.set_pending_insert_rollback(Some(PendingInsertRollback {
10824 cursor_id,
10825 rowid,
10826 previous_last_insert_rowid,
10827 previous_last_insert_rowid_valid,
10828 update_restore: pending_update_restore,
10829 }));
10830 } else {
10831 self.set_pending_insert_rollback(None);
10832 if oe_flag == 4 {
10836 self.set_conflict_skip_idx(true);
10837 }
10838 }
10839 self.last_insert_cursor_id = Some(cursor_id);
10840 if actually_inserted {
10841 self.set_conflict_skip_idx(false);
10842 }
10843 self.clear_pending_idx_entries();
10844
10845 self.pending_next_after_delete.remove(&cursor_id);
10849 pc += 1;
10850 }
10851
10852 Opcode::Delete => {
10853 let cursor_id = op.p1;
10855 let is_update = (op.p5 & OPFLAG_ISUPDATE) != 0;
10856 let mut deleted = false;
10857 let mut deleted_storage_rowid = None;
10858 let mut update_restore = None;
10859 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10863 if sc.writable && !sc.cursor.eof() {
10864 let current_rowid = sc.cursor.rowid(&sc.cx).await?;
10865 if is_update {
10866 update_restore = Some(PendingUpdateRestore::Storage {
10867 cursor_id,
10868 rowid: current_rowid,
10869 payload: sc.cursor.payload(&sc.cx).await?,
10870 });
10871 }
10872 sc.cursor.delete(&sc.cx).await?;
10873 invalidate_storage_cursor_row_cache_with_reason(
10874 sc,
10875 self.collect_vdbe_metrics,
10876 DecodeCacheInvalidationReason::WriteMutation,
10877 );
10878 deleted_storage_rowid = Some(current_rowid);
10879 deleted = true;
10880 }
10881 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
10882 if let Some(pos) = cursor.position {
10884 let root = cursor.root_page;
10885 let can_delete = self
10886 .db
10887 .as_ref()
10888 .and_then(|db| db.get_table(root))
10889 .is_some_and(|table| pos < table.rows.len());
10890 if can_delete && let Some(db) = self.db.as_mut() {
10891 if is_update
10892 && let Some(row) = db
10893 .get_table(root)
10894 .and_then(|table| table.rows.get(pos))
10895 .cloned()
10896 {
10897 update_restore = Some(PendingUpdateRestore::Mem {
10898 root_page: root,
10899 rowid: row.rowid,
10900 values: row.values,
10901 });
10902 }
10903 db.delete_at(root, pos);
10904 deleted = true;
10905 }
10906 }
10907 }
10908 if let Some(storage_rowid) = deleted_storage_rowid {
10909 self.sync_storage_table_delete_into_memdb_mirror(cursor_id, storage_rowid);
10910 }
10911 if deleted {
10912 if is_update && update_restore.is_some() {
10913 self.mark_statement_cold_state(StatementColdState::CONFLICT_TRACKING);
10914 }
10915 self.set_pending_update_restore(if is_update {
10916 update_restore
10917 } else {
10918 None
10919 });
10920 if op.p5 & 1 != 0 {
10924 self.changes += 1;
10925 }
10926 self.pending_next_after_delete.insert(cursor_id);
10927 } else if is_update {
10928 self.set_pending_update_restore(None);
10929 }
10930 pc += 1;
10931 }
10932
10933 Opcode::IdxInsert => {
10934 let cursor_id = op.p1;
10941 let key_reg = op.p2;
10942 let is_unique = (op.p5 & 1) != 0;
10943 #[allow(clippy::cast_possible_truncation)]
10944 let oe_flag = ((op.p5 >> 1) & 0x0F) as u8;
10945 let n_idx_cols = op.p3 as usize;
10946 let count_logical_change = (op.p5 & OPFLAG_IDX_NCHANGE) != 0;
10947 let mut inserted = false;
10948
10949 if self.conflict_skip_idx() {
10955 pc += 1;
10956 continue;
10957 }
10958
10959 let key_val = self.get_reg(key_reg).clone();
10960 let sideband_active = self.make_record_lookaside.sideband_is_armed_for(key_reg);
10961 let key_blob = if sideband_active {
10962 self.make_record_lookaside.disarm();
10963 self.make_record_lookaside.take_buf()
10964 } else {
10965 record_blob_bytes(&key_val).to_vec()
10966 };
10967
10968 let index_desc_flags = self.index_desc_flags_for_cursor(cursor_id);
10969 let index_collations = self.index_collations_for_cursor(cursor_id);
10970 let uses_collated_unique_probe = is_unique
10971 && n_idx_cols > 0
10972 && index_collations.iter().take(n_idx_cols).any(|collation| {
10973 collation
10974 .as_deref()
10975 .is_some_and(|name| !name.eq_ignore_ascii_case("BINARY"))
10976 });
10977 let collated_unique_registry = uses_collated_unique_probe.then(|| {
10978 self.collation_registry
10979 .lock()
10980 .unwrap_or_else(|err| err.into_inner())
10981 .clone()
10982 });
10983
10984 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
10985 if sc.writable {
10986 let key_bytes = key_blob.as_slice();
10987
10988 if is_unique && n_idx_cols > 0 {
10989 let columns_label = match &op.p4 {
10990 P4::Table(s) => s.as_str(),
10991 _ => "",
10992 };
10993 let mut rightmost_prefix_after_insert: Option<Vec<SqliteValue>> =
10994 None;
10995 let unique_insert_result = if uses_collated_unique_probe {
10996 sc.last_rightmost_unique_index_prefix = None;
10997 sc.last_rightmost_unique_index_position = None;
10998 if let Some(collation_registry) =
10999 collated_unique_registry.as_ref()
11000 {
11001 match find_conflicting_rowid_in_index_collated(
11002 sc,
11003 key_bytes,
11004 n_idx_cols,
11005 &index_desc_flags,
11006 &index_collations,
11007 collation_registry,
11008 )
11009 .await
11010 {
11011 Ok(Some(conflict_rowid)) => Err((
11012 FrankenError::UniqueViolation {
11013 columns: columns_label.to_owned(),
11014 },
11015 Some(conflict_rowid),
11016 )),
11017 Ok(None) => {
11018 match sc
11019 .cursor
11020 .index_insert(&sc.cx, key_bytes)
11021 .await
11022 {
11023 Ok(()) => Ok(()),
11024 Err(err) => Err((err, None)),
11025 }
11026 }
11027 Err(err) => Err((err, None)),
11028 }
11029 } else {
11030 Err((
11031 FrankenError::internal(
11032 "IdxInsert: missing collation registry for collated unique probe",
11033 ),
11034 None,
11035 ))
11036 }
11037 } else {
11038 let fast_append_prefix =
11039 if let (Some(last_prefix), Some(last_position)) = (
11040 sc.last_rightmost_unique_index_prefix.as_ref(),
11041 sc.last_rightmost_unique_index_position,
11042 ) {
11043 if sc.cursor.position_stamp() == Some(last_position) {
11044 parse_non_null_index_prefix(key_bytes, n_idx_cols)
11045 .and_then(|new_prefix| {
11046 (compare_index_prefix_keys(
11047 last_prefix,
11048 &new_prefix,
11049 n_idx_cols,
11050 &index_desc_flags,
11051 &index_collations,
11052 &BUILTIN_COLLATION_REGISTRY,
11053 ) == Ordering::Less)
11054 .then_some(new_prefix)
11055 })
11056 } else {
11057 None
11058 }
11059 } else {
11060 None
11061 };
11062 if let Some(prefix) = fast_append_prefix {
11063 match sc
11064 .cursor
11065 .index_append_after_current_rightmost_position(
11066 &sc.cx, key_bytes,
11067 )
11068 .await
11069 {
11070 Ok(true) => {
11071 rightmost_prefix_after_insert = Some(prefix);
11072 Ok(())
11073 }
11074 Ok(false) => {
11075 match sc
11076 .cursor
11077 .index_insert_unique_with_rightmost_report(
11078 &sc.cx,
11079 key_bytes,
11080 n_idx_cols,
11081 columns_label,
11082 )
11083 .await
11084 {
11085 Ok(inserted_after_rightmost) => {
11086 if inserted_after_rightmost {
11087 rightmost_prefix_after_insert =
11088 Some(prefix);
11089 }
11090 Ok(())
11091 }
11092 Err(FrankenError::UniqueViolation {
11093 columns,
11094 }) => Err((
11095 FrankenError::UniqueViolation { columns },
11096 None,
11097 )),
11098 Err(err) => Err((err, None)),
11099 }
11100 }
11101 Err(err) => Err((err, None)),
11102 }
11103 } else {
11104 match sc
11105 .cursor
11106 .index_insert_unique_with_rightmost_report(
11107 &sc.cx,
11108 key_bytes,
11109 n_idx_cols,
11110 columns_label,
11111 )
11112 .await
11113 {
11114 Ok(inserted_after_rightmost) => {
11115 if inserted_after_rightmost {
11116 rightmost_prefix_after_insert =
11117 parse_non_null_index_prefix(
11118 key_bytes, n_idx_cols,
11119 );
11120 }
11121 Ok(())
11122 }
11123 Err(FrankenError::UniqueViolation { columns }) => Err(
11124 (FrankenError::UniqueViolation { columns }, None),
11125 ),
11126 Err(err) => Err((err, None)),
11127 }
11128 }
11129 };
11130 match unique_insert_result {
11131 Ok(()) => {
11132 inserted = true;
11133 if let (Some(prefix), Some(position)) = (
11134 rightmost_prefix_after_insert,
11135 sc.cursor.position_stamp(),
11136 ) {
11137 sc.last_rightmost_unique_index_prefix = Some(prefix);
11138 sc.last_rightmost_unique_index_position =
11139 Some(position);
11140 } else {
11141 sc.last_rightmost_unique_index_prefix = None;
11142 sc.last_rightmost_unique_index_position = None;
11143 }
11144 invalidate_storage_cursor_row_cache_with_reason(
11145 sc,
11146 self.collect_vdbe_metrics,
11147 DecodeCacheInvalidationReason::WriteMutation,
11148 );
11149 self.push_pending_idx_entry(cursor_id, key_bytes.to_vec());
11150 }
11151 Err((FrankenError::UniqueViolation { .. }, conflict_rowid)) => {
11152 sc.last_rightmost_unique_index_prefix = None;
11153 sc.last_rightmost_unique_index_position = None;
11154 match oe_flag {
11155 4 => {
11159 if let Err(error) = self
11160 .rollback_pending_insert_after_index_conflict(
11161 true,
11162 )
11163 .await
11164 {
11165 if sideband_active {
11166 self.make_record_lookaside
11167 .replace_cleared_buf(key_blob);
11168 }
11169 return Err(error);
11170 }
11171 self.set_conflict_skip_idx(true);
11172 if sideband_active {
11173 self.make_record_lookaside
11174 .replace_cleared_buf(key_blob);
11175 }
11176 pc += 1;
11177 continue;
11178 }
11179 5 | 8 => {
11184 let conflict_rowid = if uses_collated_unique_probe {
11187 conflict_rowid
11188 } else {
11189 match find_conflicting_rowid_in_index(
11190 sc, key_bytes, n_idx_cols,
11191 )
11192 .await
11193 {
11194 Ok(rowid) => rowid,
11195 Err(error) => {
11196 if sideband_active {
11197 self.make_record_lookaside
11198 .replace_cleared_buf(key_blob);
11199 }
11200 return Err(error);
11201 }
11202 }
11203 };
11204
11205 if let Some(old_rowid) = conflict_rowid {
11206 if let Some(tbl_cid) =
11207 self.last_insert_cursor_id
11208 {
11209 if let Err(error) = self
11210 .native_replace_row(tbl_cid, old_rowid)
11211 .await
11212 {
11213 if sideband_active {
11214 self.make_record_lookaside
11215 .replace_cleared_buf(key_blob);
11216 }
11217 return Err(error);
11218 }
11219 }
11220 }
11221
11222 let Some(sc2) =
11227 self.storage_cursors.get_mut(&cursor_id)
11228 else {
11229 if sideband_active {
11230 self.make_record_lookaside
11231 .replace_cleared_buf(key_blob);
11232 }
11233 return Err(FrankenError::internal(
11234 "cursor must exist",
11235 ));
11236 };
11237 sc2.last_rightmost_unique_index_prefix = None;
11238 sc2.last_rightmost_unique_index_position = None;
11239 if let Err(error) = sc2
11240 .cursor
11241 .index_insert(&sc2.cx, key_bytes)
11242 .await
11243 {
11244 if sideband_active {
11245 self.make_record_lookaside
11246 .replace_cleared_buf(key_blob);
11247 }
11248 return Err(error);
11249 }
11250 invalidate_storage_cursor_row_cache_with_reason(
11251 sc2,
11252 self.collect_vdbe_metrics,
11253 DecodeCacheInvalidationReason::WriteMutation,
11254 );
11255 self.push_pending_idx_entry(
11256 cursor_id,
11257 key_blob.clone(),
11258 );
11259 inserted = true;
11260 }
11261 _ => {
11264 if let Err(error) = self
11265 .rollback_pending_insert_after_index_conflict(
11266 false,
11267 )
11268 .await
11269 {
11270 if sideband_active {
11271 self.make_record_lookaside
11272 .replace_cleared_buf(key_blob);
11273 }
11274 return Err(error);
11275 }
11276 if sideband_active {
11277 self.make_record_lookaside
11278 .replace_cleared_buf(key_blob);
11279 }
11280 return Err(FrankenError::UniqueViolation {
11281 columns: columns_label.to_owned(),
11282 });
11283 }
11284 }
11285 }
11286 Err((error, _)) => {
11287 if sideband_active {
11288 self.make_record_lookaside
11289 .replace_cleared_buf(key_blob);
11290 }
11291 return Err(error);
11292 }
11293 }
11294 } else {
11295 sc.last_rightmost_unique_index_prefix = None;
11296 sc.last_rightmost_unique_index_position = None;
11297 if let Err(error) = sc.cursor.index_insert(&sc.cx, key_bytes).await
11298 {
11299 if sideband_active {
11300 self.make_record_lookaside.replace_cleared_buf(key_blob);
11301 }
11302 return Err(error);
11303 }
11304 invalidate_storage_cursor_row_cache_with_reason(
11305 sc,
11306 self.collect_vdbe_metrics,
11307 DecodeCacheInvalidationReason::WriteMutation,
11308 );
11309 self.push_pending_idx_entry(cursor_id, key_blob.clone());
11310 inserted = true;
11311 }
11312 }
11313 }
11314 if sideband_active {
11317 self.make_record_lookaside.replace_cleared_buf(key_blob);
11318 }
11319 if count_logical_change && inserted {
11320 self.changes = self.changes.saturating_add(1);
11321 }
11322 pc += 1;
11323 }
11324
11325 Opcode::SorterInsert => {
11326 let cursor_id = op.p1;
11330 let record_reg = op.p2;
11331 let sideband_active =
11332 self.make_record_lookaside.sideband_is_armed_for(record_reg);
11333 let blob = if sideband_active {
11334 self.make_record_lookaside.disarm();
11335 self.make_record_lookaside.take_buf()
11336 } else {
11337 let record = self.take_reg(record_reg);
11341 record_blob_bytes(&record).to_vec()
11342 };
11343 if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
11344 let key_values =
11345 fsqlite_types::record::parse_record_prefix(&blob, sorter.key_columns)
11346 .ok_or_else(|| FrankenError::DatabaseCorrupt {
11347 detail: "malformed record in SorterInsert".to_owned(),
11348 })?;
11349 sorter.insert_row(key_values, blob)?;
11350 }
11351 pc += 1;
11352 }
11353
11354 Opcode::IdxDelete => {
11355 let cursor_id = op.p1;
11362 let key_start_reg = op.p2;
11363 let key_count = op.p3;
11364 let replace_victim = if op.p5 & OPFLAG_REPLACE_VICTIM != 0 {
11365 let (root_page, payload) = {
11366 let cursor = self
11367 .storage_cursors
11368 .get_mut(&cursor_id)
11369 .filter(|cursor| cursor.writable && !cursor.cursor.eof())
11370 .ok_or_else(|| {
11371 FrankenError::internal(
11372 "REPLACE victim capture requires a positioned writable cursor",
11373 )
11374 })?;
11375 let root_page = cursor.root_page;
11376 let payload = cursor.cursor.payload(&cursor.cx).await?;
11377 (root_page, payload)
11378 };
11379 let stored_values =
11380 parse_record(&payload).ok_or_else(|| FrankenError::DatabaseCorrupt {
11381 detail: "REPLACE victim capture encountered a malformed WITHOUT ROWID record"
11382 .to_owned(),
11383 })?;
11384 Some(self.logical_replace_victim(root_page, &stored_values, None))
11385 } else {
11386 None
11387 };
11388 let mut deleted = false;
11389
11390 let key_bytes: Option<Vec<u8>> = if key_count > 0 {
11392 let iter = (0..key_count).map(|i| self.get_reg(key_start_reg + i));
11393 Some(fsqlite_types::record::serialize_record_iter(iter))
11394 } else {
11395 None
11396 };
11397
11398 if let Some(ref key) = key_bytes {
11399 let writable = self
11400 .storage_cursors
11401 .get(&cursor_id)
11402 .map(|sc| sc.writable)
11403 .unwrap_or(false);
11404 if writable
11405 && self.storage_cursor_find_exact_index_key(
11406 cursor_id,
11407 key,
11408 "IdxDelete: missing collation registry for collated exact probe",
11409 )
11410 .await?
11411 {
11412 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11413 sc.last_rightmost_unique_index_prefix = None;
11414 sc.last_rightmost_unique_index_position = None;
11415 sc.cursor.delete(&sc.cx).await?;
11416 deleted = true;
11417 invalidate_storage_cursor_row_cache_with_reason(
11418 sc,
11419 self.collect_vdbe_metrics,
11420 DecodeCacheInvalidationReason::WriteMutation,
11421 );
11422 }
11423 }
11424 } else if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11425 if sc.writable && !sc.cursor.eof() {
11426 sc.last_rightmost_unique_index_prefix = None;
11428 sc.last_rightmost_unique_index_position = None;
11429 sc.cursor.delete(&sc.cx).await?;
11430 deleted = true;
11431 invalidate_storage_cursor_row_cache_with_reason(
11432 sc,
11433 self.collect_vdbe_metrics,
11434 DecodeCacheInvalidationReason::WriteMutation,
11435 );
11436 }
11437 }
11438 if deleted && let Some(replace_victim) = replace_victim {
11439 self.replace_victims.push(replace_victim);
11440 }
11441 pc += 1;
11443 }
11444
11445 Opcode::SorterCompare => {
11446 let cursor_id = op.p1;
11451 let is_top_n_preflight = (op.p5 & SORTER_COMPARE_TOP_N_PREFLIGHT) != 0;
11452 let preflight_sorter_exists =
11453 !is_top_n_preflight || self.sorters.get(&cursor_id).is_some();
11454 let preflight_needs_compare = is_top_n_preflight
11455 && self.sorters.get(&cursor_id).is_some_and(|sorter| {
11456 sorter
11457 .top_n_limit
11458 .is_some_and(|limit| sorter.rows.len() >= limit)
11459 });
11460 let preflight_probe = if is_top_n_preflight {
11461 if self.make_record_lookaside.sideband_is_armed_for(op.p3) {
11462 self.make_record_lookaside.disarm();
11467 let probe_buf = self.make_record_lookaside.take_buf();
11468 let decoded = preflight_needs_compare
11469 .then(|| {
11470 decode_record_bytes_with_metrics(
11471 &probe_buf,
11472 self.collect_vdbe_metrics,
11473 )
11474 })
11475 .transpose();
11476 self.make_record_lookaside.replace_cleared_buf(probe_buf);
11477 decoded?
11478 } else if preflight_needs_compare {
11479 Some(decode_record_with_metrics(
11480 self.get_reg(op.p3),
11481 self.collect_vdbe_metrics,
11482 )?)
11483 } else {
11484 None
11485 }
11486 } else {
11487 None
11488 };
11489 let ordinary_needs_compare = !is_top_n_preflight
11490 && self.sorters.get(&cursor_id).is_some_and(|sorter| {
11491 sorter
11492 .position
11493 .is_some_and(|position| sorter.rows.get(position).is_some())
11494 });
11495 let ordinary_probe = if ordinary_needs_compare {
11496 if self.make_record_lookaside.sideband_is_armed_for(op.p3) {
11497 Some(decode_record_bytes_with_metrics(
11502 self.make_record_lookaside.as_slice(),
11503 self.collect_vdbe_metrics,
11504 )?)
11505 } else {
11506 Some(decode_record_with_metrics(
11507 self.get_reg(op.p3),
11508 self.collect_vdbe_metrics,
11509 )?)
11510 }
11511 } else {
11512 None
11513 };
11514 let coll = self.lock_collation();
11515 let should_jump = if is_top_n_preflight {
11516 if !preflight_sorter_exists {
11517 true
11518 } else if !preflight_needs_compare {
11519 false
11520 } else if let Some(sorter) = self.sorters.get(&cursor_id) {
11521 !sorter.would_retain_top_n(
11522 preflight_probe
11523 .as_deref()
11524 .expect("preflight probe should be decoded"),
11525 &coll,
11526 )
11527 } else {
11528 true
11529 }
11530 } else if let Some(sorter) = self.sorters.get(&cursor_id) {
11531 if let Some(pos) = sorter.position {
11532 if let Some(current) = sorter.rows.get(pos) {
11533 let probe = ordinary_probe
11534 .as_deref()
11535 .expect("positioned sorter probe should be decoded");
11536 !sorter_keys_equal(
11537 ¤t.values,
11538 probe,
11539 sorter.key_columns,
11540 &sorter.collations,
11541 &coll,
11542 )
11543 } else {
11544 true
11545 }
11546 } else {
11547 true
11548 }
11549 } else {
11550 true
11551 };
11552 if should_jump {
11553 pc = op.p2 as usize;
11554 } else {
11555 pc += 1;
11556 }
11557 }
11558
11559 Opcode::SorterData => {
11560 let cursor_id = op.p1;
11562 let target = op.p2;
11563 let value = if let Some(sorter) = self.sorters.get(&cursor_id) {
11564 if let Some(pos) = sorter.position {
11565 if let Some(row) = sorter.rows.get(pos) {
11566 SqliteValue::Blob(row.blob.clone().into())
11567 } else {
11568 SqliteValue::Null
11569 }
11570 } else {
11571 SqliteValue::Null
11572 }
11573 } else {
11574 SqliteValue::Null
11575 };
11576 self.set_reg(target, value);
11577 pc += 1;
11578 }
11579
11580 Opcode::RowCell => {
11581 pc += 1;
11582 }
11583
11584 Opcode::ResetCount => {
11585 pc += 1;
11586 }
11587
11588 Opcode::MakeRecord => {
11590 self.execute_make_record_hot(op, collect_vdbe_metrics);
11591 pc += 1;
11592 }
11593
11594 Opcode::Affinity => {
11595 if let P4::Affinity(aff) = &op.p4 {
11598 let start = op.p1;
11599 for (i, ch) in aff.chars().enumerate() {
11600 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
11601 let reg = start + i as i32;
11602 let val = self.take_reg(reg);
11603 let affinity = char_to_affinity(ch);
11604 if collect_vdbe_metrics {
11605 let before = val.clone();
11606 let coerced = val.apply_affinity(affinity);
11607 record_type_coercion(&before, &coerced);
11608 self.set_reg(reg, coerced);
11609 } else {
11610 self.set_reg(reg, val.apply_affinity(affinity));
11611 }
11612 }
11613 }
11614 pc += 1;
11615 }
11616
11617 Opcode::HaltIfNull => {
11619 if self.get_reg(op.p3).is_null() {
11620 let msg = match &op.p4 {
11621 P4::Str(s) => s.clone(),
11622 _ => "NOT NULL constraint failed".to_owned(),
11623 };
11624 break ExecOutcome::Error {
11625 code: op.p1,
11626 message: msg,
11627 };
11628 }
11629 pc += 1;
11630 }
11631
11632 Opcode::Count => {
11633 let cursor_id = op.p1;
11641 let count: i64 = if let Some(cursor) = self.cursors.get(&cursor_id) {
11642 if let Some(db) = self.db.as_ref()
11643 && let Some(table) = db.get_table(cursor.root_page)
11644 {
11645 i64::try_from(table.rows.len()).unwrap_or(0)
11646 } else {
11647 0
11648 }
11649 } else if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11650 sc.cursor.count_all_rows(&sc.cx).await?
11655 } else {
11656 0
11657 };
11658 self.set_reg_int(op.p2, count);
11659 pc += 1;
11660 }
11661
11662 Opcode::CountIndexEqRun => {
11663 let probe_value = self.get_reg(op.p3).clone();
11664 let matched = if probe_value.is_null() {
11665 0
11666 } else {
11667 let cursor = self.storage_cursors.get_mut(&op.p1).ok_or_else(|| {
11668 FrankenError::internal(
11669 "CountIndexEqRun requires an open storage cursor",
11670 )
11671 })?;
11672 if cursor.cursor.is_table_btree() {
11673 return Err(FrankenError::internal(
11674 "CountIndexEqRun requires an index cursor",
11675 ));
11676 }
11677 storage_cursor_count_equal_first_key_run(
11678 cursor,
11679 &probe_value,
11680 self.collect_vdbe_metrics,
11681 )
11682 .await?
11683 };
11684 let next_count = self.get_reg(op.p2).to_integer().wrapping_add(matched);
11685 self.set_reg_int(op.p2, next_count);
11686 pc += 1;
11687 }
11688
11689 Opcode::Sequence => {
11690 let counter = self
11691 .ensure_cold_state_for(StatementColdState::SEQUENCE_COUNTERS)
11692 .sequence_counters
11693 .entry(op.p1)
11694 .or_insert(0);
11695 let val = *counter;
11696 *counter += 1;
11697 self.set_reg_int(op.p2, val);
11698 pc += 1;
11699 }
11700
11701 Opcode::SequenceTest => {
11702 pc += 1;
11703 }
11704
11705 Opcode::Variable => {
11706 let idx = usize::try_from(op.p1)
11708 .ok()
11709 .and_then(|one_based| one_based.checked_sub(1));
11710 let value = idx
11711 .and_then(|idx| {
11712 borrowed_bindings
11713 .and_then(|bindings| bindings.get(idx))
11714 .or_else(|| self.bindings.get(idx))
11715 })
11716 .cloned()
11717 .unwrap_or(SqliteValue::Null);
11718 self.set_reg(op.p2, value);
11719 pc += 1;
11720 }
11721
11722 Opcode::BeginSubrtn => {
11723 self.set_reg(op.p2, SqliteValue::Null);
11724 pc += 1;
11725 }
11726
11727 Opcode::IsTrue => {
11728 let val = self.get_reg(op.p1);
11731 let p4_val = match &op.p4 {
11732 P4::Int(n) => *n,
11733 _ => 0,
11734 };
11735 if val.is_null() {
11736 self.set_reg(op.p2, SqliteValue::Integer(i64::from(op.p3 ^ p4_val)));
11737 } else {
11738 let v = i32::from(vdbe_real_is_truthy(val));
11739 self.set_reg(op.p2, SqliteValue::Integer(i64::from((v ^ p4_val) & 1)));
11740 }
11741 pc += 1;
11742 }
11743
11744 Opcode::ZeroOrNull => {
11745 if self.get_reg(op.p1).is_null() || self.get_reg(op.p3).is_null() {
11749 self.set_reg(op.p2, SqliteValue::Null);
11750 } else {
11751 self.set_reg(op.p2, SqliteValue::Integer(0));
11752 }
11753 pc += 1;
11754 }
11755
11756 Opcode::IfNullRow => {
11757 let is_null = if let Some(cursor) = self.storage_cursors.get(&op.p1) {
11760 cursor.cursor.eof()
11761 } else if let Some(state) = self
11762 .cold_state()
11763 .and_then(|cold_state| cold_state.vtab_cursors.get(&op.p1))
11764 {
11765 state.null_row || state.cursor.eof()
11766 } else {
11767 self.cursors
11768 .get(&op.p1)
11769 .is_none_or(|c| c.position.is_none() && !c.is_pseudo)
11770 };
11771 if is_null {
11772 if op.p3 > 0 {
11773 self.set_reg(op.p3, SqliteValue::Null);
11774 }
11775 pc = op.p2 as usize;
11776 } else {
11777 pc += 1;
11778 }
11779 }
11780
11781 Opcode::IfNotOpen => {
11782 if self.cursors.contains_key(&op.p1)
11784 || self.storage_cursors.contains_key(&op.p1)
11785 || self
11786 .cold_state()
11787 .is_some_and(|cold_state| cold_state.vtab_cursors.contains_key(&op.p1))
11788 || self.sorters.contains_key(&op.p1)
11789 {
11790 pc += 1;
11791 } else {
11792 pc = op.p2 as usize;
11793 }
11794 }
11795
11796 Opcode::Compare => {
11797 let start_a = op.p1;
11799 let start_b = op.p2;
11800 let count = op.p3;
11801 let coll_arc = Arc::clone(&self.collation_registry);
11802 let result = {
11803 let coll = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
11804 let mut result = Ordering::Equal;
11805 for i in 0..count {
11806 let val_a = self.get_reg(start_a + i);
11807 let val_b = self.get_reg(start_b + i);
11808 let coll_name = usize::try_from(i).ok().and_then(|field_idx| {
11809 compare_collation_for_field_from_p4(&op.p4, field_idx)
11810 });
11811 let ord = if let Some(coll_name) = coll_name {
11815 collate_compare(val_a, val_b, coll_name, &coll)
11816 } else {
11817 val_a.partial_cmp(val_b)
11818 };
11819 let o = match ord {
11820 Some(o) => o,
11821 None => {
11822 match (val_a.is_null(), val_b.is_null()) {
11824 (true, true) => Ordering::Equal,
11825 (true, false) => Ordering::Less,
11826 (false, true) => Ordering::Greater,
11827 (false, false) => Ordering::Equal,
11828 }
11829 }
11830 };
11831 if o != Ordering::Equal {
11832 result = o;
11833 break;
11834 }
11835 }
11836 result
11837 };
11838 self.last_compare_result = Some(result);
11839 pc += 1;
11840 }
11841
11842 Opcode::Jump => {
11843 let target = match self.last_compare_result {
11845 Some(Ordering::Less) => op.p1,
11846 Some(Ordering::Equal) => op.p2,
11847 Some(Ordering::Greater) => op.p3,
11848 None => {
11849 op.p2
11853 }
11854 };
11855 pc = target as usize;
11856 }
11857
11858 Opcode::TypeCheck => {
11859 let p4_str = match &op.p4 {
11861 P4::Affinity(s) | P4::Str(s) => s.as_str(),
11862 _ => "",
11863 };
11864 let mut parts = p4_str.split('\t');
11866 let pattern = parts.next().unwrap_or("").as_bytes();
11867 let table_name = parts.next().unwrap_or("");
11868 let col_names: Vec<&str> = parts.collect();
11869
11870 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
11871 let count = op.p2.max(0) as usize;
11872 for offset in 0..count {
11873 #[allow(clippy::cast_possible_wrap)]
11874 let reg = op.p1 + offset as i32;
11875 let value = self.get_reg(reg);
11876 let strict_type = match pattern.get(offset).copied().unwrap_or(b'A') {
11877 b'A' | b'a' => None,
11878 b'I' | b'i' => Some(StrictColumnType::Integer),
11879 b'R' | b'r' => Some(StrictColumnType::Real),
11880 b'T' | b't' => Some(StrictColumnType::Text),
11881 b'L' | b'l' => Some(StrictColumnType::Blob),
11882 other => {
11883 return Err(FrankenError::Internal(format!(
11884 "unknown STRICT type code '{}' in OP_TypeCheck",
11885 char::from(other)
11886 )));
11887 }
11888 };
11889
11890 if let Some(expected) = strict_type {
11891 let checked =
11892 value.clone().validate_strict(expected).map_err(|err| {
11893 let col_label = col_names
11894 .get(offset)
11895 .filter(|s| !s.is_empty())
11896 .map_or_else(
11897 || format!("column {offset}"),
11898 |name| {
11899 if table_name.is_empty() {
11900 (*name).to_owned()
11901 } else {
11902 format!("{table_name}.{name}")
11903 }
11904 },
11905 );
11906 let col_type = format!("{expected:?}").to_ascii_uppercase();
11907 let actual_str = err.actual.to_string();
11908 tracing::warn!(
11909 register = reg,
11910 expected = ?expected,
11911 actual = %actual_str,
11912 column = %col_label,
11913 value = ?value,
11914 "STRICT type violation"
11915 );
11916 FrankenError::DatatypeViolation {
11917 column: col_label,
11918 column_type: col_type,
11919 actual: actual_str,
11920 }
11921 })?;
11922 self.set_reg(reg, checked);
11923 }
11924 }
11925 pc += 1;
11926 }
11927
11928 Opcode::Permutation | Opcode::CollSeq | Opcode::ElseEq | Opcode::FkCheck => {
11929 pc += 1;
11930 }
11931
11932 Opcode::IsType => {
11933 let val_ref;
11940 let val = if op.p1 < 0 {
11941 self.get_reg(op.p3)
11942 } else {
11943 val_ref = self.cursor_column(op.p1, op.p3 as usize).await?;
11944 &val_ref
11945 };
11946 let type_bit: u16 = match val {
11947 SqliteValue::Integer(_) => 0x01,
11948 SqliteValue::Float(_) => 0x02,
11949 SqliteValue::Text(_) => 0x04,
11950 SqliteValue::Blob(_) => 0x08,
11951 SqliteValue::Null => 0x10,
11952 };
11953 if op.p5 & type_bit != 0 {
11954 pc = op.p2 as usize;
11955 } else {
11956 pc += 1;
11957 }
11958 }
11959
11960 Opcode::IfEmpty => {
11961 let cursor_id = op.p1;
11968 let empty = if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
11969 let had_row = sc.cursor.first(&sc.cx).await?;
11971 !had_row
11972 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
11973 if let Some(db) = self.db.as_ref()
11974 && let Some(table) = db.get_table(cursor.root_page)
11975 {
11976 table.rows.is_empty()
11977 } else {
11978 true }
11980 } else {
11981 true
11982 };
11983 if empty {
11984 pc = op.p2 as usize;
11985 } else {
11986 pc += 1;
11987 }
11988 }
11989
11990 Opcode::IfSizeBetween => {
11991 let cursor_id = op.p1;
11997 let row_count: i64 = if let Some(cursor) = self.cursors.get(&cursor_id) {
11998 if let Some(db) = self.db.as_ref()
11999 && let Some(table) = db.get_table(cursor.root_page)
12000 {
12001 i64::try_from(table.rows.len()).unwrap_or(0)
12002 } else {
12003 0
12004 }
12005 } else {
12006 -1
12009 };
12010 #[allow(clippy::cast_precision_loss)]
12011 let x = if row_count <= 0 {
12012 -10_i32 } else {
12014 ((row_count as f64).log2() * 10.0) as i32
12015 };
12016 let lo = op.p3;
12017 let hi = match &op.p4 {
12018 P4::Int(v) => *v,
12019 _ => i32::MAX,
12020 };
12021 if x >= lo && x <= hi {
12022 pc = op.p2 as usize;
12023 } else {
12024 pc += 1;
12025 }
12026 }
12027
12028 Opcode::IdxRowid => {
12029 let cursor_id = op.p1;
12034 let target = op.p2;
12035 let val = self.cursor_rowid(cursor_id).await?;
12036 self.set_reg_fast(target, val);
12037 pc += 1;
12038 }
12039
12040 Opcode::DeferredSeek | Opcode::FinishSeek => {
12041 pc += 1;
12042 }
12043
12044 Opcode::IdxLE | Opcode::IdxGT | Opcode::IdxLT | Opcode::IdxGE => {
12059 let cursor_id = op.p1;
12060 let probe_val = self.clone_reg_materialized(op.p3);
12061
12062 let desc_flags = self.index_desc_flags_for_cursor(cursor_id);
12063 let collations = self.index_collations_for_cursor(cursor_id);
12064
12065 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
12067 if sc.cursor.eof() {
12068 let jump = matches!(op.opcode, Opcode::IdxGT | Opcode::IdxGE);
12070 if jump {
12071 pc = op.p2 as usize;
12072 } else {
12073 pc += 1;
12074 }
12075 continue;
12076 }
12077
12078 sc.target_vals_buf.clear();
12079 if let SqliteValue::Blob(bytes) = &probe_val {
12080 fsqlite_types::record::parse_record_into(
12081 bytes,
12082 &mut sc.target_vals_buf,
12083 )
12084 .ok_or_else(|| {
12085 FrankenError::internal("index seek: malformed probe key record")
12086 })?;
12087 }
12088
12089 if op.p5 == 1
12090 && let Some(probe_first) = sc.target_vals_buf.first().cloned()
12091 {
12092 let coll_arc = Arc::clone(&self.collation_registry);
12093 let coll_registry =
12094 coll_arc.lock().unwrap_or_else(|e| e.into_inner()).clone();
12095 let cmp = storage_cursor_current_first_index_key_compare(
12096 sc,
12097 &probe_first,
12098 self.collect_vdbe_metrics,
12099 "IdxCmp: malformed index record at cursor position",
12100 desc_flags.first().copied().unwrap_or(false),
12101 collations
12102 .first()
12103 .and_then(|collation| collation.as_deref()),
12104 &coll_registry,
12105 )
12106 .await?;
12107
12108 if idx_compare_condition_met(op.opcode, cmp) {
12109 pc = op.p2 as usize;
12110 } else {
12111 pc += 1;
12112 }
12113 continue;
12114 }
12115
12116 if !try_decode_storage_cursor_current_index_record(cursor_id, sc).await? {
12117 return Err(FrankenError::internal(
12118 "IdxCmp: malformed index record at cursor position",
12119 ));
12120 }
12121
12122 let n_compare = if op.p5 > 0 {
12123 op.p5 as usize
12124 } else {
12125 sc.target_vals_buf.len()
12126 };
12127 let coll_arc = Arc::clone(&self.collation_registry);
12131 let coll_guard = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
12132 let cmp = compare_index_prefix_keys(
12133 &sc.cur_vals_buf,
12134 &sc.target_vals_buf,
12135 n_compare,
12136 &desc_flags,
12137 &collations,
12138 &coll_guard,
12139 );
12140 drop(coll_guard);
12141
12142 let condition_met = idx_compare_condition_met(op.opcode, cmp);
12143
12144 if condition_met {
12145 pc = op.p2 as usize;
12146 } else {
12147 pc += 1;
12148 }
12149 } else if let Some(cursor) = self.cursors.get(&cursor_id) {
12150 let probe_fields =
12152 decode_record_with_metrics(&probe_val, self.collect_vdbe_metrics)?;
12153 if let Some(pos) = cursor.position
12154 && let Some(db) = self.db.as_ref()
12155 && let Some(table) = db.get_table(cursor.root_page)
12156 && let Some(row) = table.rows.get(pos)
12157 {
12158 let n_compare = if op.p5 > 0 {
12159 op.p5 as usize
12160 } else {
12161 probe_fields.len()
12162 };
12163 let desc_flags = self.index_desc_flags_for_root(cursor.root_page);
12164 let collations = self.index_collations_for_root(cursor.root_page);
12165 let coll_guard = self.lock_collation();
12166 let cmp = compare_index_prefix_keys(
12167 &row.values,
12168 &probe_fields,
12169 n_compare,
12170 &desc_flags,
12171 &collations,
12172 &coll_guard,
12173 );
12174 drop(coll_guard);
12175 let condition_met = idx_compare_condition_met(op.opcode, cmp);
12176 if condition_met {
12177 pc = op.p2 as usize;
12178 } else {
12179 pc += 1;
12180 }
12181 } else {
12182 let jump = matches!(op.opcode, Opcode::IdxGT | Opcode::IdxGE);
12184 if jump {
12185 pc = op.p2 as usize;
12186 } else {
12187 pc += 1;
12188 }
12189 }
12190 } else {
12191 pc += 1;
12192 }
12193 }
12194
12195 Opcode::CreateBtree => {
12197 let target = op.p2;
12200 let root_page = if let Some(db) = self.db.as_mut() {
12201 db.create_table(0) } else {
12203 0
12204 };
12205 self.set_reg(target, SqliteValue::Integer(i64::from(root_page)));
12206 pc += 1;
12207 }
12208
12209 Opcode::Clear => {
12210 if let Some(db) = self.db.as_mut() {
12212 db.clear_table(op.p1);
12213 }
12214 pc += 1;
12215 }
12216
12217 Opcode::Destroy => {
12218 if let Some(db) = self.db.as_mut() {
12227 db.destroy_table(op.p1);
12228 }
12229 pc += 1;
12230 }
12231
12232 Opcode::SqlExec
12233 | Opcode::ParseSchema
12234 | Opcode::LoadAnalysis
12235 | Opcode::DropTable
12236 | Opcode::DropIndex
12237 | Opcode::DropTrigger => {
12238 pc += 1;
12239 }
12240
12241 Opcode::ResetSorter => {
12242 if let Some(sorter) = self.sorters.get_mut(&op.p1) {
12243 sorter.reset();
12244 }
12245 pc += 1;
12246 }
12247
12248 Opcode::Savepoint => {
12250 pc += 1;
12256 }
12257
12258 Opcode::Checkpoint => {
12260 pc += 1;
12262 }
12263
12264 Opcode::Program | Opcode::Param => {
12266 pc += 1;
12267 }
12268
12269 Opcode::InitCoroutine => {
12271 self.set_reg(op.p1, SqliteValue::Integer(i64::from(op.p3)));
12272 if op.p2 > 0 {
12273 pc = op.p2 as usize;
12274 } else {
12275 pc += 1;
12276 }
12277 }
12278
12279 Opcode::Yield => {
12280 let saved = self.get_reg(op.p1).to_integer();
12281 if saved < 0 || saved as usize >= ops.len() {
12282 return Err(FrankenError::Internal(format!(
12283 "Yield: coroutine address {} out of bounds",
12284 saved
12285 )));
12286 }
12287 let current = (pc + 1) as i32;
12288 self.set_reg(op.p1, SqliteValue::Integer(i64::from(current)));
12289 pc = saved as usize;
12290 }
12291
12292 Opcode::EndCoroutine => {
12293 let saved = self.get_reg(op.p1).to_integer();
12294 if saved < 0 || saved as usize >= ops.len() {
12295 return Err(FrankenError::Internal(format!(
12296 "EndCoroutine: coroutine address {} out of bounds",
12297 saved
12298 )));
12299 }
12300 pc = saved as usize;
12301 }
12302
12303 Opcode::AggStep | Opcode::AggStep1 => {
12310 let (func_name, agg_collation): (&str, Option<&str>) = match &op.p4 {
12311 P4::FuncName(name) => (name.as_str(), None),
12312 P4::FuncNameCollated(name, coll) => (name.as_str(), Some(coll.as_str())),
12313 _ => {
12314 return Err(FrankenError::Internal(
12315 "AggStep opcode missing P4::FuncName".to_owned(),
12316 ));
12317 }
12318 };
12319
12320 let arg_count = i32::from(op.p5);
12321 let func_pc = pc;
12322 let func = if let Some(func) = self.aggregate_function_cache.get(&func_pc) {
12323 Arc::clone(func)
12324 } else {
12325 let registry = self.func_registry.as_ref().ok_or_else(|| {
12326 FrankenError::Internal(
12327 "AggStep opcode executed without function registry".to_owned(),
12328 )
12329 })?;
12330 let func =
12331 registry
12332 .find_aggregate(func_name, arg_count)
12333 .ok_or_else(|| {
12334 FrankenError::Internal(format!(
12335 "no such aggregate function: {func_name}/{arg_count}",
12336 ))
12337 })?;
12338 self.aggregate_function_cache
12339 .insert(func_pc, Arc::clone(&func));
12340 func
12341 };
12342
12343 let accum_reg = op.p3;
12344 let is_distinct = op.p1 != 0;
12345 let arg_count = usize::from(op.p5);
12346 let execution_cx = self.execution_cx.clone();
12347 if op.p2 >= 0
12348 && (op.p2 as usize).saturating_add(arg_count) <= self.registers.len()
12349 {
12350 for offset in 0..arg_count {
12351 if let Some(reg) = Self::reg_with_offset(op.p2, offset) {
12352 self.materialize_make_record_sideband(reg);
12353 }
12354 }
12355 let start_idx = op.p2 as usize;
12356 let args = &self.registers[start_idx..start_idx + arg_count];
12357 Self::agg_step_with_args(
12358 &mut self.cold_state,
12359 &mut self.statement_cold_state,
12360 AggStepCall {
12361 accum_reg,
12362 is_distinct,
12363 func: &func,
12364 func_name,
12365 agg_collation,
12366 execution_cx: &execution_cx,
12367 args,
12368 },
12369 )?;
12370 } else {
12371 let args = self.collect_reg_range(op.p2, arg_count);
12372 Self::agg_step_with_args(
12373 &mut self.cold_state,
12374 &mut self.statement_cold_state,
12375 AggStepCall {
12376 accum_reg,
12377 is_distinct,
12378 func: &func,
12379 func_name,
12380 agg_collation,
12381 execution_cx: &execution_cx,
12382 args: &args,
12383 },
12384 )?;
12385 }
12386 pc += 1;
12387 }
12388
12389 Opcode::AggFinal => {
12390 let func_name = match &op.p4 {
12391 P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12392 _ => {
12393 return Err(FrankenError::Internal(
12394 "AggFinal opcode missing P4::FuncName".to_owned(),
12395 ));
12396 }
12397 };
12398
12399 let arg_count = op.p2;
12400 let func_pc = pc;
12401 let func = if let Some(func) = self.aggregate_function_cache.get(&func_pc) {
12402 Arc::clone(func)
12403 } else {
12404 let registry = self.func_registry.as_ref().ok_or_else(|| {
12405 FrankenError::Internal(
12406 "AggFinal opcode executed without function registry".to_owned(),
12407 )
12408 })?;
12409 let func =
12410 registry
12411 .find_aggregate(func_name, arg_count)
12412 .ok_or_else(|| {
12413 FrankenError::Internal(format!(
12414 "no such aggregate function: {func_name}/{arg_count}",
12415 ))
12416 })?;
12417 self.aggregate_function_cache
12418 .insert(func_pc, Arc::clone(&func));
12419 func
12420 };
12421
12422 let accum_reg = op.p1;
12423 observe_execution_cancellation(&self.execution_cx)?;
12424 let result = match self
12425 .cold_state_mut()
12426 .and_then(|cold_state| cold_state.aggregates.remove(&accum_reg))
12427 {
12428 Some(ctx) => {
12429 if !Arc::ptr_eq(&ctx.func, &func) {
12430 return Err(FrankenError::Internal(
12431 "AggFinal accumulator used for a different aggregate"
12432 .to_owned(),
12433 ));
12434 }
12435 ctx.func.finalize(ctx.state)?
12436 }
12437 None => func.finalize(func.initial_state())?,
12438 };
12439
12440 observe_execution_cancellation(&self.execution_cx)?;
12441 self.set_reg(accum_reg, result);
12442 pc += 1;
12443 }
12444
12445 Opcode::AggInverse => {
12446 let func_name = match &op.p4 {
12451 P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12452 _ => {
12453 return Err(FrankenError::Internal(
12454 "AggInverse opcode missing P4::FuncName".to_owned(),
12455 ));
12456 }
12457 };
12458
12459 let registry = self.func_registry.as_ref().ok_or_else(|| {
12460 FrankenError::Internal(
12461 "AggInverse opcode executed without function registry".to_owned(),
12462 )
12463 })?;
12464
12465 let arg_count = i32::from(op.p5);
12466 let func = registry.find_window(func_name, arg_count).ok_or_else(|| {
12467 FrankenError::Internal(format!(
12468 "no such window function: {func_name}/{arg_count}",
12469 ))
12470 })?;
12471
12472 let accum_reg = op.p3;
12473 let args = self.collect_reg_range(op.p2, usize::from(op.p5));
12474 let execution_cx = self.execution_cx.clone();
12475 let ctx = self
12476 .ensure_cold_state_for(StatementColdState::WINDOW_CONTEXTS)
12477 .window_contexts
12478 .entry_or_insert_with(accum_reg, || {
12479 let state = func.initial_state();
12480 WindowContext {
12481 func: func.clone(),
12482 state,
12483 }
12484 });
12485
12486 observe_execution_cancellation(&execution_cx)?;
12487 ctx.func.inverse(&mut ctx.state, &args)?;
12488 observe_execution_cancellation(&execution_cx)?;
12489 pc += 1;
12490 }
12491
12492 Opcode::AggValue => {
12493 let func_name = match &op.p4 {
12500 P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12501 _ => {
12502 return Err(FrankenError::Internal(
12503 "AggValue opcode missing P4::FuncName".to_owned(),
12504 ));
12505 }
12506 };
12507
12508 let registry = self.func_registry.as_ref().ok_or_else(|| {
12509 FrankenError::Internal(
12510 "AggValue opcode executed without function registry".to_owned(),
12511 )
12512 })?;
12513
12514 let arg_count = op.p2;
12515 let func = registry.find_window(func_name, arg_count).ok_or_else(|| {
12516 FrankenError::Internal(format!(
12517 "no such window function: {func_name}/{arg_count}",
12518 ))
12519 })?;
12520
12521 let accum_reg = op.p1;
12522 observe_execution_cancellation(&self.execution_cx)?;
12523 let result = match self
12524 .cold_state()
12525 .and_then(|cold_state| cold_state.window_contexts.get(&accum_reg))
12526 {
12527 Some(ctx) => ctx.func.value(&ctx.state)?,
12528 None => func.value(&func.initial_state())?,
12529 };
12530 observe_execution_cancellation(&self.execution_cx)?;
12531 self.set_reg(op.p3, result);
12532 pc += 1;
12533 }
12534
12535 Opcode::Function | Opcode::PureFunc => {
12541 let func_name = match &op.p4 {
12542 P4::FuncName(name) | P4::FuncNameCollated(name, _) => name.as_str(),
12543 _ => {
12544 return Err(FrankenError::Internal(
12545 "Function opcode missing P4::FuncName".to_owned(),
12546 ));
12547 }
12548 };
12549 let arg_count = op.p5 as usize;
12550 let first_arg_reg = op.p2;
12551 let output_reg = op.p3;
12552
12553 let func_pc = pc;
12554 #[allow(clippy::cast_possible_wrap)]
12555 let (func, schema_safety, function_consumes_argument_collation) =
12556 if let Some(resolved) = self.scalar_function_cache.get(&func_pc) {
12557 (
12558 resolved.function(),
12559 resolved.schema_safety(),
12560 resolved.consumes_argument_collation(),
12561 )
12562 } else {
12563 let registry = self.func_registry.as_ref().ok_or_else(|| {
12564 FrankenError::Internal(
12565 "Function opcode executed without function registry".to_owned(),
12566 )
12567 })?;
12568 let resolved = registry
12569 .resolve_scalar_precanonical(func_name, arg_count as i32)
12570 .or_else(|| registry.resolve_scalar(func_name, arg_count as i32))
12571 .ok_or_else(|| {
12572 registry
12573 .resolve_application_function(func_name, arg_count as i32)
12574 .filter(|resolution| {
12575 resolution.kind() != ApplicationFunctionKind::Scalar
12576 })
12577 .map_or_else(
12578 || {
12579 FrankenError::Internal(format!(
12580 "no such function: {func_name}/{arg_count}",
12581 ))
12582 },
12583 |resolution| {
12584 FrankenError::function_error(format!(
12585 "misuse of {} function {}()",
12586 resolution.kind().label(),
12587 func_name.to_ascii_lowercase(),
12588 ))
12589 },
12590 )
12591 })?;
12592 let func = resolved.function();
12593 let schema_safety = resolved.schema_safety();
12594 let consumes_argument_collation =
12595 resolved.consumes_argument_collation();
12596 self.scalar_function_cache.insert(func_pc, resolved);
12597 (func, schema_safety, consumes_argument_collation)
12598 };
12599
12600 let arg_subtypes: smallvec::SmallVec<[u32; 4]> = (0..arg_count)
12605 .map(|i| {
12606 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
12607 let reg = first_arg_reg + i as i32;
12608 self.register_subtype(reg).unwrap_or(0)
12609 })
12610 .collect();
12611 let any_arg_subtype = arg_subtypes.iter().any(|&st| st != 0);
12612 let function_collation = if function_consumes_argument_collation {
12613 match &op.p4 {
12614 P4::FuncNameCollated(_, collation_name) => {
12615 let registry = self
12616 .collation_registry
12617 .lock()
12618 .unwrap_or_else(std::sync::PoisonError::into_inner);
12619 Some(registry.find(collation_name).ok_or_else(|| {
12620 FrankenError::function_error(format!(
12621 "no such collation sequence: {collation_name}"
12622 ))
12623 })?)
12624 }
12625 P4::FuncName(_) => None,
12626 _ => unreachable!("function P4 was validated above"),
12627 }
12628 } else {
12629 None
12630 };
12631
12632 let result = if first_arg_reg >= 0
12637 && (first_arg_reg as usize).saturating_add(arg_count)
12638 <= self.registers.len()
12639 {
12640 let start_idx = first_arg_reg as usize;
12641 let end_idx = start_idx + arg_count;
12642 let args = &self.registers[start_idx..end_idx];
12643 if let Some(context) = SchemaEvaluationContext::from_function_p1(op.p1) {
12644 validate_schema_function_invocation(
12645 context,
12646 func_name,
12647 schema_safety,
12648 args,
12649 )?;
12650 }
12651 observe_execution_cancellation(&self.execution_cx)?;
12652 if function_consumes_argument_collation {
12653 func.invoke_with_collation(args, function_collation.as_deref())?
12654 } else if any_arg_subtype {
12655 func.invoke_with_arg_subtypes(args, &arg_subtypes)?
12656 } else {
12657 func.invoke(args)?
12658 }
12659 } else {
12660 let args = self.collect_reg_range(first_arg_reg, arg_count);
12661 if let Some(context) = SchemaEvaluationContext::from_function_p1(op.p1) {
12662 validate_schema_function_invocation(
12663 context,
12664 func_name,
12665 schema_safety,
12666 &args,
12667 )?;
12668 }
12669 observe_execution_cancellation(&self.execution_cx)?;
12670 if function_consumes_argument_collation {
12671 func.invoke_with_collation(&args, function_collation.as_deref())?
12672 } else if any_arg_subtype {
12673 func.invoke_with_arg_subtypes(&args, &arg_subtypes)?
12674 } else {
12675 func.invoke(&args)?
12676 }
12677 };
12678 observe_execution_cancellation(&self.execution_cx)?;
12679
12680 let result_subtype = func.result_subtype();
12683
12684 if self.trace_opcodes {
12685 let result_type = match &result {
12686 SqliteValue::Null => "null",
12687 SqliteValue::Integer(_) => "integer",
12688 SqliteValue::Float(_) => "real",
12689 SqliteValue::Text(_) => "text",
12690 SqliteValue::Blob(_) => "blob",
12691 };
12692 tracing::trace!(
12693 target: "fsqlite_func::eval",
12694 func_name,
12695 arg_count,
12696 result_type,
12697 "func_eval",
12698 );
12699 }
12700
12701 fsqlite_func::record_func_call_count_only();
12703
12704 self.set_reg(output_reg, result);
12705 if let Some(subtype) = result_subtype {
12708 self.set_register_subtype(output_reg, subtype);
12709 }
12710 pc += 1;
12711 }
12712
12713 Opcode::LikeConstFast => {
12714 let kind = SqlLikeFastPathKind::from_opcode_tag(op.p3).ok_or_else(|| {
12715 FrankenError::Internal(
12716 "LikeConstFast opcode has invalid fast-path tag".to_owned(),
12717 )
12718 })?;
12719 let literal = match &op.p4 {
12720 P4::Str(text) => text.as_str(),
12721 _ => {
12722 return Err(FrankenError::Internal(
12723 "LikeConstFast opcode missing P4::Str literal".to_owned(),
12724 ));
12725 }
12726 };
12727 let input = self.get_reg(op.p1);
12728 let case_sensitive = fsqlite_func::case_sensitive_like_active();
12731 let result = if input.is_null() {
12732 SqliteValue::Null
12733 } else {
12734 let matched = match input {
12735 SqliteValue::Text(text) => sql_like_fast_path_matches_cased(
12736 kind,
12737 literal,
12738 text,
12739 case_sensitive,
12740 ),
12741 SqliteValue::Blob(bytes) => {
12742 let text = String::from_utf8_lossy(bytes);
12743 sql_like_fast_path_matches_cased(
12744 kind,
12745 literal,
12746 &text,
12747 case_sensitive,
12748 )
12749 }
12750 _ => {
12751 let text = input.to_text();
12752 sql_like_fast_path_matches_cased(
12753 kind,
12754 literal,
12755 &text,
12756 case_sensitive,
12757 )
12758 }
12759 };
12760 let final_match = if op.p5 != 0 { !matched } else { matched };
12761 SqliteValue::Integer(i64::from(final_match))
12762 };
12763 self.set_reg(op.p2, result);
12764 pc += 1;
12765 }
12766
12767 Opcode::DecrJumpZero => {
12772 let mut val = self.get_reg(op.p1).to_integer();
12773 if val > 0 {
12774 val -= 1;
12775 self.set_reg_int(op.p1, val);
12776 if val == 0 {
12777 #[allow(clippy::cast_sign_loss)]
12778 {
12779 pc = op.p2 as usize;
12780 }
12781 } else {
12782 pc += 1;
12783 }
12784 } else {
12785 pc += 1;
12786 }
12787 }
12788
12789 Opcode::IfPos => {
12792 let val = self.get_reg(op.p1).to_integer();
12793 if val > 0 {
12794 let decremented = val - i64::from(op.p3);
12795 self.set_reg_int(op.p1, decremented);
12796 #[allow(clippy::cast_sign_loss)]
12797 {
12798 pc = op.p2 as usize;
12799 }
12800 } else {
12801 pc += 1;
12802 }
12803 }
12804
12805 Opcode::RowSetAdd => {
12808 let rowset_reg = op.p1;
12810 let val = self.get_reg(op.p2).to_integer();
12811 self.ensure_cold_state_for(StatementColdState::ROWSETS)
12812 .rowsets
12813 .entry_or_insert_with(rowset_reg, RowSet::new)
12814 .add(val);
12815 pc += 1;
12816 }
12817
12818 Opcode::RowSetRead => {
12819 let rowset_reg = op.p1;
12822 let next_val = self
12823 .cold_state_mut()
12824 .and_then(|cold_state| cold_state.rowsets.get_mut(&rowset_reg))
12825 .and_then(|rs| rs.read_next());
12826 match next_val {
12827 Some(val) => {
12828 self.set_reg_int(op.p3, val);
12829 pc += 1;
12830 }
12831 None => {
12832 pc = op.p2 as usize;
12833 }
12834 }
12835 }
12836
12837 Opcode::RowSetTest => {
12838 let rowset_reg = op.p1;
12841 let val = self.get_reg(op.p3).to_integer();
12842 let found = self
12843 .cold_state()
12844 .and_then(|cold_state| cold_state.rowsets.get(&rowset_reg))
12845 .is_some_and(|rs| rs.contains(val));
12846 if found {
12847 pc = op.p2 as usize;
12848 } else {
12849 self.ensure_cold_state_for(StatementColdState::ROWSETS)
12850 .rowsets
12851 .entry_or_insert_with(rowset_reg, RowSet::new)
12852 .add(val);
12853 pc += 1;
12854 }
12855 }
12856
12857 Opcode::FkCounter => {
12859 self.fk_counter += i64::from(op.p2);
12862 pc += 1;
12863 }
12864
12865 Opcode::FkIfZero => {
12866 if self.fk_counter == 0 {
12869 pc = op.p2 as usize;
12870 } else {
12871 pc += 1;
12872 }
12873 }
12874
12875 Opcode::MemMax => {
12877 let val1 = self.get_reg(op.p1).to_integer();
12878 let val2 = self.get_reg(op.p2).to_integer();
12879 if val1 > val2 {
12880 self.set_reg_int(op.p2, val1);
12881 }
12882 pc += 1;
12883 }
12884
12885 Opcode::OffsetLimit => {
12892 let limit = self.get_reg(op.p1).to_integer();
12893 let offset = self.get_reg(op.p2).to_integer();
12894 let combined = if limit < 0 {
12895 -1
12896 } else {
12897 limit.saturating_add(offset)
12898 };
12899 self.set_reg_int(op.p3, combined);
12900 pc += 1;
12901 }
12902
12903 Opcode::IfNotZero => {
12905 let val = self.get_reg(op.p1).to_integer();
12906 if val != 0 {
12907 self.set_reg_int(op.p1, val.wrapping_sub(1));
12908 pc = op.p2 as usize;
12909 } else {
12910 pc += 1;
12911 }
12912 }
12913
12914 Opcode::Pagecount => {
12916 let count = self.db.as_ref().map_or(0, |db| db.table_count());
12919 self.set_reg(op.p2, SqliteValue::Integer(i64::from(count)));
12920 pc += 1;
12921 }
12922
12923 Opcode::MaxPgcnt => {
12924 self.set_reg(op.p2, SqliteValue::Integer(1_073_741_823));
12926 pc += 1;
12927 }
12928
12929 Opcode::JournalMode => {
12931 self.set_reg(op.p2, SqliteValue::Text(SmallText::new("wal")));
12934 pc += 1;
12935 }
12936
12937 Opcode::Vacuum | Opcode::IncrVacuum => {
12939 if op.opcode == Opcode::IncrVacuum {
12942 pc = op.p2 as usize;
12943 } else {
12944 pc += 1;
12945 }
12946 }
12947
12948 Opcode::IntegrityCk => {
12950 self.set_reg(op.p2, SqliteValue::Text(SmallText::new("ok")));
12954 pc += 1;
12955 }
12956
12957 Opcode::Expire => {
12959 pc += 1;
12962 }
12963
12964 Opcode::CursorLock | Opcode::CursorUnlock => {
12966 pc += 1;
12968 }
12969
12970 Opcode::ClrSubtype => {
12974 self.clear_register_subtype(op.p1);
12976 pc += 1;
12977 }
12978
12979 Opcode::GetSubtype => {
12980 let st = self.register_subtype(op.p1).unwrap_or(0);
12983 #[allow(clippy::cast_possible_wrap)]
12984 self.set_reg(op.p2, SqliteValue::Integer(st as i64));
12985 pc += 1;
12986 }
12987
12988 Opcode::SetSubtype => {
12989 let val = self.get_reg(op.p1);
12992 #[allow(clippy::cast_sign_loss)]
12993 let st = match val {
12994 SqliteValue::Integer(i) => *i as u32,
12995 _ => 0,
12996 };
12997 self.set_register_subtype(op.p2, st);
12998 pc += 1;
12999 }
13000
13001 Opcode::FilterAdd => {
13006 let hash = bloom_hash(self.get_reg(op.p3));
13009 let filter = self
13010 .ensure_cold_state_for(StatementColdState::BLOOM_FILTERS)
13011 .bloom_filters
13012 .entry(op.p1)
13013 .or_insert_with(|| vec![0u64; BLOOM_FILTER_WORDS]);
13014 let bit = (hash as usize) % (filter.len() * 64);
13015 filter[bit / 64] |= 1u64 << (bit % 64);
13016 pc += 1;
13017 }
13018
13019 Opcode::Filter => {
13020 if let Some(filter) = self
13023 .cold_state()
13024 .and_then(|cold_state| cold_state.bloom_filters.get(&op.p1))
13025 {
13026 let hash = bloom_hash(self.get_reg(op.p3));
13027 let bit = (hash as usize) % (filter.len() * 64);
13028 let present = (filter[bit / 64] >> (bit % 64)) & 1 == 1;
13029 if !present {
13030 pc = op.p2 as usize;
13031 } else {
13032 pc += 1;
13033 }
13034 } else {
13035 pc += 1;
13037 }
13038 }
13039
13040 Opcode::CursorHint | Opcode::Trace | Opcode::Abortable | Opcode::ReleaseReg => {
13042 pc += 1;
13044 }
13045
13046 Opcode::VOpen => {
13048 let cursor_id = op.p1;
13052 if !self
13053 .cold_state()
13054 .is_some_and(|cold_state| cold_state.vtab_cursors.contains_key(&cursor_id))
13055 {
13056 if let Some(vtab) = self.vtab_instances.get(&cursor_id) {
13057 match vtab.open_cursor() {
13058 Ok(cursor) => {
13059 self.register_vtab_cursor(cursor_id, cursor);
13060 }
13061 Err(e) => {
13062 break vtab_exec_outcome("VOpen", e)?;
13063 }
13064 }
13065 }
13066 }
13067 pc += 1;
13068 }
13069
13070 Opcode::VFilter => {
13071 let cursor_id = op.p1;
13077 let jump_if_empty = op.p2;
13078 let cx = self.derive_execution_cx();
13079 let idx_num_reg = self.get_reg(op.p3).to_integer();
13080 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
13081 let idx_num = idx_num_reg as i32;
13082 let argc = self.get_reg(op.p3 + 1).to_integer();
13083 let n_args = usize::try_from(argc).unwrap_or(0);
13084 let args: Vec<SqliteValue> = (0..n_args)
13085 .map(|i| {
13086 #[allow(clippy::cast_possible_wrap)]
13087 self.registers
13088 .get((op.p3 + 2 + i as i32) as usize)
13089 .cloned()
13090 .unwrap_or(SqliteValue::Null)
13091 })
13092 .collect();
13093 let idx_str = match &op.p4 {
13094 P4::Str(s) => Some(s.as_str()),
13095 _ => None,
13096 };
13097
13098 if let Some(state) = self
13099 .cold_state_mut()
13100 .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&cursor_id))
13101 {
13102 observe_execution_cancellation(&cx)?;
13103 if let Err(e) = state.cursor.filter(&cx, idx_num, idx_str, &args) {
13104 break vtab_exec_outcome("VFilter", e)?;
13105 }
13106 state.null_row = false;
13107 observe_execution_cancellation(&cx)?;
13108 if state.cursor.eof() {
13109 #[allow(clippy::cast_sign_loss)]
13110 {
13111 pc = jump_if_empty as usize;
13112 }
13113 continue;
13114 }
13115 } else {
13116 #[allow(clippy::cast_sign_loss)]
13117 {
13118 pc = jump_if_empty as usize;
13119 }
13120 continue;
13121 }
13122 pc += 1;
13123 }
13124
13125 Opcode::VColumn => {
13126 let cursor_id = op.p1;
13131 let col = op.p2;
13132 let dest = op.p3;
13133
13134 let column_value = if let Some(state) = self
13135 .cold_state()
13136 .and_then(|cold_state| cold_state.vtab_cursors.get(&cursor_id))
13137 {
13138 if state.null_row || state.cursor.eof() {
13139 SqliteValue::Null
13140 } else {
13141 observe_execution_cancellation(&self.execution_cx)?;
13142 let mut ctx = ColumnContext::new();
13143 if let Err(e) = state.cursor.column(&mut ctx, col) {
13144 break vtab_exec_outcome("VColumn", e)?;
13145 }
13146 observe_execution_cancellation(&self.execution_cx)?;
13147 ctx.take_value().unwrap_or(SqliteValue::Null)
13148 }
13149 } else {
13150 SqliteValue::Null
13151 };
13152 self.set_reg(dest, column_value);
13153 pc += 1;
13154 }
13155
13156 Opcode::VNext => {
13157 let cursor_id = op.p1;
13161 let jump_if_more = op.p2;
13162 let cx = self.derive_execution_cx();
13163
13164 if let Some(state) = self
13165 .cold_state_mut()
13166 .and_then(|cold_state| cold_state.vtab_cursors.get_mut(&cursor_id))
13167 {
13168 if state.null_row {
13169 pc += 1;
13170 continue;
13171 }
13172 observe_execution_cancellation(&cx)?;
13173 if let Err(e) = state.cursor.next(&cx) {
13174 break vtab_exec_outcome("VNext", e)?;
13175 }
13176 observe_execution_cancellation(&cx)?;
13177 if !state.cursor.eof() {
13178 #[allow(clippy::cast_sign_loss)]
13179 {
13180 pc = jump_if_more as usize;
13181 }
13182 continue;
13183 }
13184 }
13185 pc += 1;
13186 }
13187
13188 Opcode::VUpdate => {
13189 let cursor_id = op.p1;
13192 let n_args = op.p2;
13193 let first_reg = op.p3;
13194 let dest_reg = op.p5 as i32;
13195 let cx = self.derive_execution_cx();
13196 #[allow(clippy::cast_sign_loss)]
13197 let args: Vec<SqliteValue> = (0..n_args)
13198 .map(|i| {
13199 self.registers
13200 .get((first_reg + i) as usize)
13201 .cloned()
13202 .unwrap_or(SqliteValue::Null)
13203 })
13204 .collect();
13205 observe_execution_cancellation(&cx)?;
13206 let vtab_update_result =
13207 if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13208 match vtab.vtab_update(&cx, &args) {
13209 Ok(Some(rowid)) => SqliteValue::Integer(rowid),
13210 Ok(None) => SqliteValue::Null,
13211 Err(e) => {
13212 break vtab_exec_outcome("VUpdate", e)?;
13213 }
13214 }
13215 } else {
13216 SqliteValue::Null
13217 };
13218 observe_execution_cancellation(&cx)?;
13219 #[allow(clippy::cast_sign_loss)]
13220 if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13221 *reg = vtab_update_result;
13222 }
13223 pc += 1;
13224 }
13225
13226 Opcode::VBegin => {
13227 let cursor_id = op.p1;
13230 let cx = self.derive_execution_cx();
13231 observe_execution_cancellation(&cx)?;
13232 if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13233 if let Err(e) = vtab.begin(&cx) {
13234 break vtab_exec_outcome("VBegin", e)?;
13235 }
13236 }
13237 observe_execution_cancellation(&cx)?;
13238 pc += 1;
13239 }
13240
13241 Opcode::VCreate => {
13242 pc += 1;
13244 }
13245
13246 Opcode::VDestroy => {
13247 pc += 1;
13249 }
13250
13251 Opcode::VCheck => {
13252 let dest_reg = op.p2;
13257 #[allow(clippy::cast_sign_loss)]
13258 if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13259 *reg = SqliteValue::Null;
13260 }
13261 pc += 1;
13262 }
13263
13264 Opcode::VInitIn => {
13265 let _cursor_id = op.p1;
13269 let src_reg = op.p2;
13270 let dest_reg = op.p3;
13271 #[allow(clippy::cast_sign_loss)]
13272 {
13273 let val = self
13274 .registers
13275 .get(src_reg as usize)
13276 .cloned()
13277 .unwrap_or(SqliteValue::Null);
13278 if let Some(reg) = self.registers.get_mut(dest_reg as usize) {
13279 *reg = val;
13280 }
13281 }
13282 pc += 1;
13283 }
13284
13285 Opcode::VRename => {
13286 let cursor_id = op.p1;
13290 let cx = self.derive_execution_cx();
13291 observe_execution_cancellation(&cx)?;
13292 if let Some(vtab) = self.vtab_instances.get_mut(&cursor_id) {
13293 let new_name = match &op.p4 {
13294 P4::Str(s) => s.as_str(),
13295 _ => "",
13296 };
13297 if let Err(e) = vtab.rename(&cx, new_name) {
13298 break vtab_exec_outcome("VRename", e)?;
13299 }
13300 }
13301 observe_execution_cancellation(&cx)?;
13302 pc += 1;
13303 }
13304
13305 #[allow(unreachable_patterns)]
13307 _ => {
13308 break ExecOutcome::Error {
13309 code: 1,
13310 message: format!("unimplemented opcode {:?} at pc={}", op.opcode, pc),
13311 };
13312 }
13313 }
13314 };
13315
13316 if !needs_statement_timing {
13318 return Ok(outcome);
13319 }
13320
13321 let elapsed = start_time
13322 .expect("statement timing state exists when post-execution bookkeeping is enabled")
13323 .elapsed();
13324 let elapsed_us = elapsed.as_micros();
13325 let result_rows = self.results.len();
13326
13327 if collect_vdbe_metrics {
13328 let local_opcode_execution_totals = local_opcode_execution_totals
13329 .as_deref()
13330 .expect("opcode metrics buffer exists when VDBE metrics are enabled");
13331 FSQLITE_VDBE_OPCODES_EXECUTED_TOTAL.fetch_add(opcode_count, AtomicOrdering::Relaxed);
13332 FSQLITE_VDBE_STATEMENTS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
13333 #[allow(clippy::cast_possible_truncation)]
13334 FSQLITE_VDBE_STATEMENT_DURATION_US_TOTAL
13335 .fetch_add(elapsed_us as u64, AtomicOrdering::Relaxed);
13336 for (idx, total) in local_opcode_execution_totals.iter().enumerate().skip(1) {
13337 if *total == 0 {
13338 continue;
13339 }
13340 FSQLITE_VDBE_OPCODE_EXECUTION_TOTALS[idx]
13341 .fetch_add(*total, AtomicOrdering::Relaxed);
13342 }
13343 }
13344
13345 let log_statement_done = || {
13346 if statement_debug_enabled {
13347 tracing::debug!(
13348 target: "fsqlite_vdbe::statement",
13349 program_id,
13350 opcode_count,
13351 result_rows,
13352 elapsed_us = elapsed_us as u64,
13353 outcome = ?outcome,
13354 "vdbe statement done",
13355 );
13356 }
13357
13358 if slow_query_info_enabled && elapsed.as_millis() >= SLOW_QUERY_THRESHOLD_MS {
13359 #[allow(clippy::cast_possible_truncation)]
13360 let millis = elapsed.as_millis() as u64;
13361 tracing::info!(
13362 target: "fsqlite_vdbe::slow_query",
13363 program_id,
13364 opcode_count,
13365 result_rows,
13366 elapsed_ms = millis,
13367 "slow vdbe statement",
13368 );
13369 }
13370 };
13371
13372 if exec_info_enabled {
13373 let span = tracing::info_span!(
13374 target: "fsqlite_vdbe",
13375 "vdbe_exec",
13376 opcode_count,
13377 program_id,
13378 result_rows,
13379 elapsed_us = elapsed_us as u64,
13380 );
13381 let _guard = span.enter();
13382 log_statement_done();
13383 } else {
13384 log_statement_done();
13385 }
13386
13387 Ok(outcome)
13388 }
13389
13390 pub fn results(&self) -> &[smallvec::SmallVec<[SqliteValue; 16]>] {
13392 &self.results
13393 }
13394
13395 pub fn take_results(&mut self) -> Vec<smallvec::SmallVec<[SqliteValue; 16]>> {
13397 let mut results = Vec::with_capacity(self.results.capacity());
13398 std::mem::swap(&mut results, &mut self.results);
13399 results
13400 }
13401
13402 pub fn take_exactly_one_result_row(&mut self) -> ExactResultRowOutcome {
13404 match self.results.len() {
13405 0 => ExactResultRowOutcome::NoRows,
13406 1 => ExactResultRowOutcome::Row(Box::new(
13407 self.results
13408 .pop()
13409 .expect("one-row result set should contain one row"),
13410 )),
13411 _ => {
13412 self.results.clear();
13413 ExactResultRowOutcome::MultipleRows
13414 }
13415 }
13416 }
13417
13418 #[cfg(test)]
13419 fn result_buffer_capacity(&self) -> usize {
13420 self.results.capacity()
13421 }
13422
13423 #[inline(always)]
13430 #[allow(clippy::inline_always)]
13431 fn get_reg(&self, r: i32) -> &SqliteValue {
13432 if r >= 0 && (r as usize) < self.registers.len() {
13433 &self.registers[r as usize]
13434 } else {
13435 &SqliteValue::Null
13436 }
13437 }
13438
13439 fn reg_with_offset(start: i32, offset: usize) -> Option<i32> {
13440 i32::try_from(offset)
13441 .ok()
13442 .and_then(|delta| start.checked_add(delta))
13443 }
13444
13445 fn register_range_contains(start: i32, count: usize, register: i32) -> bool {
13446 if count == 0 {
13447 return false;
13448 }
13449 let Ok(count) = i64::try_from(count) else {
13450 return true;
13451 };
13452 let start = i64::from(start);
13453 let register = i64::from(register);
13454 register >= start && register < start + count
13455 }
13456
13457 #[inline]
13458 fn invalidate_make_record_sideband_if_overwritten(&mut self, r: i32) {
13459 self.make_record_lookaside.clear_sideband_for_register(r);
13460 }
13461
13462 #[inline]
13463 #[allow(clippy::cast_sign_loss)]
13464 fn materialize_make_record_sideband(&mut self, r: i32) {
13465 if !self.make_record_lookaside.sideband_is_armed_for(r) {
13466 return;
13467 }
13468 if !(0..=65535).contains(&r) {
13469 self.make_record_lookaside.reset();
13470 return;
13471 }
13472 let idx = r as usize;
13473 if idx >= self.registers.len() {
13474 self.registers.resize(idx + 1, SqliteValue::Null);
13475 }
13476 self.clear_register_subtype(r);
13477 #[cfg(test)]
13478 FSQLITE_VDBE_MAKE_RECORD_SIDEBAND_MATERIALIZATIONS_TOTAL.with(|counter| {
13479 counter.set(counter.get().saturating_add(1));
13480 });
13481 let buf = self.make_record_lookaside.take_buf();
13482 self.replace_register_value(idx, SqliteValue::Blob(Arc::<[u8]>::from(buf)));
13483 self.make_record_lookaside.disarm();
13484 }
13485
13486 #[inline]
13487 fn replace_register_value(&mut self, idx: usize, value: SqliteValue) {
13488 let old_value = std::mem::replace(&mut self.registers[idx], value);
13489 pool_return_reusable(old_value);
13490 }
13491
13492 #[inline]
13497 fn clone_reg_materialized(&mut self, r: i32) -> SqliteValue {
13498 self.materialize_make_record_sideband(r);
13499 self.get_reg(r).clone()
13500 }
13501
13502 #[inline(always)]
13509 async fn try_execute_hot_opcode(
13510 &mut self,
13511 op: &VdbeOp,
13512 pc: &mut usize,
13513 collect_vdbe_metrics: bool,
13514 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
13515 borrowed_bindings: Option<&[SqliteValue]>,
13516 ) -> Result<bool> {
13517 match op.opcode {
13518 Opcode::Column => {
13519 self.execute_column_hot(op).await?;
13520 *pc += 1;
13521 Ok(true)
13522 }
13523 Opcode::ColumnSubstrPrefix => {
13524 self.execute_column_substr_prefix_hot(op).await?;
13525 *pc += 1;
13526 Ok(true)
13527 }
13528 Opcode::ColumnOctetLength => {
13529 self.execute_column_octet_length_hot(op).await?;
13530 *pc += 1;
13531 Ok(true)
13532 }
13533 Opcode::ResultRow => {
13534 self.execute_result_row_hot(op, collect_vdbe_metrics, row_handler)?;
13535 *pc += 1;
13536 Ok(true)
13537 }
13538 Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
13539 self.execute_comparison_jump_hot(op, pc)?;
13540 Ok(true)
13541 }
13542 Opcode::MakeRecord => {
13543 self.execute_make_record_hot(op, collect_vdbe_metrics);
13544 *pc += 1;
13545 Ok(true)
13546 }
13547 Opcode::Integer => {
13552 self.set_reg_int(op.p2, i64::from(op.p1));
13553 *pc += 1;
13554 Ok(true)
13555 }
13556 Opcode::Int64 => {
13557 let val = match &op.p4 {
13558 P4::Int64(v) => *v,
13559 _ => 0,
13560 };
13561 self.set_reg_int(op.p2, val);
13562 *pc += 1;
13563 Ok(true)
13564 }
13565 Opcode::Variable => {
13566 let idx = usize::try_from(op.p1)
13567 .ok()
13568 .and_then(|one_based| one_based.checked_sub(1));
13569 let bound = idx.and_then(|idx| {
13570 borrowed_bindings
13571 .and_then(|bindings| bindings.get(idx))
13572 .or_else(|| self.bindings.get(idx))
13573 });
13574 if let Some(SqliteValue::Integer(v)) = bound {
13579 let v = *v;
13580 self.set_reg_int(op.p2, v);
13581 } else {
13582 let value = bound.cloned().unwrap_or(SqliteValue::Null);
13583 self.set_reg_fast(op.p2, value);
13584 }
13585 *pc += 1;
13586 Ok(true)
13587 }
13588 Opcode::Goto => {
13589 *pc = op.p2 as usize;
13590 Ok(true)
13591 }
13592 Opcode::Noop => {
13593 *pc += 1;
13594 Ok(true)
13595 }
13596 Opcode::Null => {
13597 let target = op.p2;
13599 if op.p3 > 0 {
13600 for reg in target..=op.p3 {
13601 self.set_reg_null(reg);
13602 }
13603 } else {
13604 self.set_reg_null(target);
13605 }
13606 *pc += 1;
13607 Ok(true)
13608 }
13609 Opcode::SoftNull => {
13610 self.set_reg_null(op.p1);
13611 *pc += 1;
13612 Ok(true)
13613 }
13614 Opcode::AddImm => {
13621 let val = self
13622 .get_reg(op.p1)
13623 .to_integer()
13624 .wrapping_add(i64::from(op.p2));
13625 self.set_reg_int(op.p1, val);
13626 *pc += 1;
13627 Ok(true)
13628 }
13629 Opcode::Add => {
13630 let a = self.get_reg(op.p2);
13631 let b = self.get_reg(op.p1);
13632 let result = a.sql_add(b);
13633 self.set_reg_arith_result(op.p3, result);
13634 *pc += 1;
13635 Ok(true)
13636 }
13637 Opcode::Subtract => {
13638 let a = self.get_reg(op.p2);
13639 let b = self.get_reg(op.p1);
13640 let result = a.sql_sub(b);
13641 self.set_reg_arith_result(op.p3, result);
13642 *pc += 1;
13643 Ok(true)
13644 }
13645 Opcode::Multiply => {
13646 let a = self.get_reg(op.p2);
13647 let b = self.get_reg(op.p1);
13648 let result = a.sql_mul(b);
13649 self.set_reg_arith_result(op.p3, result);
13650 *pc += 1;
13651 Ok(true)
13652 }
13653 Opcode::Divide => {
13654 let divisor = self.get_reg(op.p1);
13655 let dividend = self.get_reg(op.p2);
13656 let result = sql_div(dividend, divisor);
13657 self.set_reg_arith_result(op.p3, result);
13658 *pc += 1;
13659 Ok(true)
13660 }
13661 Opcode::Remainder => {
13662 let divisor = self.get_reg(op.p1);
13663 let dividend = self.get_reg(op.p2);
13664 let result = sql_rem(dividend, divisor);
13665 self.set_reg_fast(op.p3, result);
13666 *pc += 1;
13667 Ok(true)
13668 }
13669 Opcode::BitAnd => {
13670 let a = self.get_reg(op.p1);
13671 let b = self.get_reg(op.p2);
13672 if a.is_null() || b.is_null() {
13673 self.set_reg_fast(op.p3, SqliteValue::Null);
13674 } else {
13675 self.set_reg_int(op.p3, a.to_integer() & b.to_integer());
13676 }
13677 *pc += 1;
13678 Ok(true)
13679 }
13680 Opcode::BitOr => {
13681 let a = self.get_reg(op.p1);
13682 let b = self.get_reg(op.p2);
13683 if a.is_null() || b.is_null() {
13684 self.set_reg_fast(op.p3, SqliteValue::Null);
13685 } else {
13686 self.set_reg_int(op.p3, a.to_integer() | b.to_integer());
13687 }
13688 *pc += 1;
13689 Ok(true)
13690 }
13691 Opcode::BitNot => {
13692 let a = self.get_reg(op.p1);
13693 if a.is_null() {
13694 self.set_reg_fast(op.p2, SqliteValue::Null);
13695 } else {
13696 self.set_reg_int(op.p2, !a.to_integer());
13697 }
13698 *pc += 1;
13699 Ok(true)
13700 }
13701 Opcode::ShiftLeft => {
13702 let a = self.get_reg(op.p1);
13703 let b = self.get_reg(op.p2);
13704 if a.is_null() || b.is_null() {
13705 self.set_reg_fast(op.p3, SqliteValue::Null);
13706 } else {
13707 let result = sql_shift_left(b.to_integer(), a.to_integer());
13708 self.set_reg_fast(op.p3, result);
13709 }
13710 *pc += 1;
13711 Ok(true)
13712 }
13713 Opcode::ShiftRight => {
13714 let a = self.get_reg(op.p1);
13715 let b = self.get_reg(op.p2);
13716 if a.is_null() || b.is_null() {
13717 self.set_reg_fast(op.p3, SqliteValue::Null);
13718 } else {
13719 let result = sql_shift_right(b.to_integer(), a.to_integer());
13720 self.set_reg_fast(op.p3, result);
13721 }
13722 *pc += 1;
13723 Ok(true)
13724 }
13725 Opcode::Copy => {
13732 if op.p3 == 0 {
13733 self.copy_single_reg(op.p1, op.p2);
13734 } else {
13735 self.copy_reg_range(op.p1, op.p2, op.p3);
13736 }
13737 *pc += 1;
13738 Ok(true)
13739 }
13740 Opcode::SCopy => {
13744 self.copy_single_reg(op.p1, op.p2);
13745 *pc += 1;
13746 Ok(true)
13747 }
13748 Opcode::IntCopy => {
13749 let val = self.get_reg(op.p1).to_integer();
13750 self.set_reg_int(op.p2, val);
13751 *pc += 1;
13752 Ok(true)
13753 }
13754 Opcode::Move => {
13760 if op.p3 == 1 {
13761 let value = self.take_reg(op.p1);
13762 self.set_reg_fast(op.p2, value);
13763 } else {
13764 let count = usize::try_from(op.p3).unwrap_or(0);
13765 self.move_reg_range(op.p1, op.p2, count);
13766 }
13767 *pc += 1;
13768 Ok(true)
13769 }
13770 Opcode::DecrJumpZero => {
13778 let mut val = self.get_reg(op.p1).to_integer();
13779 if val > 0 {
13780 val -= 1;
13781 self.set_reg_int(op.p1, val);
13782 if val == 0 {
13783 #[allow(clippy::cast_sign_loss)]
13784 {
13785 *pc = op.p2 as usize;
13786 }
13787 } else {
13788 *pc += 1;
13789 }
13790 } else {
13791 *pc += 1;
13792 }
13793 Ok(true)
13794 }
13795 Opcode::IfPos => {
13803 let val = self.get_reg(op.p1).to_integer();
13804 if val > 0 {
13805 let decremented = val - i64::from(op.p3);
13806 self.set_reg_int(op.p1, decremented);
13807 #[allow(clippy::cast_sign_loss)]
13808 {
13809 *pc = op.p2 as usize;
13810 }
13811 } else {
13812 *pc += 1;
13813 }
13814 Ok(true)
13815 }
13816 Opcode::IfNotZero => {
13821 let val = self.get_reg(op.p1).to_integer();
13822 if val != 0 {
13823 self.set_reg_int(op.p1, val.wrapping_sub(1));
13824 #[allow(clippy::cast_sign_loss)]
13825 {
13826 *pc = op.p2 as usize;
13827 }
13828 } else {
13829 *pc += 1;
13830 }
13831 Ok(true)
13832 }
13833 Opcode::IsNull => {
13844 if self.get_reg(op.p1).is_null() {
13845 #[allow(clippy::cast_sign_loss)]
13846 {
13847 *pc = op.p2 as usize;
13848 }
13849 } else {
13850 *pc += 1;
13851 }
13852 Ok(true)
13853 }
13854 Opcode::IfNot => {
13864 let val = self.get_reg(op.p1);
13865 let should_jump = if val.is_null() {
13866 op.p3 != 0
13867 } else {
13868 !vdbe_real_is_truthy(val)
13869 };
13870 if should_jump {
13871 #[allow(clippy::cast_sign_loss)]
13872 {
13873 *pc = op.p2 as usize;
13874 }
13875 } else {
13876 *pc += 1;
13877 }
13878 Ok(true)
13879 }
13880 Opcode::IsTrue => {
13881 let val = self.get_reg(op.p1);
13882 let p4_val = match &op.p4 {
13883 P4::Int(n) => *n,
13884 _ => 0,
13885 };
13886 if val.is_null() {
13887 self.set_reg_int(op.p2, i64::from(op.p3 ^ p4_val));
13888 } else {
13889 let v = i32::from(vdbe_real_is_truthy(val));
13890 self.set_reg_int(op.p2, i64::from((v ^ p4_val) & 1));
13891 }
13892 *pc += 1;
13893 Ok(true)
13894 }
13895 Opcode::ZeroOrNull => {
13896 if self.get_reg(op.p1).is_null() || self.get_reg(op.p3).is_null() {
13897 self.set_reg_fast(op.p2, SqliteValue::Null);
13898 } else {
13899 self.set_reg_int(op.p2, 0);
13900 }
13901 *pc += 1;
13902 Ok(true)
13903 }
13904 Opcode::Not => {
13909 let val = self.get_reg(op.p1);
13910 if val.is_null() {
13911 self.set_reg_fast(op.p2, SqliteValue::Null);
13912 } else {
13913 self.set_reg_int(op.p2, i64::from(!vdbe_real_is_truthy(val)));
13914 }
13915 *pc += 1;
13916 Ok(true)
13917 }
13918 Opcode::Rowid => {
13931 let cursor_id = op.p1;
13932 let target = op.p2;
13933 let val = self.cursor_rowid(cursor_id).await?;
13934 self.set_reg_fast(target, val);
13935 *pc += 1;
13936 Ok(true)
13937 }
13938 Opcode::IdxRowid => {
13953 let cursor_id = op.p1;
13954 let target = op.p2;
13955 let val = self.cursor_rowid(cursor_id).await?;
13956 self.set_reg_fast(target, val);
13957 *pc += 1;
13958 Ok(true)
13959 }
13960 Opcode::FusedAppendInsert => {
13964 let cursor_id = op.p1;
13965 let first_reg = op.p2;
13966 let num_cols = usize::try_from(op.p3).unwrap_or(0);
13967 let concurrent_allocator = self.concurrent_rowid_allocator.clone();
13968 let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
13969 let previous_last_insert_rowid = self.last_insert_rowid;
13970 let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
13971
13972 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
13973 if sc.writable {
13974 let root_page = sc.root_page;
13975 let autoinc_max = sc.autoincrement_high_water;
13976 let rowid_mode = sc.rowid_mode;
13977 let rowid = if let Some(allocator) = concurrent_allocator.as_ref() {
13979 Self::allocate_concurrent_storage_rowid(
13980 allocator,
13981 concurrent_schema_epoch,
13982 root_page,
13983 rowid_mode,
13984 autoinc_max,
13985 sc,
13986 "rowid overflow in FusedAppendInsert",
13987 )
13988 .await?
13989 } else {
13990 Self::allocate_serialized_storage_rowid(
13991 sc,
13992 autoinc_max,
13993 "rowid overflow in FusedAppendInsert",
13994 )
13995 .await?
13996 };
13997
13998 let mut rec_buf = self.make_record_lookaside.take_buf();
14004 self.serialize_record_from_register_range(
14005 first_reg,
14006 num_cols,
14007 &op.p4,
14008 &mut rec_buf,
14009 );
14010
14011 let append_result =
14013 if let Some(sc) = self.storage_cursors.get_mut(&cursor_id) {
14014 let result = sc
14015 .cursor
14016 .table_append_after_last_position(&sc.cx, rowid, &rec_buf)
14017 .await;
14018 if result.is_ok() {
14019 sc.last_successful_insert_rowid = Some(rowid);
14020 }
14021 result
14022 } else {
14023 Err(FrankenError::internal(
14024 "cursor disappeared in FusedAppendInsert",
14025 ))
14026 };
14027
14028 rec_buf.clear();
14030 self.make_record_lookaside.replace_buf(rec_buf);
14031 append_result?;
14032
14033 self.changes += 1;
14035 self.last_insert_rowid = rowid;
14036 self.last_insert_rowid_valid = true;
14037 self.last_insert_cursor_id = Some(cursor_id);
14038 self.set_conflict_skip_idx(false);
14039 self.set_pending_insert_rollback(Some(PendingInsertRollback {
14040 cursor_id,
14041 rowid,
14042 previous_last_insert_rowid,
14043 previous_last_insert_rowid_valid,
14044 update_restore: None,
14045 }));
14046 self.pending_next_after_delete.remove(&cursor_id);
14047 self.mark_storage_root_page_dirty(root_page);
14052 } else {
14053 return Err(FrankenError::internal(
14054 "FusedAppendInsert: cursor is not writable",
14055 ));
14056 }
14057 } else if let Some(root_page) = self.cursors.get(&cursor_id).map(|c| c.root_page) {
14058 let rowid = self.db.as_mut().map_or(1, |db| db.alloc_rowid(root_page));
14062 let mut rec_buf = self.make_record_lookaside.take_buf();
14063 self.serialize_record_from_register_range(
14064 first_reg,
14065 num_cols,
14066 &op.p4,
14067 &mut rec_buf,
14068 );
14069 let values = parse_record(&rec_buf).ok_or_else(|| {
14070 FrankenError::internal("malformed SQLite record in TEMP FusedAppendInsert")
14071 })?;
14072 rec_buf.clear();
14073 self.make_record_lookaside.replace_buf(rec_buf);
14074
14075 let unique_conflicts = self
14076 .db
14077 .as_ref()
14078 .and_then(|db| db.get_table(root_page))
14079 .map(|table| table.find_unique_conflicts(&values))
14080 .unwrap_or_default();
14081 if !unique_conflicts.is_empty() {
14082 return Err(FrankenError::UniqueViolation {
14083 columns: "TEMP table unique constraint".to_owned(),
14084 });
14085 }
14086 let db = self.db.as_mut().ok_or_else(|| {
14087 FrankenError::internal(
14088 "FusedAppendInsert: TEMP MemDatabase is not attached",
14089 )
14090 })?;
14091 db.upsert_row(root_page, rowid, values);
14092
14093 self.changes += 1;
14094 self.last_insert_rowid = rowid;
14095 self.last_insert_rowid_valid = true;
14096 self.last_insert_cursor_id = Some(cursor_id);
14097 self.set_conflict_skip_idx(false);
14098 self.set_pending_insert_rollback(Some(PendingInsertRollback {
14099 cursor_id,
14100 rowid,
14101 previous_last_insert_rowid,
14102 previous_last_insert_rowid_valid,
14103 update_restore: None,
14104 }));
14105 self.pending_next_after_delete.remove(&cursor_id);
14106 } else {
14107 return Err(FrankenError::internal(format!(
14108 "FusedAppendInsert: no cursor for id {cursor_id}"
14109 )));
14110 }
14111 *pc += 1;
14112 Ok(true)
14113 }
14114 Opcode::FusedLiteralResultRow => {
14124 let lit = i64::from(op.p1);
14125 let reg = op.p2;
14126 self.set_reg_int(reg, lit);
14129
14130 let should_retain_row = self.collect_result_rows
14131 && match self.max_collected_result_rows {
14132 Some(limit) => self.results.len() < limit,
14133 None => true,
14134 };
14135 if row_handler.is_some() || should_retain_row {
14136 let materialize_start = collect_vdbe_metrics.then(Instant::now);
14137 let mut row: smallvec::SmallVec<[SqliteValue; 16]> =
14140 smallvec::SmallVec::with_capacity(1);
14141 row.push(self.take_reg(reg));
14142 if collect_vdbe_metrics {
14143 if let Some(materialize_start) = materialize_start {
14144 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14145 u64::try_from(materialize_start.elapsed().as_nanos())
14146 .unwrap_or(u64::MAX),
14147 AtomicOrdering::Relaxed,
14148 );
14149 }
14150 record_result_row_metrics(&row);
14151 }
14152 if let Some(handler) = row_handler.as_mut() {
14153 (*handler)(row)?;
14154 } else {
14155 self.results.push(row);
14156 }
14157 } else {
14158 let _ = self.take_reg(reg);
14161 }
14162 *pc += 1;
14163 Ok(true)
14164 }
14165 Opcode::String8 => {
14166 match &op.p4 {
14167 P4::Str(s) => self.write_text_to_reg(op.p2, s),
14168 _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
14169 }
14170 *pc += 1;
14171 Ok(true)
14172 }
14173 Opcode::String => {
14174 match &op.p4 {
14175 P4::Str(s) => self.write_text_to_reg(op.p2, s),
14176 _ => self.set_reg_fast(op.p2, SqliteValue::Text(SmallText::new(""))),
14177 }
14178 *pc += 1;
14179 Ok(true)
14180 }
14181 Opcode::Real => {
14182 let val = match &op.p4 {
14183 P4::Real(v) => *v,
14184 _ => 0.0,
14185 };
14186 self.set_reg_real(op.p2, val);
14187 *pc += 1;
14188 Ok(true)
14189 }
14190 Opcode::If => {
14191 let val = self.get_reg(op.p1);
14192 let should_jump = if val.is_null() {
14193 op.p3 != 0
14194 } else {
14195 vdbe_real_is_truthy(val)
14196 };
14197 if should_jump {
14198 #[allow(clippy::cast_sign_loss)]
14199 {
14200 *pc = op.p2 as usize;
14201 }
14202 } else {
14203 *pc += 1;
14204 }
14205 Ok(true)
14206 }
14207 Opcode::Next | Opcode::SorterNext => {
14208 self.execute_next_hot(op, pc).await?;
14209 Ok(true)
14210 }
14211 _ => Ok(false),
14212 }
14213 }
14214
14215 #[inline(always)]
14216 async fn execute_next_hot(&mut self, op: &VdbeOp, pc: &mut usize) -> Result<()> {
14217 let cursor_id = op.p1;
14218 let has_next = if !self.pending_next_after_delete.is_empty()
14219 && self.pending_next_after_delete.remove(&cursor_id)
14220 {
14221 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
14222 !cursor.cursor.eof()
14223 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
14224 if cursor.is_pseudo {
14225 false
14226 } else if let Some(pos) = cursor.position {
14227 if let Some(table) = self
14228 .db
14229 .as_ref()
14230 .and_then(|db| db.get_table(cursor.root_page))
14231 {
14232 if pos < table.rows.len() {
14233 true
14234 } else {
14235 cursor.position = None;
14236 false
14237 }
14238 } else {
14239 false
14240 }
14241 } else {
14242 false
14243 }
14244 } else {
14245 false
14246 }
14247 } else if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
14248 cursor.cursor.next(&cursor.cx).await?
14249 } else if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
14250 if let Some(pos) = sorter.position {
14251 let next = pos + 1;
14252 if next < sorter.rows.len() {
14253 sorter.position = Some(next);
14254 true
14255 } else {
14256 sorter.position = None;
14257 false
14258 }
14259 } else {
14260 false
14261 }
14262 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
14263 if cursor.is_pseudo {
14264 false
14265 } else if let Some(db) = self.db.as_ref() {
14266 if let Some(table) = db.get_table(cursor.root_page) {
14267 if let Some(pos) = cursor.position {
14268 let next = pos + 1;
14269 if next < table.rows.len() {
14270 cursor.position = Some(next);
14271 true
14272 } else {
14273 cursor.position = None;
14274 false
14275 }
14276 } else {
14277 false
14278 }
14279 } else {
14280 false
14281 }
14282 } else {
14283 false
14284 }
14285 } else {
14286 false
14287 };
14288 if has_next {
14289 #[allow(clippy::cast_sign_loss)]
14290 {
14291 *pc = op.p2 as usize;
14292 }
14293 } else {
14294 *pc += 1;
14295 }
14296 Ok(())
14297 }
14298
14299 #[inline(always)]
14300 async fn execute_column_hot(&mut self, op: &VdbeOp) -> Result<()> {
14301 let cursor_id = op.p1;
14302 let col_idx = op.p2 as usize;
14303 let target = op.p3;
14304 if !self
14305 .column_to_reg_direct(cursor_id, col_idx, target)
14306 .await?
14307 {
14308 let val = self.cursor_column(cursor_id, col_idx).await?;
14309 self.set_reg_fast(target, val);
14310 }
14311 Ok(())
14312 }
14313
14314 #[inline(always)]
14315 async fn execute_column_substr_prefix_hot(&mut self, op: &VdbeOp) -> Result<()> {
14316 let Ok(col_idx) = usize::try_from(op.p2) else {
14317 self.set_reg_fast(op.p3, SqliteValue::Null);
14318 return Ok(());
14319 };
14320 let P4::Int(prefix_len) = &op.p4 else {
14321 self.set_reg_fast(op.p3, SqliteValue::Null);
14322 return Ok(());
14323 };
14324 let Ok(prefix_len) = usize::try_from(*prefix_len) else {
14325 self.set_reg_fast(op.p3, SqliteValue::Null);
14326 return Ok(());
14327 };
14328
14329 if let Some(value) = self
14330 .column_substr_prefix_direct(op.p1, col_idx, prefix_len)
14331 .await?
14332 {
14333 self.set_reg_fast(op.p3, value);
14334 return Ok(());
14335 }
14336
14337 let value = self.cursor_column(op.p1, col_idx).await?;
14338 self.set_reg_fast(op.p3, sqlite_substr_prefix_value(&value, prefix_len));
14339 Ok(())
14340 }
14341
14342 #[inline(always)]
14343 async fn execute_column_octet_length_hot(&mut self, op: &VdbeOp) -> Result<()> {
14344 let Ok(col_idx) = usize::try_from(op.p2) else {
14345 self.set_reg_fast(op.p3, SqliteValue::Null);
14346 return Ok(());
14347 };
14348 if let Some(value) = self.column_octet_length_direct(op.p1, col_idx).await? {
14349 self.set_reg_fast(op.p3, value);
14350 return Ok(());
14351 }
14352 let value = self.cursor_column(op.p1, col_idx).await?;
14353 self.set_reg_fast(op.p3, sqlite_octet_length_value(&value));
14354 Ok(())
14355 }
14356
14357 #[inline(always)]
14358 fn execute_result_row_hot(
14359 &mut self,
14360 op: &VdbeOp,
14361 collect_vdbe_metrics: bool,
14362 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14363 ) -> Result<()> {
14364 let count = usize::try_from(op.p2).unwrap_or(0);
14365 let should_retain_row = self.collect_result_rows
14366 && match self.max_collected_result_rows {
14367 Some(limit) => self.results.len() < limit,
14368 None => true,
14369 };
14370 if row_handler.is_some() || should_retain_row {
14371 let materialize_start = collect_vdbe_metrics.then(Instant::now);
14372 let row = self.take_reg_range(op.p1, count);
14373 if collect_vdbe_metrics {
14374 if let Some(materialize_start) = materialize_start {
14375 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14376 u64::try_from(materialize_start.elapsed().as_nanos()).unwrap_or(u64::MAX),
14377 AtomicOrdering::Relaxed,
14378 );
14379 }
14380 record_result_row_metrics(&row);
14381 }
14382 if let Some(handler) = row_handler.as_mut() {
14383 (*handler)(row)?;
14384 } else {
14385 self.results.push(row);
14386 }
14387 } else {
14388 self.discard_reg_range(op.p1, count);
14389 }
14390 Ok(())
14391 }
14392
14393 fn emit_compiled_result_row(
14394 &mut self,
14395 values: &[SqliteValue],
14396 collect_vdbe_metrics: bool,
14397 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14398 ) -> Result<()> {
14399 let should_retain_row = self.collect_result_rows
14400 && match self.max_collected_result_rows {
14401 Some(limit) => self.results.len() < limit,
14402 None => true,
14403 };
14404 if row_handler.is_some() || should_retain_row {
14405 let materialize_start = collect_vdbe_metrics.then(Instant::now);
14406 let row: smallvec::SmallVec<[SqliteValue; 16]> = values.iter().cloned().collect();
14407 if collect_vdbe_metrics {
14408 if let Some(materialize_start) = materialize_start {
14409 FSQLITE_VDBE_RESULT_ROW_MATERIALIZATION_TIME_NS_TOTAL.fetch_add(
14410 u64::try_from(materialize_start.elapsed().as_nanos()).unwrap_or(u64::MAX),
14411 AtomicOrdering::Relaxed,
14412 );
14413 }
14414 record_result_row_metrics(&row);
14415 }
14416 if let Some(handler) = row_handler.as_mut() {
14417 (*handler)(row)?;
14418 } else {
14419 self.results.push(row);
14420 }
14421 }
14422 Ok(())
14423 }
14424
14425 fn compiled_column_values(
14426 &self,
14427 value_sources: &[InsertValueSource],
14428 borrowed_bindings: Option<&[SqliteValue]>,
14429 collect_vdbe_metrics: bool,
14430 affinity: Option<&str>,
14431 ) -> smallvec::SmallVec<[SqliteValue; 16]> {
14432 let binding_source = borrowed_bindings.unwrap_or(self.bindings.as_slice());
14433 let mut values = value_sources
14434 .iter()
14435 .map(|source| match source {
14436 InsertValueSource::Binding(binding_index) => binding_source
14437 .get(*binding_index)
14438 .cloned()
14439 .unwrap_or(SqliteValue::Null),
14440 InsertValueSource::Constant(value) => value.clone(),
14441 })
14442 .collect::<smallvec::SmallVec<[_; 16]>>();
14443
14444 if let Some(affinity) = affinity {
14445 for (value, ch) in values.iter_mut().zip(affinity.chars()) {
14446 let before = value.clone();
14447 let coerced = value.clone().apply_affinity(char_to_affinity(ch));
14448 if collect_vdbe_metrics {
14449 record_type_coercion(&before, &coerced);
14450 }
14451 *value = coerced;
14452 }
14453 }
14454
14455 values
14456 }
14457
14458 fn apply_make_record_null_placeholders(values: &mut [SqliteValue], record_p4: &P4) {
14459 match record_p4 {
14460 P4::PrecomputedHeader(header) if header.column_count() == values.len() => {
14461 for (value, slot) in values.iter_mut().zip(&header.slots) {
14462 if slot.kind == PrecomputedSerialTypeKind::NullPlaceholder {
14463 *value = SqliteValue::Null;
14464 }
14465 }
14466 }
14467 P4::Affinity(affinity) => {
14468 for (value, affinity) in values.iter_mut().zip(affinity.bytes()) {
14469 if affinity == b'X' {
14470 *value = SqliteValue::Null;
14471 }
14472 }
14473 }
14474 _ => {}
14475 }
14476 }
14477
14478 fn execute_compiled_constant_result_row(
14479 &mut self,
14480 template: &ConstantResultRowTemplate,
14481 collect_vdbe_metrics: bool,
14482 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14483 ) -> Result<ExecOutcome> {
14484 self.emit_compiled_result_row(&template.values, collect_vdbe_metrics, row_handler)?;
14485 Ok(ExecOutcome::Done)
14486 }
14487
14488 async fn execute_compiled_simple_insert(
14489 &mut self,
14490 template: &SimpleInsertTemplate,
14491 borrowed_bindings: Option<&[SqliteValue]>,
14492 collect_vdbe_metrics: bool,
14493 ) -> Result<ExecOutcome> {
14494 if (template.insert_flags & 0x0F) != 2 {
14495 return Err(FrankenError::internal(
14496 "compiled simple INSERT only supports OE_Abort semantics",
14497 ));
14498 }
14499
14500 if !self
14501 .open_storage_cursor(template.cursor_id, template.root_page, true)
14502 .await
14503 {
14504 return Err(FrankenError::internal(format!(
14505 "compiled simple INSERT could not open writable cursor {} on root {}",
14506 template.cursor_id, template.root_page
14507 )));
14508 }
14509
14510 let mut values = self.compiled_column_values(
14511 &template.value_sources,
14512 borrowed_bindings,
14513 collect_vdbe_metrics,
14514 template.affinity.as_deref(),
14515 );
14516 Self::apply_make_record_null_placeholders(&mut values, &template.record_p4);
14517 let concurrent_allocator = self.concurrent_rowid_allocator.clone();
14518 let concurrent_schema_epoch = self.concurrent_rowid_schema_epoch;
14519 let previous_last_insert_rowid = self.last_insert_rowid;
14520 let previous_last_insert_rowid_valid = self.last_insert_rowid_valid;
14521
14522 let rowid = {
14523 let sc = self
14524 .storage_cursors
14525 .get_mut(&template.cursor_id)
14526 .ok_or_else(|| FrankenError::internal("compiled simple INSERT lost its cursor"))?;
14527 if !sc.writable {
14528 return Err(FrankenError::internal(
14529 "compiled simple INSERT cursor is not writable",
14530 ));
14531 }
14532 let autoinc_max = sc.autoincrement_high_water;
14533 let rowid_mode = sc.rowid_mode;
14534 let rowid = if let Some(allocator) = concurrent_allocator.as_ref() {
14535 Self::allocate_concurrent_storage_rowid(
14536 allocator,
14537 concurrent_schema_epoch,
14538 template.root_page,
14539 rowid_mode,
14540 autoinc_max,
14541 sc,
14542 "rowid overflow in compiled simple INSERT",
14543 )
14544 .await?
14545 } else {
14546 Self::allocate_serialized_storage_rowid(
14547 sc,
14548 autoinc_max,
14549 "rowid overflow in compiled simple INSERT",
14550 )
14551 .await?
14552 };
14553 rowid
14554 };
14555 let record_plan =
14556 build_compiled_record_write_plan(values.as_slice(), &template.record_builder);
14557 let payload_len = record_plan.exact_size();
14558 let appended_directly = if let Some(sc) = self.storage_cursors.get_mut(&template.cursor_id)
14559 {
14560 let result = sc
14561 .cursor
14562 .table_append_after_last_position_with_writer(
14563 &sc.cx,
14564 rowid,
14565 payload_len,
14566 move |dst| {
14567 record_plan.write_into_slice(dst).map_err(|()| {
14568 FrankenError::internal(
14569 "compiled simple INSERT direct record serialization size mismatch",
14570 )
14571 })
14572 },
14573 )
14574 .await?;
14575 if result {
14576 sc.last_successful_insert_rowid = Some(rowid);
14577 }
14578 result
14579 } else {
14580 return Err(FrankenError::internal(
14581 "compiled simple INSERT lost its cursor",
14582 ));
14583 };
14584 if !appended_directly {
14585 let mut payload_buf = self.make_record_lookaside.take_buf();
14586 serialize_compiled_record_into_vec(
14587 values.as_slice(),
14588 &template.record_builder,
14589 &mut payload_buf,
14590 );
14591 let append_result = if let Some(sc) = self.storage_cursors.get_mut(&template.cursor_id)
14592 {
14593 let result = sc
14594 .cursor
14595 .table_append_after_last_position(&sc.cx, rowid, &payload_buf)
14596 .await;
14597 if result.is_ok() {
14598 sc.last_successful_insert_rowid = Some(rowid);
14599 }
14600 result
14601 } else {
14602 Err(FrankenError::internal(
14603 "compiled simple INSERT lost its cursor",
14604 ))
14605 };
14606 payload_buf.clear();
14607 self.make_record_lookaside.replace_buf(payload_buf);
14608 append_result?;
14609 }
14610
14611 self.changes += 1;
14612 self.last_insert_rowid = rowid;
14613 self.last_insert_rowid_valid = true;
14614 self.last_insert_cursor_id = Some(template.cursor_id);
14615 self.set_conflict_skip_idx(false);
14616 self.set_pending_insert_rollback(Some(PendingInsertRollback {
14617 cursor_id: template.cursor_id,
14618 rowid,
14619 previous_last_insert_rowid,
14620 previous_last_insert_rowid_valid,
14621 update_restore: None,
14622 }));
14623 self.clear_pending_idx_entries();
14624 self.pending_next_after_delete.remove(&template.cursor_id);
14625 self.mark_storage_root_page_dirty(template.root_page);
14626
14627 Ok(ExecOutcome::Done)
14628 }
14629
14630 async fn execute_compiled_rowid_lookup_select(
14631 &mut self,
14632 template: &RowidLookupSelectTemplate,
14633 borrowed_bindings: Option<&[SqliteValue]>,
14634 collect_vdbe_metrics: bool,
14635 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14636 ) -> Result<ExecOutcome> {
14637 if !self
14638 .open_storage_cursor(template.cursor_id, template.root_page, false)
14639 .await
14640 {
14641 return Err(FrankenError::internal(format!(
14642 "compiled rowid-lookup SELECT could not open cursor {} on root {}",
14643 template.cursor_id, template.root_page
14644 )));
14645 }
14646
14647 let rowid_val = match &template.rowid_source {
14648 InsertValueSource::Binding(idx) => {
14649 let bindings = borrowed_bindings.ok_or_else(|| {
14650 FrankenError::internal("compiled rowid-lookup SELECT: missing bindings")
14651 })?;
14652 bindings
14653 .get(*idx)
14654 .cloned()
14655 .unwrap_or(SqliteValue::Null)
14656 .to_integer()
14657 }
14658 InsertValueSource::Constant(v) => v.to_integer(),
14659 };
14660
14661 let found = {
14662 let sc = self
14663 .storage_cursors
14664 .get_mut(&template.cursor_id)
14665 .ok_or_else(|| {
14666 FrankenError::internal("compiled rowid-lookup SELECT lost its cursor")
14667 })?;
14668 sc.cursor.table_move_to(&sc.cx, rowid_val).await?.is_found()
14669 };
14670
14671 if found {
14672 let mut values = Vec::with_capacity(template.column_indices.len());
14673 for &col_idx in &template.column_indices {
14674 let val = self
14675 .cursor_column(template.cursor_id, col_idx as usize)
14676 .await?;
14677 values.push(val);
14678 }
14679 self.emit_compiled_result_row(&values, collect_vdbe_metrics, row_handler)?;
14680 }
14681
14682 self.storage_cursors.remove(&template.cursor_id);
14683 Ok(ExecOutcome::Done)
14684 }
14685
14686 async fn execute_compiled_full_scan_select(
14687 &mut self,
14688 template: &FullScanSelectTemplate,
14689 collect_vdbe_metrics: bool,
14690 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14691 ) -> Result<ExecOutcome> {
14692 if !self
14693 .open_storage_cursor(template.cursor_id, template.root_page, false)
14694 .await
14695 {
14696 return Err(FrankenError::internal(format!(
14697 "compiled full-scan SELECT could not open cursor {} on root {}",
14698 template.cursor_id, template.root_page
14699 )));
14700 }
14701
14702 let has_rows = {
14703 let sc = self
14704 .storage_cursors
14705 .get_mut(&template.cursor_id)
14706 .ok_or_else(|| {
14707 FrankenError::internal("compiled full-scan SELECT lost its cursor")
14708 })?;
14709 sc.cursor.first(&sc.cx).await?
14710 };
14711
14712 if has_rows {
14713 loop {
14714 let mut values = Vec::with_capacity(template.column_indices.len());
14715 for &col_idx in &template.column_indices {
14716 let val = self
14717 .cursor_column(template.cursor_id, col_idx as usize)
14718 .await?;
14719 values.push(val);
14720 }
14721 self.emit_compiled_result_row(&values, collect_vdbe_metrics, row_handler)?;
14722
14723 let more = {
14724 let sc = self
14725 .storage_cursors
14726 .get_mut(&template.cursor_id)
14727 .ok_or_else(|| {
14728 FrankenError::internal("compiled full-scan SELECT lost cursor mid-scan")
14729 })?;
14730 sc.cursor.next(&sc.cx).await?
14731 };
14732 if !more {
14733 break;
14734 }
14735 }
14736 }
14737
14738 self.storage_cursors.remove(&template.cursor_id);
14739 Ok(ExecOutcome::Done)
14740 }
14741
14742 async fn execute_compiled_program(
14743 &mut self,
14744 compiled_program: &CompiledProgram,
14745 borrowed_bindings: Option<&[SqliteValue]>,
14746 collect_vdbe_metrics: bool,
14747 row_handler: &mut Option<&mut ResultRowCallback<'_>>,
14748 ) -> Result<ExecOutcome> {
14749 match compiled_program {
14750 CompiledProgram::ConstantResultRow(template) => self
14751 .execute_compiled_constant_result_row(template, collect_vdbe_metrics, row_handler),
14752 CompiledProgram::SimpleInsert(template) => {
14753 self.execute_compiled_simple_insert(
14754 template,
14755 borrowed_bindings,
14756 collect_vdbe_metrics,
14757 )
14758 .await
14759 }
14760 CompiledProgram::RowidLookupSelect(template) => {
14761 self.execute_compiled_rowid_lookup_select(
14762 template,
14763 borrowed_bindings,
14764 collect_vdbe_metrics,
14765 row_handler,
14766 )
14767 .await
14768 }
14769 CompiledProgram::FullScanSelect(template) => {
14770 self.execute_compiled_full_scan_select(template, collect_vdbe_metrics, row_handler)
14771 .await
14772 }
14773 }
14774 }
14775
14776 #[inline(always)]
14777 fn execute_comparison_jump_hot(&mut self, op: &VdbeOp, pc: &mut usize) -> Result<()> {
14778 let lhs = self.get_reg(op.p3);
14779 let rhs = self.get_reg(op.p1);
14780 let store_p2 = (op.p5 & 0x20) != 0; if lhs.is_null() || rhs.is_null() {
14783 let null_eq = (op.p5 & 0x80) != 0;
14784 if null_eq {
14785 let both_null = lhs.is_null() && rhs.is_null();
14786 let should_jump = match op.opcode {
14787 Opcode::Eq => both_null,
14788 Opcode::Ne => !both_null,
14789 _ => false,
14790 };
14791 if store_p2 {
14792 self.set_reg_int(op.p2, i64::from(should_jump));
14793 *pc += 1;
14794 } else if should_jump {
14795 *pc = op.p2 as usize;
14796 } else {
14797 *pc += 1;
14798 }
14799 } else if store_p2 {
14800 self.set_reg_fast(op.p2, SqliteValue::Null);
14801 *pc += 1;
14802 } else if (op.p5 & 0x10) != 0 {
14803 *pc = op.p2 as usize;
14804 } else {
14805 *pc += 1;
14806 }
14807 return Ok(());
14808 }
14809
14810 let cmp = if let Some(cmp) = fast_compare_same_storage_class(lhs, rhs, &op.p4, op.p5) {
14811 cmp
14812 } else {
14813 let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, op.p5);
14814 if let P4::Collation(ref coll_name) = op.p4 {
14815 let coll = self.lock_collation();
14816 collate_compare(&cmp_lhs, &cmp_rhs, coll_name, &coll)
14817 } else {
14818 cmp_lhs.partial_cmp(&cmp_rhs)
14819 }
14820 };
14821
14822 let should_jump = matches!(
14823 (op.opcode, cmp),
14824 (Opcode::Eq, Some(Ordering::Equal))
14825 | (Opcode::Lt, Some(Ordering::Less))
14826 | (Opcode::Le, Some(Ordering::Less | Ordering::Equal))
14827 | (Opcode::Gt, Some(Ordering::Greater))
14828 | (Opcode::Ge, Some(Ordering::Greater | Ordering::Equal))
14829 ) || matches!((op.opcode, cmp), (Opcode::Ne, Some(ord)) if ord != Ordering::Equal);
14830
14831 if store_p2 {
14832 if cmp.is_none() {
14833 self.set_reg_fast(op.p2, SqliteValue::Null);
14834 } else {
14835 self.set_reg_int(op.p2, i64::from(should_jump));
14836 }
14837 *pc += 1;
14838 } else if should_jump {
14839 *pc = op.p2 as usize;
14840 } else {
14841 *pc += 1;
14842 }
14843 Ok(())
14844 }
14845
14846 #[inline(always)]
14847 fn serialize_record_from_register_range(
14848 &self,
14849 first_reg: i32,
14850 n_cols: usize,
14851 p4: &P4,
14852 rec_buf: &mut Vec<u8>,
14853 ) {
14854 match p4 {
14855 P4::PrecomputedHeader(header) if header.column_count() == n_cols => {
14856 let null_placeholder = SqliteValue::Null;
14857 let make_iter = || {
14858 header
14859 .slots
14860 .iter()
14861 .enumerate()
14862 .map(|(i, slot)| match slot.kind {
14863 PrecomputedSerialTypeKind::NullPlaceholder => &null_placeholder,
14864 _ => {
14865 #[allow(clippy::cast_possible_wrap)]
14866 let reg = first_reg + i as i32;
14867 self.get_reg(reg)
14868 }
14869 })
14870 };
14871 let used_integer_fast_path =
14872 n_cols >= 4 && simd_serialize_integer_record(make_iter(), rec_buf);
14873 if !used_integer_fast_path
14874 && !serialize_record_iter_with_precomputed_header_into(
14875 make_iter(),
14876 header,
14877 rec_buf,
14878 )
14879 {
14880 fsqlite_types::record::serialize_record_iter_into(make_iter(), rec_buf);
14881 }
14882 }
14883 P4::PrecomputedHeader(_) => {
14888 let iter = (0..n_cols).map(move |i| {
14889 #[allow(clippy::cast_possible_wrap)]
14890 let reg = first_reg + i as i32;
14891 self.get_reg(reg)
14892 });
14893 if n_cols >= 4 && simd_serialize_integer_record(iter.clone(), rec_buf) {
14894 } else {
14896 fsqlite_types::record::serialize_record_iter_into(iter, rec_buf);
14897 }
14898 }
14899 P4::Affinity(aff) => {
14900 let null_placeholder = SqliteValue::Null;
14901 let affinity = aff.as_bytes();
14902 let make_iter = || {
14903 (0..n_cols).map(|i| {
14904 if affinity.get(i) == Some(&b'X') {
14905 &null_placeholder
14906 } else {
14907 #[allow(clippy::cast_possible_wrap)]
14908 let reg = first_reg + i as i32;
14909 self.get_reg(reg)
14910 }
14911 })
14912 };
14913 if n_cols >= 4 && simd_serialize_integer_record(make_iter(), rec_buf) {
14914 } else {
14916 fsqlite_types::record::serialize_record_iter_into(make_iter(), rec_buf);
14917 }
14918 }
14919 _ => {
14920 let iter = (0..n_cols).map(move |i| {
14921 #[allow(clippy::cast_possible_wrap)]
14922 let reg = first_reg + i as i32;
14923 self.get_reg(reg)
14924 });
14925 if n_cols >= 4 && simd_serialize_integer_record(iter.clone(), rec_buf) {
14926 } else {
14928 fsqlite_types::record::serialize_record_iter_into(iter, rec_buf);
14929 }
14930 }
14931 }
14932 }
14933
14934 #[inline(always)]
14935 fn execute_make_record_hot(&mut self, op: &VdbeOp, collect_vdbe_metrics: bool) {
14936 let target = op.p3;
14937 let n_cols = usize::try_from(op.p2).unwrap_or(0);
14938 if let Some(armed_reg) = self.make_record_lookaside.armed_register()
14939 && (armed_reg != target || Self::register_range_contains(op.p1, n_cols, armed_reg))
14940 {
14941 self.materialize_make_record_sideband(armed_reg);
14942 }
14943 let mut rec_buf = self.make_record_lookaside.take_buf();
14944 self.serialize_record_from_register_range(op.p1, n_cols, &op.p4, &mut rec_buf);
14945 if collect_vdbe_metrics {
14946 FSQLITE_VDBE_MAKE_RECORD_CALLS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
14947 FSQLITE_VDBE_MAKE_RECORD_BLOB_BYTES_TOTAL.fetch_add(
14948 u64::try_from(rec_buf.len()).unwrap_or(u64::MAX),
14949 AtomicOrdering::Relaxed,
14950 );
14951 }
14952 self.set_reg(target, SqliteValue::Null);
14956 self.make_record_lookaside.replace_buf(rec_buf); self.make_record_lookaside.arm_sideband(target);
14962 }
14963
14964 #[cold]
14965 fn execute_set_snapshot_cold(&mut self, op: &VdbeOp) -> Result<()> {
14966 let cursor_id = op.p1;
14967 let target = match &op.p4 {
14968 P4::TimeTravelCommitSeq(seq) => TimeTravelMarker::CommitSeq(*seq),
14969 P4::TimeTravelTimestamp(ts) => TimeTravelMarker::Timestamp(ts.clone()),
14970 _ => {
14971 return Err(FrankenError::Internal(
14972 "SetSnapshot: invalid P4 (expected time-travel target)".to_owned(),
14973 ));
14974 }
14975 };
14976 self.time_travel_cursors.insert(cursor_id, target.clone());
14977
14978 let version_store = self.version_store.as_ref().ok_or_else(|| {
14979 FrankenError::Internal(
14980 "SetSnapshot: VersionStore not available for time-travel".to_owned(),
14981 )
14982 })?;
14983 let commit_log = self.time_travel_commit_log.as_ref().ok_or_else(|| {
14984 FrankenError::Internal(
14985 "SetSnapshot: CommitLog not available for time-travel".to_owned(),
14986 )
14987 })?;
14988 let gc_horizon = self.time_travel_gc_horizon.ok_or_else(|| {
14989 FrankenError::Internal(
14990 "SetSnapshot: GC horizon not available for time-travel".to_owned(),
14991 )
14992 })?;
14993
14994 let has_txn_cursor = self
14995 .storage_cursors
14996 .get(&cursor_id)
14997 .is_some_and(|sc| matches!(&sc.cursor, CursorBackend::Txn(_)));
14998 if !has_txn_cursor {
14999 return Err(FrankenError::Internal(format!(
15000 "SetSnapshot: cursor {cursor_id} is not a transactional cursor"
15001 )));
15002 }
15003
15004 let time_travel_target = match &target {
15005 TimeTravelMarker::CommitSeq(seq) => {
15006 TimeTravelTarget::CommitSequence(CommitSeq::new(*seq))
15007 }
15008 TimeTravelMarker::Timestamp(ts) => {
15009 return Err(FrankenError::Internal(format!(
15010 "SetSnapshot: timestamp-based time-travel not yet supported (datetime parser not wired); timestamp='{ts}'"
15011 )));
15012 }
15013 };
15014
15015 let schema_epoch = SchemaEpoch::new(u64::from(self.schema_cookie));
15016 let commit_log = commit_log
15017 .lock()
15018 .unwrap_or_else(std::sync::PoisonError::into_inner);
15019 let tt_snapshot =
15020 create_time_travel_snapshot(time_travel_target, &commit_log, gc_horizon, schema_epoch)
15021 .map_err(|err| {
15022 FrankenError::Internal(format!("time-travel snapshot error: {err}"))
15023 })?;
15024
15025 let old_sc = self.storage_cursors.remove(&cursor_id).ok_or_else(|| {
15026 FrankenError::Internal(format!("SetSnapshot: cursor {} not found", cursor_id))
15027 })?;
15028 let root_page = self.cursor_root_pages.get(&cursor_id).copied().unwrap_or(1);
15029 let root_pgno = PageNumber::new(root_page as u32).unwrap_or(PageNumber::ONE);
15030 let inner_page_io = if let Some(ref page_io) = self.txn_page_io {
15031 page_io.clone()
15032 } else {
15033 return Err(FrankenError::Internal(
15034 "SetSnapshot: no transaction page I/O available".to_owned(),
15035 ));
15036 };
15037
15038 let commit_seq = tt_snapshot.target_commit_seq().get();
15039 let tt_page_io = TimeTravelPageIo {
15040 inner: inner_page_io,
15041 version_store: Arc::clone(version_store),
15042 snapshot: tt_snapshot,
15043 };
15044 let is_table_btree = old_sc.cursor.is_table_btree();
15045 let index_desc_flags = if is_table_btree {
15046 Vec::new()
15047 } else {
15048 self.index_desc_flags_for_root(root_page)
15049 };
15050 let page_layout = BtreeCursorPageLayout {
15051 usable_size: old_sc.cursor.usable_size(),
15052 page_size: old_sc.cursor.page_size(),
15053 };
15054 let mut new_cursor = BtCursor::new_with_index_desc(
15055 tt_page_io,
15056 root_pgno,
15057 page_layout.usable_size,
15058 is_table_btree,
15059 index_desc_flags,
15060 );
15061 if !is_table_btree {
15062 new_cursor.set_index_collation_context(
15063 self.index_collations_for_root(root_page),
15064 Arc::clone(&self.collation_registry),
15065 );
15066 }
15067 configure_btree_cursor_page_size(&mut new_cursor, page_layout);
15068 self.storage_cursors.insert(
15069 cursor_id,
15070 StorageCursor {
15071 cursor: CursorBackend::TimeTravel(new_cursor),
15072 cx: old_sc.cx,
15073 writable: false,
15074 root_page,
15075 rowid_mode: old_sc.rowid_mode,
15076 autoincrement_high_water: old_sc.autoincrement_high_water,
15077 last_alloc_rowid: 0,
15078 payload_buf: Vec::new(),
15079 target_vals_buf: Vec::new(),
15080 cur_vals_buf: Vec::new(),
15081 row_decode: RowDecodeScratch::default(),
15082 last_position_stamp: None,
15083 cached_rowid: None,
15084 payload_includes_rowid_alias: None,
15085 last_successful_insert_rowid: None,
15086 last_rightmost_unique_index_prefix: None,
15087 last_rightmost_unique_index_position: None,
15088 ipk_col_idx: old_sc.ipk_col_idx,
15089 table_column_count: old_sc.table_column_count,
15090 first_not_null_non_ipk_col: old_sc.first_not_null_non_ipk_col,
15091 },
15092 );
15093 tracing::info!(
15094 cursor_id,
15095 commit_seq,
15096 "SetSnapshot: upgraded cursor to time-travel backend"
15097 );
15098 Ok(())
15099 }
15100
15101 fn collect_reg_range(
15106 &mut self,
15107 start: i32,
15108 count: usize,
15109 ) -> smallvec::SmallVec<[SqliteValue; 16]> {
15110 let mut row = smallvec::SmallVec::with_capacity(count);
15111 for offset in 0..count {
15112 let val = Self::reg_with_offset(start, offset)
15113 .map_or_else(|| SqliteValue::Null, |reg| self.clone_reg_materialized(reg));
15114 row.push(val);
15115 }
15116 row
15117 }
15118
15119 #[inline]
15124 fn take_reg_range(
15125 &mut self,
15126 start: i32,
15127 count: usize,
15128 ) -> smallvec::SmallVec<[SqliteValue; 16]> {
15129 let mut row = smallvec::SmallVec::with_capacity(count);
15130 for offset in 0..count {
15131 let val = Self::reg_with_offset(start, offset)
15132 .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15133 row.push(val);
15134 }
15135 row
15136 }
15137
15138 #[inline]
15139 fn positive_register_ranges_overlap(left_start: i32, right_start: i32, count: usize) -> bool {
15140 let Ok(count) = i64::try_from(count) else {
15141 return true;
15142 };
15143 let left_start = i64::from(left_start);
15144 let right_start = i64::from(right_start);
15145 let left_end = left_start + count;
15146 let right_end = right_start + count;
15147 left_start < right_end && right_start < left_end
15148 }
15149
15150 #[inline]
15151 fn move_reg_range(&mut self, src_start: i32, dst_start: i32, count: usize) {
15152 if count == 0 {
15153 return;
15154 }
15155
15156 let can_stream_directly = count == 1
15157 || (src_start > 0
15158 && dst_start > 0
15159 && !Self::positive_register_ranges_overlap(src_start, dst_start, count));
15160
15161 if can_stream_directly {
15162 for offset in 0..count {
15163 let value = Self::reg_with_offset(src_start, offset)
15164 .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15165 if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15166 self.set_reg_fast(dst, value);
15167 }
15168 }
15169 return;
15170 }
15171
15172 let mut temp: smallvec::SmallVec<[SqliteValue; 16]> =
15173 smallvec::SmallVec::with_capacity(count);
15174 for offset in 0..count {
15175 let value = Self::reg_with_offset(src_start, offset)
15176 .map_or_else(|| SqliteValue::Null, |reg| self.take_reg(reg));
15177 temp.push(value);
15178 }
15179
15180 for (offset, value) in temp.into_iter().enumerate() {
15181 if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15182 self.set_reg_fast(dst, value);
15183 }
15184 }
15185 }
15186
15187 #[inline(always)]
15190 #[allow(clippy::inline_always)]
15191 fn copy_single_reg(&mut self, src: i32, dst: i32) {
15192 self.materialize_make_record_sideband(src);
15193
15194 let dst_is_pre_sized = usize::try_from(dst)
15195 .is_ok_and(|idx| u16::try_from(idx).is_ok() && idx < self.registers.len());
15196 if dst_is_pre_sized && let SqliteValue::Integer(value) = self.get_reg(src) {
15197 let value = *value;
15198 self.set_reg_int(dst, value);
15199 return;
15200 }
15201
15202 let value = self.get_reg(src).clone();
15203 self.set_reg_fast(dst, value);
15204 }
15205
15206 #[inline]
15207 fn copy_reg_range(&mut self, src_start: i32, dst_start: i32, additional: i32) {
15208 let Ok(additional) = usize::try_from(additional) else {
15209 return;
15210 };
15211 let Some(count) = additional.checked_add(1) else {
15212 return;
15213 };
15214
15215 if count == 1 {
15216 self.copy_single_reg(src_start, dst_start);
15217 return;
15218 }
15219
15220 let values = self.collect_reg_range(src_start, count);
15221 for (offset, value) in values.into_iter().enumerate() {
15222 if let Some(dst) = Self::reg_with_offset(dst_start, offset) {
15223 self.set_reg_fast(dst, value);
15224 }
15225 }
15226 }
15227
15228 #[inline]
15229 fn discard_reg_range(&mut self, start: i32, count: usize) {
15230 for offset in 0..count {
15231 if let Some(reg) = Self::reg_with_offset(start, offset) {
15232 let _ = self.take_reg(reg);
15233 }
15234 }
15235 }
15236
15237 fn agg_step_with_args(
15238 cold_state: &mut Option<Box<ColdVdbeState>>,
15239 statement_cold_state: &mut StatementColdState,
15240 step: AggStepCall<'_>,
15241 ) -> Result<()> {
15242 statement_cold_state.insert(StatementColdState::AGGREGATES);
15243 let ctx = cold_state
15244 .get_or_insert_with(|| Box::new(ColdVdbeState::new()))
15245 .aggregates
15246 .entry_or_insert_with(step.accum_reg, || {
15247 let state = step.func.initial_state();
15248 AggregateContext {
15249 func: step.func.clone(),
15250 state,
15251 distinct_seen: if step.is_distinct {
15252 Some(std::collections::HashSet::new())
15253 } else {
15254 None
15255 },
15256 }
15257 });
15258
15259 if !Arc::ptr_eq(&ctx.func, step.func) {
15260 return Err(FrankenError::Internal(
15261 "AggStep accumulator reused for a different aggregate".to_owned(),
15262 ));
15263 }
15264
15265 let should_step = if let Some(ref mut seen) = ctx.distinct_seen {
15268 if step.args.iter().any(|a| matches!(a, SqliteValue::Null)) {
15269 false
15270 } else {
15271 seen.insert(distinct_key_collated(step.args, step.agg_collation))
15272 }
15273 } else {
15274 true
15275 };
15276
15277 observe_execution_cancellation(step.execution_cx)?;
15278 if should_step {
15279 if let Some(collation) = step.agg_collation
15280 && (step.func_name.eq_ignore_ascii_case("min")
15281 || step.func_name.eq_ignore_ascii_case("max"))
15282 && !step.args.is_empty()
15283 && !step.args[0].is_null()
15284 {
15285 agg_step_min_max_collated(
15286 &mut ctx.state,
15287 &step.args[0],
15288 step.func_name.eq_ignore_ascii_case("max"),
15289 collation,
15290 );
15291 } else {
15292 ctx.func.step(&mut ctx.state, step.args)?;
15293 }
15294 }
15295 observe_execution_cancellation(step.execution_cx)
15296 }
15297
15298 #[allow(dead_code)]
15299 fn collect_reg_range_refs(&self, start: i32, count: usize) -> Vec<&SqliteValue> {
15300 let mut row = Vec::with_capacity(count);
15301 for offset in 0..count {
15302 let val = Self::reg_with_offset(start, offset)
15303 .map_or(&SqliteValue::Null, |reg| self.get_reg(reg));
15304 row.push(val);
15305 }
15306 row
15307 }
15308
15309 #[inline(always)]
15310 #[allow(clippy::inline_always)]
15311 fn take_reg(&mut self, r: i32) -> SqliteValue {
15316 if r >= 0 && (r as usize) < self.registers.len() {
15317 self.materialize_make_record_sideband(r);
15318 self.clear_register_subtype(r);
15319 std::mem::replace(&mut self.registers[r as usize], SqliteValue::Null)
15320 } else {
15321 SqliteValue::Null
15322 }
15323 }
15324
15325 #[inline]
15331 #[allow(clippy::cast_sign_loss)]
15332 fn set_reg(&mut self, r: i32, val: SqliteValue) {
15333 if !(0..=65535).contains(&r) {
15334 return;
15337 }
15338 let idx = r as usize;
15339 if idx >= self.registers.len() {
15340 self.registers.resize(idx + 1, SqliteValue::Null);
15341 }
15342 self.invalidate_make_record_sideband_if_overwritten(r);
15343 self.clear_register_subtype(r);
15348 let normalized = match val {
15349 SqliteValue::Float(f) if f.is_nan() => SqliteValue::Null,
15350 other => other,
15351 };
15352 self.replace_register_value(idx, normalized);
15353 }
15354
15355 #[inline(always)]
15362 #[allow(clippy::inline_always)]
15363 #[allow(clippy::cast_sign_loss)]
15364 fn set_reg_fast(&mut self, r: i32, val: SqliteValue) {
15365 if !(0..=65535).contains(&r) {
15366 return;
15367 }
15368 let idx = r as usize;
15369 if idx >= self.registers.len() {
15370 self.registers.resize(idx + 1, SqliteValue::Null);
15371 }
15372 self.invalidate_make_record_sideband_if_overwritten(r);
15373 self.clear_register_subtype(r);
15374 let normalized = match val {
15375 SqliteValue::Float(f) if f.is_nan() => SqliteValue::Null,
15376 other => other,
15377 };
15378 self.replace_register_value(idx, normalized);
15379 }
15380
15381 #[inline(always)]
15389 #[allow(clippy::inline_always)]
15390 fn set_reg_arith_result(&mut self, r: i32, value: SqliteValue) {
15391 if let SqliteValue::Integer(v) = value {
15392 self.set_reg_int(r, v);
15393 } else {
15394 self.set_reg_fast(r, value);
15395 }
15396 }
15397
15398 #[inline(always)]
15404 #[allow(clippy::inline_always, clippy::cast_sign_loss)]
15405 fn set_reg_null(&mut self, r: i32) {
15406 if !(0..=65535).contains(&r) {
15407 return;
15408 }
15409 let idx = r as usize;
15410 if idx >= self.registers.len() {
15411 self.registers.resize(idx + 1, SqliteValue::Null);
15412 }
15413 self.invalidate_make_record_sideband_if_overwritten(r);
15414 self.clear_register_subtype(r);
15415 if !self.registers[idx].is_null() {
15416 self.replace_register_value(idx, SqliteValue::Null);
15417 }
15418 }
15419
15420 #[allow(clippy::inline_always)]
15425 #[inline(always)]
15426 #[allow(clippy::cast_sign_loss)]
15427 fn set_reg_int(&mut self, r: i32, val: i64) {
15428 let idx = r as usize;
15429 debug_assert!(
15432 idx < self.registers.len(),
15433 "register {r} out of pre-sized bounds"
15434 );
15435 if idx < self.registers.len() {
15436 self.invalidate_make_record_sideband_if_overwritten(r);
15437 self.clear_register_subtype(r);
15438 if let SqliteValue::Integer(current) = &mut self.registers[idx] {
15439 *current = val;
15440 } else {
15441 self.replace_register_value(idx, SqliteValue::Integer(val));
15442 }
15443 }
15444 }
15445
15446 #[inline(always)]
15451 #[allow(clippy::inline_always, clippy::cast_sign_loss)]
15452 fn set_reg_real(&mut self, r: i32, val: f64) {
15453 if !(0..=65535).contains(&r) {
15454 return;
15455 }
15456 let idx = r as usize;
15457 if idx >= self.registers.len() {
15458 self.registers.resize(idx + 1, SqliteValue::Null);
15459 }
15460 self.invalidate_make_record_sideband_if_overwritten(r);
15461 self.clear_register_subtype(r);
15462 if val.is_nan() {
15463 self.replace_register_value(idx, SqliteValue::Null);
15464 } else if let SqliteValue::Float(current) = &mut self.registers[idx] {
15465 *current = val;
15466 } else {
15467 self.replace_register_value(idx, SqliteValue::Float(val));
15468 }
15469 }
15470
15471 #[inline]
15476 #[allow(clippy::cast_sign_loss)]
15477 fn write_text_to_reg(&mut self, r: i32, text: &str) {
15478 if !(0..=65535).contains(&r) {
15479 return;
15480 }
15481 let idx = r as usize;
15482 if idx >= self.registers.len() {
15483 self.registers.resize(idx + 1, SqliteValue::Null);
15484 }
15485 self.invalidate_make_record_sideband_if_overwritten(r);
15486 self.clear_register_subtype(r);
15487 if let SqliteValue::Text(existing) = &mut self.registers[idx] {
15488 existing.overwrite(text);
15489 } else {
15490 self.replace_register_value(idx, SqliteValue::Text(SmallText::new(text)));
15491 }
15492 }
15493
15494 #[inline]
15499 #[allow(clippy::cast_sign_loss)]
15500 fn write_blob_to_reg(&mut self, r: i32, blob: &[u8]) {
15501 if !(0..=65535).contains(&r) {
15502 return;
15503 }
15504 let idx = r as usize;
15505 if idx >= self.registers.len() {
15506 self.registers.resize(idx + 1, SqliteValue::Null);
15507 }
15508 self.invalidate_make_record_sideband_if_overwritten(r);
15509 self.clear_register_subtype(r);
15510 if let SqliteValue::Blob(existing) = &mut self.registers[idx]
15511 && existing.len() == blob.len()
15512 && let Some(existing_bytes) = Arc::get_mut(existing)
15513 {
15514 existing_bytes.copy_from_slice(blob);
15515 return;
15516 }
15517 self.replace_register_value(idx, SqliteValue::Blob(Arc::from(blob)));
15518 }
15519
15520 #[allow(clippy::too_many_lines, clippy::cast_sign_loss)]
15523 async fn column_substr_prefix_direct(
15524 &mut self,
15525 cursor_id: i32,
15526 col_idx: usize,
15527 prefix_len: usize,
15528 ) -> Result<Option<SqliteValue>> {
15529 let collect_vdbe_metrics = self.collect_vdbe_metrics;
15530 let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15531 return Ok(None);
15532 };
15533 if cursor.cursor.eof() {
15534 return Ok(Some(SqliteValue::Null));
15535 }
15536
15537 ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15538
15539 let ipk_col_idx = cursor.ipk_col_idx;
15540 let payload_includes = if let Some(ipk) = ipk_col_idx {
15541 if let Some(cached) = cursor.payload_includes_rowid_alias {
15542 cached
15543 } else {
15544 let includes = payload_includes_rowid_alias_without_rowid(
15545 &cursor.row_decode,
15546 ipk,
15547 cursor.table_column_count,
15548 cursor.first_not_null_non_ipk_col,
15549 );
15550 cursor.payload_includes_rowid_alias = Some(includes);
15551 includes
15552 }
15553 } else {
15554 false
15555 };
15556
15557 let payload_idx = if let Some(ipk) = ipk_col_idx {
15558 if col_idx == ipk {
15559 return Ok(None);
15560 }
15561 if col_idx > ipk && !payload_includes {
15562 col_idx - 1
15563 } else {
15564 col_idx
15565 }
15566 } else {
15567 col_idx
15568 };
15569
15570 let Some(col) = cursor.row_decode.column_offset(payload_idx).copied() else {
15571 return Ok(None);
15572 };
15573 let Some(col_end) = column_payload_end(&col) else {
15574 return Err(FrankenError::DatabaseCorrupt {
15575 detail: format!("malformed column {payload_idx} payload length"),
15576 });
15577 };
15578 ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics).await?;
15579 let start =
15580 usize::try_from(col.body_offset).map_err(|_| FrankenError::DatabaseCorrupt {
15581 detail: format!("malformed column {payload_idx} payload offset"),
15582 })?;
15583 let bytes = cursor.payload_buf.get(start..col_end).ok_or_else(|| {
15584 FrankenError::DatabaseCorrupt {
15585 detail: format!("column {payload_idx} payload exceeds row image"),
15586 }
15587 })?;
15588
15589 let value = match classify_serial_type(col.serial_type) {
15590 SerialTypeClass::Null => Some(SqliteValue::Null),
15591 SerialTypeClass::Text => {
15592 if !bytes.is_ascii() {
15593 return Ok(None);
15594 }
15595 let end = prefix_len.min(bytes.len());
15596 let text = std::str::from_utf8(&bytes[..end])
15597 .expect("ASCII storage text prefix must be valid UTF-8");
15598 Some(SqliteValue::Text(SmallText::new(text)))
15599 }
15600 SerialTypeClass::Blob => {
15601 let end = prefix_len.min(bytes.len());
15602 Some(SqliteValue::Blob(Arc::from(&bytes[..end])))
15603 }
15604 SerialTypeClass::Integer
15605 | SerialTypeClass::Float
15606 | SerialTypeClass::Zero
15607 | SerialTypeClass::One
15608 | SerialTypeClass::Reserved => None,
15609 };
15610
15611 if collect_vdbe_metrics && let Some(value) = value.as_ref() {
15612 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15613 record_decoded_value_metrics(value);
15614 }
15615 Ok(value)
15616 }
15617
15618 async fn column_octet_length_direct(
15625 &mut self,
15626 cursor_id: i32,
15627 col_idx: usize,
15628 ) -> Result<Option<SqliteValue>> {
15629 let collect_vdbe_metrics = self.collect_vdbe_metrics;
15630 let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15631 return Ok(None);
15632 };
15633 if cursor.cursor.eof() {
15634 return Ok(Some(SqliteValue::Null));
15635 }
15636
15637 ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15638
15639 let ipk_col_idx = cursor.ipk_col_idx;
15640 let payload_includes = if let Some(ipk) = ipk_col_idx {
15641 if let Some(cached) = cursor.payload_includes_rowid_alias {
15642 cached
15643 } else {
15644 let includes = payload_includes_rowid_alias_without_rowid(
15645 &cursor.row_decode,
15646 ipk,
15647 cursor.table_column_count,
15648 cursor.first_not_null_non_ipk_col,
15649 );
15650 cursor.payload_includes_rowid_alias = Some(includes);
15651 includes
15652 }
15653 } else {
15654 false
15655 };
15656
15657 let payload_idx = if let Some(ipk) = ipk_col_idx {
15658 if col_idx == ipk {
15659 return Ok(None);
15660 }
15661 if col_idx > ipk && !payload_includes {
15662 col_idx - 1
15663 } else {
15664 col_idx
15665 }
15666 } else {
15667 col_idx
15668 };
15669
15670 let Some(col) = cursor.row_decode.column_offset(payload_idx) else {
15671 return Ok(None);
15672 };
15673 let value = match classify_serial_type(col.serial_type) {
15674 SerialTypeClass::Null | SerialTypeClass::Reserved => Some(SqliteValue::Null),
15675 SerialTypeClass::Text | SerialTypeClass::Blob => {
15676 Some(SqliteValue::Integer(i64::from(col.value_len)))
15677 }
15678 SerialTypeClass::Integer
15679 | SerialTypeClass::Float
15680 | SerialTypeClass::Zero
15681 | SerialTypeClass::One => None,
15682 };
15683
15684 if collect_vdbe_metrics && let Some(value) = value.as_ref() {
15685 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15686 record_decoded_value_metrics(value);
15687 }
15688 Ok(value)
15689 }
15690
15691 #[allow(clippy::too_many_lines, clippy::cast_sign_loss)]
15702 async fn column_to_reg_direct(
15703 &mut self,
15704 cursor_id: i32,
15705 col_idx: usize,
15706 target: i32,
15707 ) -> Result<bool> {
15708 let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) else {
15709 return Ok(false);
15710 };
15711 if cursor.cursor.eof() {
15712 self.set_reg(target, SqliteValue::Null);
15713 return Ok(true);
15714 }
15715
15716 let mut refresh_state =
15717 ensure_storage_cursor_row_layout(cursor, 0, self.collect_vdbe_metrics).await?;
15718
15719 let ipk_col_idx = cursor.ipk_col_idx;
15721 let payload_includes = if let Some(ipk) = ipk_col_idx {
15722 if let Some(cached) = cursor.payload_includes_rowid_alias {
15723 cached
15724 } else {
15725 let includes = payload_includes_rowid_alias_without_rowid(
15726 &cursor.row_decode,
15727 ipk,
15728 cursor.table_column_count,
15729 cursor.first_not_null_non_ipk_col,
15730 );
15731 cursor.payload_includes_rowid_alias = Some(includes);
15732 includes
15733 }
15734 } else {
15735 false
15736 };
15737
15738 let payload_idx = if let Some(ipk) = ipk_col_idx {
15739 if col_idx == ipk {
15740 let rowid = storage_cursor_cached_rowid(cursor).await?;
15741 self.set_reg_fast(target, SqliteValue::Integer(rowid));
15742 return Ok(true);
15743 }
15744 if col_idx > ipk && !payload_includes {
15745 col_idx - 1
15746 } else {
15747 col_idx
15748 }
15749 } else {
15750 col_idx
15751 };
15752
15753 let collect_vdbe_metrics = self.collect_vdbe_metrics;
15755
15756 if payload_idx < cursor.row_decode.column_count() {
15757 if let Some(col) = cursor.row_decode.column_offset(payload_idx).copied()
15758 && let Some(col_end) = column_payload_end(&col)
15759 {
15760 let col_refresh =
15761 ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics).await?;
15762 refresh_state.refreshed |= col_refresh.refreshed;
15763 refresh_state.eager_values_ready |= col_refresh.eager_values_ready;
15764 }
15765 let cached_value_ready = cursor.row_decode.cached_value_ready(payload_idx);
15767 if cached_value_ready {
15768 if let Some(cached) = cursor.row_decode.cached_value(payload_idx) {
15769 if refresh_state.refreshed && refresh_state.eager_values_ready {
15770 note_decode_cache_miss(collect_vdbe_metrics);
15771 } else {
15772 note_decode_cache_hit(collect_vdbe_metrics);
15773 }
15774 if collect_vdbe_metrics {
15775 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15776 record_decoded_value_metrics(cached);
15777 }
15778 let reg_idx = target as usize;
15782 if (0..=65535).contains(&target) {
15783 if reg_idx >= self.registers.len() {
15784 self.registers.resize(reg_idx + 1, SqliteValue::Null);
15785 }
15786 self.make_record_lookaside
15787 .clear_sideband_for_register(target);
15788 if let Some(cold_state) = self.cold_state.as_mut()
15789 && cold_state.has_subtypes
15790 {
15791 cold_state.register_subtypes.remove(&target);
15792 if cold_state.register_subtypes.is_empty() {
15793 cold_state.has_subtypes = false;
15794 }
15795 }
15796 match cached {
15797 SqliteValue::Float(f) if f.is_nan() => {
15799 self.registers[reg_idx] = SqliteValue::Null;
15800 }
15801 val => {
15802 self.registers[reg_idx] = val.clone();
15803 }
15804 }
15805 }
15806 return Ok(true);
15807 }
15808 }
15809
15810 note_decode_cache_miss(collect_vdbe_metrics);
15814 let hint = cursor.row_decode.cached_value(payload_idx);
15815 let val = fsqlite_types::record::decode_column_from_offset_reuse(
15816 &cursor.payload_buf,
15817 cursor
15818 .row_decode
15819 .column_offset(payload_idx)
15820 .expect("payload_idx checked against column_count"),
15821 hint,
15822 collect_vdbe_metrics,
15823 )
15824 .ok_or_else(|| FrankenError::DatabaseCorrupt {
15825 detail: format!("failed to decode column {payload_idx} from cursor payload"),
15826 })?;
15827
15828 if collect_vdbe_metrics {
15829 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15830 record_decoded_value_metrics(&val);
15831 }
15832
15833 cursor.row_decode.cache_decoded(payload_idx, val.clone());
15835
15836 self.set_reg_fast(target, val);
15838 return Ok(true);
15839 }
15840
15841 if let Some(&rp) = self.cursor_root_pages.get(&cursor_id) {
15843 if let Some(defaults) = self.column_defaults_by_root_page.get(&rp) {
15844 if let Some(Some(default_val)) = defaults.get(col_idx) {
15845 if collect_vdbe_metrics {
15846 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15847 record_decoded_value_metrics(default_val);
15848 }
15849 self.set_reg(target, default_val.clone());
15850 return Ok(true);
15851 }
15852 }
15853 }
15854 if collect_vdbe_metrics {
15855 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15856 record_decoded_value_metrics(&SqliteValue::Null);
15857 }
15858 self.set_reg(target, SqliteValue::Null);
15859 Ok(true)
15860 }
15861
15862 async fn cursor_column(&mut self, cursor_id: i32, col_idx: usize) -> Result<SqliteValue> {
15872 let collect_vdbe_metrics = self.collect_vdbe_metrics;
15873 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
15874 if cursor.cursor.eof() {
15875 return Ok(SqliteValue::Null);
15876 }
15877 let mut refresh_state =
15878 ensure_storage_cursor_row_layout(cursor, 0, collect_vdbe_metrics).await?;
15879
15880 let ipk_col_idx = cursor.ipk_col_idx;
15881 let payload_includes_rowid_alias = if let Some(ipk_col_idx) = ipk_col_idx {
15882 if let Some(cached) = cursor.payload_includes_rowid_alias {
15883 cached
15884 } else {
15885 let includes = payload_includes_rowid_alias_without_rowid(
15886 &cursor.row_decode,
15887 ipk_col_idx,
15888 cursor.table_column_count,
15889 cursor.first_not_null_non_ipk_col,
15890 );
15891 cursor.payload_includes_rowid_alias = Some(includes);
15892 includes
15893 }
15894 } else {
15895 false
15896 };
15897 let payload_idx = if let Some(ipk) = ipk_col_idx {
15898 if col_idx == ipk {
15899 let rowid = storage_cursor_cached_rowid(cursor).await?;
15900 return Ok(SqliteValue::Integer(rowid));
15901 }
15902 if col_idx > ipk && !payload_includes_rowid_alias {
15903 col_idx - 1
15904 } else {
15905 col_idx
15906 }
15907 } else {
15908 col_idx
15909 };
15910
15911 if payload_idx < cursor.row_decode.column_count() {
15913 if let Some(col) = cursor.row_decode.column_offset(payload_idx).copied()
15914 && let Some(col_end) = column_payload_end(&col)
15915 {
15916 let col_refresh =
15917 ensure_storage_cursor_row_layout(cursor, col_end, collect_vdbe_metrics)
15918 .await?;
15919 refresh_state.refreshed |= col_refresh.refreshed;
15920 refresh_state.eager_values_ready |= col_refresh.eager_values_ready;
15921 }
15922 let cached_value_ready = cursor.row_decode.cached_value_ready(payload_idx);
15924 if cached_value_ready {
15925 if let Some(val) = cursor.row_decode.cached_value(payload_idx).cloned() {
15927 if refresh_state.refreshed && refresh_state.eager_values_ready {
15928 note_decode_cache_miss(collect_vdbe_metrics);
15929 } else {
15930 note_decode_cache_hit(collect_vdbe_metrics);
15931 }
15932 if collect_vdbe_metrics {
15933 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15934 record_decoded_value_metrics(&val);
15935 }
15936 return Ok(val);
15937 }
15938 }
15939 note_decode_cache_miss(collect_vdbe_metrics);
15942 let hint = cursor.row_decode.cached_value(payload_idx);
15943 let val = fsqlite_types::record::decode_column_from_offset_reuse(
15944 &cursor.payload_buf,
15945 cursor
15946 .row_decode
15947 .column_offset(payload_idx)
15948 .expect("payload_idx checked against column_count"),
15949 hint,
15950 collect_vdbe_metrics,
15951 )
15952 .ok_or_else(|| FrankenError::DatabaseCorrupt {
15953 detail: format!("failed to decode column {payload_idx} from cursor payload"),
15954 })?;
15955 if collect_vdbe_metrics {
15956 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15957 record_decoded_value_metrics(&val);
15958 }
15959 cursor.row_decode.cache_decoded(payload_idx, val.clone());
15962 return Ok(val);
15963 }
15964 if let Some(&root_page) = self.cursor_root_pages.get(&cursor_id) {
15966 if let Some(defaults) = self.column_defaults_by_root_page.get(&root_page) {
15967 if let Some(Some(default_val)) = defaults.get(col_idx) {
15968 if collect_vdbe_metrics {
15969 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15970 record_decoded_value_metrics(default_val);
15971 }
15972 return Ok(default_val.clone());
15973 }
15974 }
15975 }
15976 if collect_vdbe_metrics {
15977 FSQLITE_VDBE_COLUMN_READS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
15978 record_decoded_value_metrics(&SqliteValue::Null);
15979 }
15980 return Ok(SqliteValue::Null);
15981 }
15982
15983 if let Some(cursor) = self.cursors.get(&cursor_id) {
15986 if cursor.is_pseudo {
15987 if let Some(row) = &cursor.pseudo_row {
15988 let value = row.get(col_idx).cloned().unwrap_or(SqliteValue::Null);
15989 if collect_vdbe_metrics {
15990 record_decoded_value_metrics(&value);
15991 }
15992 return Ok(value);
15993 }
15994 if let Some(reg) = cursor.pseudo_reg {
15995 let blob = self.get_reg(reg).clone();
15996
15997 let use_cache = if let Some(cursor) = self.cursors.get(&cursor_id) {
15998 if let Some((cached_blob, _)) = &cursor.cached_pseudo_row {
15999 cached_blob == &blob
16000 } else {
16001 false
16002 }
16003 } else {
16004 false
16005 };
16006
16007 if !use_cache {
16008 if let Some(cursor) = self.cursors.get(&cursor_id)
16009 && let Some((cached_blob, _)) = &cursor.cached_pseudo_row
16010 && cached_blob != &blob
16011 {
16012 note_decode_cache_invalidation(
16013 collect_vdbe_metrics,
16014 DecodeCacheInvalidationReason::PseudoRowChange,
16015 );
16016 }
16017 note_decode_cache_miss(collect_vdbe_metrics);
16018 if let Ok(values) = decode_record_with_metrics(&blob, collect_vdbe_metrics)
16019 {
16020 if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
16021 cursor.cached_pseudo_row = Some((blob, values));
16022 }
16023 } else if let Some(cursor) = self.cursors.get_mut(&cursor_id) {
16024 cursor.cached_pseudo_row = None;
16025 }
16026 }
16027
16028 if let Some(cursor) = self.cursors.get(&cursor_id) {
16029 if let Some((_, values)) = &cursor.cached_pseudo_row {
16030 if use_cache {
16031 note_decode_cache_hit(collect_vdbe_metrics);
16032 }
16033 let value = values.get(col_idx).cloned().unwrap_or(SqliteValue::Null);
16034 if collect_vdbe_metrics {
16035 record_decoded_value_metrics(&value);
16036 }
16037 return Ok(value);
16038 }
16039 }
16040 }
16041 return Ok(SqliteValue::Null);
16042 }
16043 if let Some(pos) = cursor.position
16044 && let Some(db) = self.db.as_ref()
16045 && let Some(table) = db.get_table(cursor.root_page)
16046 && let Some(row) = table.rows.get(pos)
16047 {
16048 let value = row
16049 .values
16050 .get(col_idx)
16051 .cloned()
16052 .unwrap_or(SqliteValue::Null);
16053 if collect_vdbe_metrics {
16054 record_decoded_value_metrics(&value);
16055 }
16056 return Ok(value);
16057 }
16058 }
16059
16060 if let Some(sorter) = self.sorters.get_mut(&cursor_id) {
16062 if let Some(pos) = sorter.position {
16063 if let Some(value) = sorter
16064 .rows
16065 .get(pos)
16066 .and_then(|row| row.values.get(col_idx))
16067 .cloned()
16068 {
16069 note_decode_cache_hit(collect_vdbe_metrics);
16070 if collect_vdbe_metrics {
16071 record_decoded_value_metrics(&value);
16072 }
16073 return Ok(value);
16074 }
16075 let refresh_state = ensure_sorter_row_cache(sorter, collect_vdbe_metrics, pos)?;
16076 let rows = &sorter.rows;
16077 let cached_row_decode = &mut sorter.cached_row_decode;
16078 let row = rows.get(pos).ok_or_else(|| FrankenError::DatabaseCorrupt {
16079 detail: format!("missing sorter row at position {pos}"),
16080 })?;
16081 let value = if col_idx < cached_row_decode.column_count() {
16082 let cached_value_ready = cached_row_decode.cached_value_ready(col_idx);
16083 if cached_value_ready {
16084 if refresh_state.refreshed && refresh_state.eager_values_ready {
16085 note_decode_cache_miss(collect_vdbe_metrics);
16086 } else {
16087 note_decode_cache_hit(collect_vdbe_metrics);
16088 }
16089 cached_row_decode
16090 .cached_value(col_idx)
16091 .cloned()
16092 .unwrap_or(SqliteValue::Null)
16093 } else if let Some(value) = fsqlite_types::record::decode_column_from_offset(
16094 &row.blob,
16095 cached_row_decode
16096 .column_offset(col_idx)
16097 .expect("col_idx checked against column_count"),
16098 collect_vdbe_metrics,
16099 ) {
16100 note_decode_cache_miss(collect_vdbe_metrics);
16101 cached_row_decode.cache_decoded(col_idx, value.clone());
16102 value
16103 } else {
16104 return Err(FrankenError::DatabaseCorrupt {
16105 detail: format!(
16106 "malformed sorter record while reading column {col_idx}"
16107 ),
16108 });
16109 }
16110 } else {
16111 SqliteValue::Null
16112 };
16113 if collect_vdbe_metrics {
16114 record_decoded_value_metrics(&value);
16115 }
16116 return Ok(value);
16117 }
16118 }
16119
16120 Ok(SqliteValue::Null)
16121 }
16122
16123 async fn cursor_rowid(&mut self, cursor_id: i32) -> Result<SqliteValue> {
16125 if let Some(cursor) = self.storage_cursors.get_mut(&cursor_id) {
16126 if cursor.cursor.eof() {
16127 return Ok(SqliteValue::Null);
16128 }
16129 return Ok(SqliteValue::Integer(
16130 storage_cursor_cached_rowid(cursor).await?,
16131 ));
16132 }
16133
16134 if let Some(state) = self
16135 .cold_state()
16136 .and_then(|cold_state| cold_state.vtab_cursors.get(&cursor_id))
16137 {
16138 if state.null_row || state.cursor.eof() {
16139 return Ok(SqliteValue::Null);
16140 }
16141 return Ok(SqliteValue::Integer(state.cursor.rowid()?));
16142 }
16143
16144 if let Some(cursor) = self.cursors.get(&cursor_id)
16145 && let Some(pos) = cursor.position
16146 && let Some(db) = self.db.as_ref()
16147 && let Some(table) = db.get_table(cursor.root_page)
16148 && let Some(row) = table.rows.get(pos)
16149 {
16150 return Ok(SqliteValue::Integer(row.rowid));
16151 }
16152 Ok(SqliteValue::Null)
16153 }
16154
16155 #[allow(clippy::cast_sign_loss)]
16156 async fn open_storage_cursor(
16157 &mut self,
16158 cursor_id: i32,
16159 root_page: i32,
16160 writable: bool,
16161 ) -> bool {
16162 let _page_size_u32 = self.page_size.get();
16163 let trace_id = self.execution_cx.trace_id();
16166 let certifying_mode = self.reject_mem_fallback;
16167 let mode = if self.reject_mem_fallback {
16168 "parity_cert"
16169 } else {
16170 "fallback_allowed"
16171 };
16172
16173 let Some(root_pgno) = PageNumber::new(root_page as u32) else {
16174 Self::log_open_storage_cursor_fallback_decision(
16175 trace_id,
16176 certifying_mode,
16177 cursor_id,
16178 root_page,
16179 writable,
16180 "none",
16181 "invalid_page_number",
16182 "refused",
16183 "invalid root page number",
16184 );
16185 tracing::debug!(
16186 cursor_id,
16187 root_page,
16188 writable,
16189 mode,
16190 backend_kind = "none",
16191 decision_reason = "invalid_page_number",
16192 "open_storage_cursor: invalid root page number"
16193 );
16194 return false;
16195 };
16196
16197 let has_txn = self.txn_page_io.is_some();
16198 let txn_cx = self.derive_execution_cx();
16199 let (rowid_mode, autoincrement_high_water) = self.storage_cursor_runtime_meta(root_page);
16200 if self.cursor_root_pages.get(&cursor_id).copied() == Some(root_page)
16201 && let Some(existing) = self.storage_cursors.get_mut(&cursor_id)
16202 {
16203 let backend_matches = if has_txn {
16204 existing.cursor.is_txn()
16205 } else {
16206 existing.cursor.is_mem()
16207 };
16208 if backend_matches {
16209 existing.writable |= writable;
16210 existing.cx = txn_cx.clone();
16211 existing.root_page = root_page;
16212 existing.rowid_mode = rowid_mode;
16213 existing.autoincrement_high_water = autoincrement_high_water;
16214 self.cursor_root_pages.insert(cursor_id, root_page);
16215 tracing::trace!(
16216 cursor_id,
16217 page_id = root_page,
16218 writable,
16219 has_txn,
16220 backend_kind = existing.cursor.kind_str(),
16221 "open_storage_cursor: reused retained cursor"
16222 );
16223 return true;
16224 }
16225 }
16226 let mut mem_decision_reason = "no_pager_transaction";
16227
16228 'pager_block: {
16245 if let Some(ref mut page_io) = self.txn_page_io {
16246 let page_data = match page_io.read_page(&txn_cx, root_pgno).await {
16250 Ok(bytes) => bytes,
16251 Err(err) => {
16252 let has_mem_table = self
16254 .db
16255 .as_ref()
16256 .is_some_and(|db| db.get_table(root_page).is_some());
16257 if has_mem_table && !self.reject_mem_fallback && !writable {
16258 mem_decision_reason = "pager_read_failed_mem_fallback";
16259 Self::log_open_storage_cursor_fallback_decision(
16260 trace_id,
16261 certifying_mode,
16262 cursor_id,
16263 root_page,
16264 writable,
16265 "mem",
16266 "pager_read_failed_mem_fallback",
16267 "vdbe_mempage_fallback_allowed",
16268 "pager read failed and MemDatabase owns read-only root page",
16269 );
16270 tracing::debug!(
16271 cursor_id,
16272 page_id = root_page,
16273 writable,
16274 has_txn,
16275 mode,
16276 backend_kind = "mem",
16277 decision_reason = "pager_read_failed_mem_fallback",
16278 error = %err,
16279 "open_storage_cursor: pager read failed, falling through to MemDatabase"
16280 );
16281 break 'pager_block;
16283 }
16284 Self::log_open_storage_cursor_fallback_decision(
16285 trace_id,
16286 certifying_mode,
16287 cursor_id,
16288 root_page,
16289 writable,
16290 "txn",
16291 "pager_read_failed",
16292 "refused",
16293 "pager read failed before storage cursor dispatch",
16294 );
16295 tracing::warn!(
16296 cursor_id,
16297 page_id = root_page,
16298 writable,
16299 has_txn,
16300 mode,
16301 backend_kind = "txn",
16302 decision_reason = "pager_read_failed",
16303 error = %err,
16304 has_mem_table,
16305 reject_mem_fallback = self.reject_mem_fallback,
16306 "open_storage_cursor: failed to read root page from pager"
16307 );
16308 return false;
16309 }
16310 };
16311 let hdr_offset = header_offset_for_page(root_pgno);
16312 let parsed_header = BtreePageHeader::parse(&page_data, hdr_offset).ok();
16313 let is_valid_btree =
16319 parsed_header.is_some() || (!page_data.is_empty() && page_data[0] != 0x00);
16320 let is_zero_page = page_data.iter().all(|&byte| byte == 0);
16321
16322 let page_layout = btree_cursor_page_layout_for_reader_or_default(
16323 page_io,
16324 &txn_cx,
16325 self.page_size,
16326 )
16327 .await;
16328
16329 if is_valid_btree {
16330 let (is_table_btree, detected_page_type) = if let Some(header) = parsed_header {
16334 (header.page_type.is_table(), Some(header.page_type))
16335 } else {
16336 let type_byte = page_data.get(hdr_offset).copied().unwrap_or(0);
16340 let is_table = match type_byte {
16341 0x02 | 0x0A => false, 0x05 | 0x0D => true, _ => {
16344 tracing::warn!(
16345 cursor_id,
16346 page_id = root_page,
16347 type_byte,
16348 "open_storage_cursor: unparseable header with unknown page-type byte, defaulting to table"
16349 );
16350 true
16351 }
16352 };
16353 (is_table, None)
16354 };
16355 let mut cursor = BtCursor::new_with_index_desc(
16356 page_io.clone(),
16357 root_pgno,
16358 page_layout.usable_size,
16359 is_table_btree,
16360 if is_table_btree {
16361 Vec::new()
16362 } else {
16363 self.index_desc_flags_for_root(root_page)
16364 },
16365 );
16366 if !is_table_btree {
16367 cursor.set_index_collation_context(
16368 self.index_collations_for_root(root_page),
16369 Arc::clone(&self.collation_registry),
16370 );
16371 }
16372 configure_btree_cursor_page_size(&mut cursor, page_layout);
16373 let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16374 let table_column_count = self
16375 .table_column_count_by_root_page
16376 .get(&root_page)
16377 .copied();
16378 let first_not_null_non_ipk_col = self
16379 .first_not_null_non_ipk_col_by_root_page
16380 .get(&root_page)
16381 .copied();
16382 self.storage_cursors.insert(
16383 cursor_id,
16384 StorageCursor {
16385 cursor: CursorBackend::Txn(cursor),
16386 cx: txn_cx,
16387 writable,
16388 root_page,
16389 rowid_mode,
16390 autoincrement_high_water,
16391 last_alloc_rowid: 0,
16392 last_successful_insert_rowid: None,
16393 last_rightmost_unique_index_prefix: None,
16394 last_rightmost_unique_index_position: None,
16395 payload_buf: Vec::new(),
16396 target_vals_buf: Vec::new(),
16397 cur_vals_buf: Vec::new(),
16398 row_decode: RowDecodeScratch::default(),
16399 last_position_stamp: None,
16400 cached_rowid: None,
16401 payload_includes_rowid_alias: None,
16402 ipk_col_idx,
16403 table_column_count,
16404 first_not_null_non_ipk_col,
16405 },
16406 );
16407 self.cursor_root_pages.insert(cursor_id, root_page);
16408 Self::log_open_storage_cursor_fallback_decision(
16409 trace_id,
16410 certifying_mode,
16411 cursor_id,
16412 root_page,
16413 writable,
16414 "txn",
16415 "valid_btree_page",
16416 "real_backend_dispatch",
16417 "pager transaction opened a valid B-tree page",
16418 );
16419 tracing::debug!(
16420 cursor_id,
16421 page_id = root_page,
16422 writable,
16423 has_txn,
16424 mode,
16425 backend_kind = "txn",
16426 decision_reason = "valid_btree_page",
16427 detected_page_type = ?detected_page_type,
16428 is_table_btree,
16429 "open_storage_cursor: routed through pager transaction"
16430 );
16431 return true;
16432 }
16433
16434 if writable && is_zero_page {
16437 let is_known_index =
16447 self.index_desc_flags_by_root_page.contains_key(&root_page)
16448 || self.index_collations_by_root_page.contains_key(&root_page);
16449 let is_table_btree = if is_known_index {
16450 false
16451 } else {
16452 self.db
16453 .as_ref()
16454 .is_none_or(|db| db.get_table(root_page).is_some())
16455 };
16456 let init_page_type = if is_table_btree {
16457 BtreePageType::LeafTable
16458 } else {
16459 BtreePageType::LeafIndex
16460 };
16461 let mut page = vec![0u8; page_layout.page_size as usize];
16465 page[0] = if is_table_btree {
16466 fsqlite_types::BTreePageType::LeafTable as u8
16467 } else {
16468 fsqlite_types::BTreePageType::LeafIndex as u8
16469 };
16470 let content_raw = if page_layout.usable_size >= 65_536 {
16471 0u16
16472 } else {
16473 page_layout.usable_size as u16
16474 };
16475 page[5..7].copy_from_slice(&content_raw.to_be_bytes());
16476
16477 if let Err(err) = page_io
16483 .write_page_data(&txn_cx, root_pgno, PageData::from_vec(page))
16484 .await
16485 {
16486 Self::log_open_storage_cursor_fallback_decision(
16487 trace_id,
16488 certifying_mode,
16489 cursor_id,
16490 root_page,
16491 writable,
16492 "txn",
16493 "zero_page_init_failed",
16494 "refused",
16495 "failed to initialize writable root page in pager",
16496 );
16497 tracing::warn!(
16498 cursor_id,
16499 page_id = root_page,
16500 writable,
16501 has_txn,
16502 mode,
16503 backend_kind = "txn",
16504 decision_reason = "zero_page_init_failed",
16505 error = %err,
16506 "open_storage_cursor: failed to initialize writable root page in pager"
16507 );
16508 return false;
16509 }
16510 let mut cursor = BtCursor::new_with_index_desc(
16511 page_io.clone(),
16512 root_pgno,
16513 page_layout.usable_size,
16514 is_table_btree,
16515 if is_table_btree {
16516 Vec::new()
16517 } else {
16518 self.index_desc_flags_for_root(root_page)
16519 },
16520 );
16521 if !is_table_btree {
16522 cursor.set_index_collation_context(
16523 self.index_collations_for_root(root_page),
16524 Arc::clone(&self.collation_registry),
16525 );
16526 }
16527 configure_btree_cursor_page_size(&mut cursor, page_layout);
16528 let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16529 let table_column_count = self
16530 .table_column_count_by_root_page
16531 .get(&root_page)
16532 .copied();
16533 let first_not_null_non_ipk_col = self
16534 .first_not_null_non_ipk_col_by_root_page
16535 .get(&root_page)
16536 .copied();
16537 self.storage_cursors.insert(
16538 cursor_id,
16539 StorageCursor {
16540 cursor: CursorBackend::Txn(cursor),
16541 cx: txn_cx,
16542 writable,
16543 root_page,
16544 rowid_mode,
16545 autoincrement_high_water,
16546 last_alloc_rowid: 0,
16547 last_successful_insert_rowid: None,
16548 last_rightmost_unique_index_prefix: None,
16549 last_rightmost_unique_index_position: None,
16550 payload_buf: Vec::new(),
16551 target_vals_buf: Vec::new(),
16552 cur_vals_buf: Vec::new(),
16553 row_decode: RowDecodeScratch::default(),
16554 last_position_stamp: None,
16555 cached_rowid: None,
16556 payload_includes_rowid_alias: None,
16557 ipk_col_idx,
16558 table_column_count,
16559 first_not_null_non_ipk_col,
16560 },
16561 );
16562 self.cursor_root_pages.insert(cursor_id, root_page);
16563 Self::log_open_storage_cursor_fallback_decision(
16564 trace_id,
16565 certifying_mode,
16566 cursor_id,
16567 root_page,
16568 writable,
16569 "txn",
16570 "zero_page_initialized",
16571 "real_backend_dispatch",
16572 "initialized empty root page through pager transaction",
16573 );
16574 tracing::debug!(
16575 cursor_id,
16576 page_id = root_page,
16577 writable,
16578 has_txn,
16579 mode,
16580 backend_kind = "txn",
16581 decision_reason = "zero_page_initialized",
16582 initialized_page_type = ?init_page_type,
16583 is_table_btree,
16584 "open_storage_cursor: initialized empty root page via pager"
16585 );
16586 return true;
16587 }
16588
16589 let has_mem_table = self
16594 .db
16595 .as_ref()
16596 .is_some_and(|db| db.get_table(root_page).is_some());
16597 if !has_mem_table || writable {
16598 Self::log_open_storage_cursor_fallback_decision(
16600 trace_id,
16601 certifying_mode,
16602 cursor_id,
16603 root_page,
16604 writable,
16605 "none",
16606 "invalid_page_no_mem_fallback",
16607 "refused",
16608 "invalid transaction-backed root page has no allowed MemDatabase fallback",
16609 );
16610 tracing::warn!(
16611 cursor_id,
16612 page_id = root_page,
16613 writable,
16614 has_txn,
16615 mode,
16616 backend_kind = "none",
16617 decision_reason = "invalid_page_no_mem_fallback",
16618 first_byte = page_data.first().copied().unwrap_or_default(),
16619 is_zero_page,
16620 has_mem_table,
16621 "open_storage_cursor: refusing on invalid transaction-backed root page"
16622 );
16623 return false;
16624 }
16625 mem_decision_reason = "txn_page_invalid_mem_fallback";
16627 Self::log_open_storage_cursor_fallback_decision(
16628 trace_id,
16629 certifying_mode,
16630 cursor_id,
16631 root_page,
16632 writable,
16633 "mem",
16634 "txn_page_invalid_mem_fallback",
16635 "vdbe_mempage_fallback_allowed",
16636 "invalid pager page can be served by read-only MemDatabase table",
16637 );
16638 tracing::debug!(
16639 cursor_id,
16640 page_id = root_page,
16641 writable,
16642 has_txn,
16643 mode,
16644 backend_kind = "mem",
16645 decision_reason = "txn_page_invalid_mem_fallback",
16646 "open_storage_cursor: pager page invalid, falling through to MemDatabase"
16647 );
16648 } } if self.reject_mem_fallback {
16653 Self::log_open_storage_cursor_fallback_decision(
16654 trace_id,
16655 certifying_mode,
16656 cursor_id,
16657 root_page,
16658 writable,
16659 "mem",
16660 "parity_cert_rejection",
16661 "vdbe_mempage_fallback_rejected",
16662 "MemPageStore fallback rejected in parity-cert mode",
16663 );
16664 tracing::warn!(
16665 cursor_id,
16666 page_id = root_page,
16667 writable,
16668 has_txn,
16669 mode,
16670 backend_kind = "mem",
16671 decision_reason = "parity_cert_rejection",
16672 "open_storage_cursor: MemPageStore fallback rejected in parity-cert mode"
16673 );
16674 return false;
16675 }
16676
16677 let is_table_btree = self
16681 .db
16682 .as_ref()
16683 .is_none_or(|db| db.get_table(root_page).is_some());
16684 let transient_root_pgno = transient_mem_root_pgno(root_pgno);
16685 let store = if is_table_btree {
16686 MemPageStore::with_empty_table(transient_root_pgno, self.page_size.get())
16687 } else {
16688 MemPageStore::with_empty_index(transient_root_pgno, self.page_size.get())
16689 };
16690 let cx = self.derive_execution_cx();
16691 let mut cursor = BtCursor::new_with_index_desc(
16692 store,
16693 transient_root_pgno,
16694 self.page_size.get(),
16695 is_table_btree,
16696 if is_table_btree {
16697 Vec::new()
16698 } else {
16699 self.index_desc_flags_for_root(root_page)
16700 },
16701 );
16702 if !is_table_btree {
16703 cursor.set_index_collation_context(
16704 self.index_collations_for_root(root_page),
16705 Arc::clone(&self.collation_registry),
16706 );
16707 }
16708 if is_table_btree
16710 && let Some(table) = self.db.as_ref().and_then(|db| db.get_table(root_page))
16711 {
16712 for row in &table.rows {
16713 let payload = encode_record(&row.values);
16714 if cursor.table_insert(&cx, row.rowid, &payload).await.is_err() {
16715 return false;
16716 }
16717 }
16718 }
16719
16720 let ipk_col_idx = self.rowid_alias_col_by_root_page.get(&root_page).copied();
16721 let table_column_count = self
16722 .table_column_count_by_root_page
16723 .get(&root_page)
16724 .copied();
16725 let first_not_null_non_ipk_col = self
16726 .first_not_null_non_ipk_col_by_root_page
16727 .get(&root_page)
16728 .copied();
16729 self.storage_cursors.insert(
16730 cursor_id,
16731 StorageCursor {
16732 cursor: CursorBackend::Mem(cursor),
16733 cx,
16734 writable,
16735 root_page,
16736 rowid_mode,
16737 autoincrement_high_water,
16738 last_alloc_rowid: 0,
16739 last_successful_insert_rowid: None,
16740 last_rightmost_unique_index_prefix: None,
16741 last_rightmost_unique_index_position: None,
16742 payload_buf: Vec::new(),
16743 target_vals_buf: Vec::new(),
16744 cur_vals_buf: Vec::new(),
16745 row_decode: RowDecodeScratch::default(),
16746 last_position_stamp: None,
16747 cached_rowid: None,
16748 payload_includes_rowid_alias: None,
16749 ipk_col_idx,
16750 table_column_count,
16751 first_not_null_non_ipk_col,
16752 },
16753 );
16754 self.cursor_root_pages.insert(cursor_id, root_page);
16755 Self::log_open_storage_cursor_fallback_decision(
16756 trace_id,
16757 certifying_mode,
16758 cursor_id,
16759 root_page,
16760 writable,
16761 "mem",
16762 mem_decision_reason,
16763 "vdbe_mempage_fallback_allowed",
16764 "routed through transient MemPageStore fallback",
16765 );
16766 tracing::debug!(
16767 cursor_id,
16768 page_id = root_page,
16769 writable,
16770 has_txn,
16771 mode,
16772 backend_kind = "mem",
16773 decision_reason = mem_decision_reason,
16774 is_table_btree,
16775 "open_storage_cursor: routed through MemPageStore fallback"
16776 );
16777 true
16778 }
16779
16780 fn trace_opcode(&self, pc: usize, op: &VdbeOp) {
16781 if !self.trace_opcodes || !tracing::enabled!(tracing::Level::TRACE) {
16782 return;
16783 }
16784 let spans = opcode_register_spans(op);
16785 tracing::trace!(
16786 target: "fsqlite_vdbe::opcode",
16787 logging_standard = VDBE_TRACE_LOGGING_STANDARD,
16788 pc,
16789 opcode = %op.opcode.name(),
16790 p1 = op.p1,
16791 p2 = op.p2,
16792 p3 = op.p3,
16793 p5 = op.p5,
16794 read_start = spans.read_start,
16795 read_len = spans.read_len,
16796 write_start = spans.write_start,
16797 write_len = spans.write_len,
16798 "executing vdbe opcode",
16799 );
16800 }
16801}
16802
16803const SQLITE_AFF_MASK: u16 = 0x47;
16812const SQLITE_AFF_TEXT: u16 = 0x42; const SQLITE_AFF_NUMERIC: u16 = 0x43; fn vdbe_real_is_truthy(val: &SqliteValue) -> bool {
16818 match val {
16819 SqliteValue::Null => false,
16820 SqliteValue::Integer(n) => *n != 0,
16821 SqliteValue::Float(f) => *f != 0.0,
16822 SqliteValue::Text(_) | SqliteValue::Blob(_) => {
16823 let i = val.to_integer();
16824 if i != 0 {
16825 return true;
16826 }
16827 val.to_float() != 0.0
16828 }
16829 }
16830}
16831
16832fn coerce_for_comparison<'a>(
16840 lhs: &'a SqliteValue,
16841 rhs: &'a SqliteValue,
16842 p5: u16,
16843) -> (
16844 std::borrow::Cow<'a, SqliteValue>,
16845 std::borrow::Cow<'a, SqliteValue>,
16846) {
16847 use std::borrow::Cow;
16848
16849 let affinity = p5 & SQLITE_AFF_MASK;
16850
16851 if affinity == SQLITE_AFF_TEXT {
16853 let coerce_to_text = |v: &SqliteValue| -> Option<SqliteValue> {
16854 match v {
16855 SqliteValue::Integer(_) | SqliteValue::Float(_) => {
16856 Some(SqliteValue::Text(v.to_text().into()))
16857 }
16858 _ => None,
16859 }
16860 };
16861 let new_lhs = coerce_to_text(lhs);
16862 let new_rhs = coerce_to_text(rhs);
16863 return (
16864 new_lhs.map_or_else(|| Cow::Borrowed(lhs), Cow::Owned),
16865 new_rhs.map_or_else(|| Cow::Borrowed(rhs), Cow::Owned),
16866 );
16867 }
16868
16869 if affinity >= SQLITE_AFF_NUMERIC {
16873 let coerce_to_numeric = |value: &SqliteValue| match value {
16874 SqliteValue::Text(text) => try_coerce_text_to_numeric_cmp(text),
16875 _ => None,
16876 };
16877 let new_lhs = coerce_to_numeric(lhs);
16878 let new_rhs = coerce_to_numeric(rhs);
16879 return (
16880 new_lhs.map_or_else(|| Cow::Borrowed(lhs), Cow::Owned),
16881 new_rhs.map_or_else(|| Cow::Borrowed(rhs), Cow::Owned),
16882 );
16883 }
16884
16885 (Cow::Borrowed(lhs), Cow::Borrowed(rhs))
16886}
16887
16888fn values_equal_without_collation(lhs: &SqliteValue, rhs: &SqliteValue) -> bool {
16889 if lhs.is_null() || rhs.is_null() {
16890 return false;
16891 }
16892 if let (SqliteValue::Integer(a), SqliteValue::Integer(b)) = (lhs, rhs) {
16893 return a == b;
16894 }
16895 let (cmp_lhs, cmp_rhs) = coerce_for_comparison(lhs, rhs, 0);
16896 cmp_lhs.partial_cmp(&cmp_rhs) == Some(Ordering::Equal)
16897}
16898
16899fn try_coerce_text_to_numeric_cmp(s: &str) -> Option<SqliteValue> {
16901 let trimmed = s.trim();
16902 if trimmed.is_empty() {
16903 return None;
16904 }
16905 if let Ok(i) = trimmed.parse::<i64>() {
16907 return Some(SqliteValue::Integer(i));
16908 }
16909 if let Ok(f) = trimmed.parse::<f64>() {
16911 if !f.is_finite() {
16912 let lower = trimmed.to_ascii_lowercase();
16913 if lower.contains("inf") || lower.contains("nan") {
16914 return None;
16915 }
16916 }
16917 return Some(SqliteValue::Float(f));
16918 }
16919 None
16920}
16921
16922fn collate_compare(
16923 lhs: &SqliteValue,
16924 rhs: &SqliteValue,
16925 coll_name: &str,
16926 collation_registry: &CollationRegistry,
16927) -> Option<std::cmp::Ordering> {
16928 match (lhs, rhs) {
16929 (SqliteValue::Text(l), SqliteValue::Text(r)) => Some(compare_text_with_collation(
16930 l.as_bytes_direct(),
16931 r.as_bytes_direct(),
16932 coll_name,
16933 collation_registry,
16934 )),
16935 _ => lhs.partial_cmp(rhs),
16936 }
16937}
16938
16939fn compare_text_with_collation(
16940 left: &[u8],
16941 right: &[u8],
16942 coll_name: &str,
16943 collation_registry: &CollationRegistry,
16944) -> Ordering {
16945 collation_registry
16946 .find(coll_name)
16947 .map(|collation| collation.compare(left, right))
16948 .unwrap_or_else(|| left.cmp(right))
16949}
16950
16951fn builtin_collation_compare_text(
16952 left: &SmallText,
16953 right: &SmallText,
16954 coll_name: &str,
16955) -> Option<Ordering> {
16956 let left = left.as_bytes_direct();
16957 let right = right.as_bytes_direct();
16958 if coll_name.eq_ignore_ascii_case("BINARY") {
16959 return Some(left.cmp(right));
16960 }
16961 if coll_name.eq_ignore_ascii_case("NOCASE") {
16962 return Some(compare_ascii_nocase_bytes(left, right));
16963 }
16964 if coll_name.eq_ignore_ascii_case("RTRIM") {
16965 return Some(trim_rtrim_collation_text(left).cmp(trim_rtrim_collation_text(right)));
16966 }
16967 None
16968}
16969
16970fn cmp_sqlite_values_collated(a: &SqliteValue, b: &SqliteValue, coll: &str) -> Ordering {
16971 match (a, b) {
16972 (SqliteValue::Text(l), SqliteValue::Text(r)) => builtin_collation_compare_text(l, r, coll)
16973 .unwrap_or_else(|| l.as_bytes_direct().cmp(r.as_bytes_direct())),
16974 _ => a.partial_cmp(b).unwrap_or(Ordering::Equal),
16975 }
16976}
16977
16978fn agg_step_min_max_collated(
16979 state: &mut Box<dyn Any + Send>,
16980 candidate: &SqliteValue,
16981 is_max: bool,
16982 coll: &str,
16983) {
16984 let current: &mut Option<SqliteValue> = state
16985 .downcast_mut()
16986 .expect("MIN/MAX aggregate state must be Option<SqliteValue>");
16987 match current {
16988 None => *current = Some(candidate.clone()),
16989 &mut Some(ref cur) => {
16990 let ord = cmp_sqlite_values_collated(candidate, cur, coll);
16991 if (is_max && ord == Ordering::Greater) || (!is_max && ord == Ordering::Less) {
16992 *current = Some(candidate.clone());
16993 }
16994 }
16995 }
16996}
16997
16998fn compare_ascii_nocase_bytes(left: &[u8], right: &[u8]) -> Ordering {
16999 let shared_len = left.len().min(right.len());
17000 for idx in 0..shared_len {
17001 let l = left[idx].to_ascii_lowercase();
17002 let r = right[idx].to_ascii_lowercase();
17003 match l.cmp(&r) {
17004 Ordering::Equal => {}
17005 non_equal => return non_equal,
17006 }
17007 }
17008 left.len().cmp(&right.len())
17009}
17010
17011fn fast_compare_same_storage_class(
17012 lhs: &SqliteValue,
17013 rhs: &SqliteValue,
17014 p4: &P4,
17015 p5: u16,
17016) -> Option<Option<Ordering>> {
17017 let affinity = p5 & SQLITE_AFF_MASK;
17018 match (lhs, rhs) {
17019 (SqliteValue::Integer(a), SqliteValue::Integer(b)) if affinity != SQLITE_AFF_TEXT => {
17020 Some(Some(a.cmp(b)))
17021 }
17022 (SqliteValue::Float(a), SqliteValue::Float(b)) if affinity != SQLITE_AFF_TEXT => {
17023 Some(a.partial_cmp(b))
17024 }
17025 (SqliteValue::Text(a), SqliteValue::Text(b)) if affinity < SQLITE_AFF_NUMERIC => match p4 {
17026 P4::Collation(coll_name) => builtin_collation_compare_text(a, b, coll_name).map(Some),
17027 _ => Some(Some(a.as_bytes_direct().cmp(b.as_bytes_direct()))),
17028 },
17029 (SqliteValue::Blob(a), SqliteValue::Blob(b)) if !matches!(p4, P4::Collation(_)) => {
17030 Some(Some(a.as_ref().cmp(b.as_ref())))
17031 }
17032 _ => None,
17033 }
17034}
17035
17036fn extract_collation_names_owned(p4: &P4) -> Vec<Option<String>> {
17037 match p4 {
17038 P4::Collation(name) => vec![Some(name.clone())],
17039 P4::Str(spec) => spec
17040 .split([',', '|', '\0'])
17041 .map(str::trim)
17042 .filter(|entry| !entry.is_empty())
17043 .map(|s| Some(s.to_owned()))
17044 .collect(),
17045 _ => Vec::new(),
17046 }
17047}
17048
17049fn compare_collation_for_field_from_p4(p4: &P4, field_idx: usize) -> Option<&str> {
17050 match p4 {
17051 P4::Collation(name) => Some(name.as_str()),
17052 P4::Str(spec) => {
17053 let mut idx = 0;
17054 for entry in spec.split([',', '|', '\0']) {
17055 let trimmed = entry.trim();
17056 if trimmed.is_empty() {
17057 continue;
17058 }
17059 if idx == field_idx {
17060 return Some(trimmed);
17061 }
17062 idx += 1;
17063 }
17064 if idx == 1 && field_idx > 0 {
17065 for entry in spec.split([',', '|', '\0']) {
17066 let trimmed = entry.trim();
17067 if !trimmed.is_empty() {
17068 return Some(trimmed);
17069 }
17070 }
17071 }
17072 None
17073 }
17074 _ => None,
17075 }
17076}
17077
17078fn try_decode_storage_cursor_target_index_record(
17083 cursor: &mut StorageCursor,
17084 key_bytes: &[u8],
17085) -> bool {
17086 cursor.target_vals_buf.clear();
17087 fsqlite_types::record::parse_record_into(key_bytes, &mut cursor.target_vals_buf).is_some()
17088}
17089
17090fn decode_storage_cursor_target_index_record_strict(
17091 cursor: &mut StorageCursor,
17092 key_bytes: &[u8],
17093 malformed_detail: &'static str,
17094) -> Result<()> {
17095 try_decode_storage_cursor_target_index_record(cursor, key_bytes)
17096 .then_some(())
17097 .ok_or_else(|| FrankenError::internal(malformed_detail))
17098}
17099
17100async fn try_decode_storage_cursor_current_index_record(
17101 cursor_id: i32,
17102 cursor: &mut StorageCursor,
17103) -> Result<bool> {
17104 refresh_storage_cursor_first_key_state(cursor, false);
17105 cursor.payload_buf.clear();
17106 cursor
17107 .cursor
17108 .payload_into(&cursor.cx, &mut cursor.payload_buf)
17109 .await?;
17110 cursor.cur_vals_buf.clear();
17111 let decoded =
17112 fsqlite_types::record::parse_record_into(&cursor.payload_buf, &mut cursor.cur_vals_buf)
17113 .is_some();
17114 cursor.cached_rowid = if decoded {
17115 cursor.cur_vals_buf.last().and_then(SqliteValue::as_integer)
17116 } else {
17117 None
17118 };
17119 tracing::trace!(
17120 cursor_id,
17121 decoded,
17122 payload_len = cursor.payload_buf.len(),
17123 decoded_value_count = cursor.cur_vals_buf.len(),
17124 scratch_payload_capacity = cursor.payload_buf.capacity(),
17125 scratch_value_capacity = cursor.cur_vals_buf.capacity(),
17126 scratch_model = "per_cursor",
17127 decode_path = "storage_cursor_index_probe",
17128 "decoded storage cursor index probe into reusable scratch"
17129 );
17130 Ok(decoded)
17131}
17132
17133async fn decode_storage_cursor_current_index_record_strict(
17134 cursor_id: i32,
17135 cursor: &mut StorageCursor,
17136 malformed_detail: &'static str,
17137) -> Result<()> {
17138 try_decode_storage_cursor_current_index_record(cursor_id, cursor)
17139 .await?
17140 .then_some(())
17141 .ok_or_else(|| FrankenError::internal(malformed_detail))
17142}
17143
17144async fn storage_cursor_no_conflict_prefix_match(
17145 cursor_id: i32,
17146 cursor: &mut StorageCursor,
17147 key_bytes: &[u8],
17148) -> Result<bool> {
17149 if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17150 return Ok(false);
17151 }
17152 cursor.cursor.index_move_to(&cursor.cx, key_bytes).await?;
17153 if cursor.cursor.eof() {
17154 return Ok(false);
17155 }
17156 if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17157 return Ok(false);
17158 }
17159 let prefix_len = cursor.target_vals_buf.len();
17160 Ok(cursor.cur_vals_buf.len() >= prefix_len
17161 && cursor.cur_vals_buf[..prefix_len] == cursor.target_vals_buf[..])
17162}
17163
17164async fn storage_cursor_no_conflict_prefix_match_collated(
17165 cursor_id: i32,
17166 cursor: &mut StorageCursor,
17167 key_bytes: &[u8],
17168 desc_flags: &[bool],
17169 collations: &[Option<String>],
17170 collation_registry: &CollationRegistry,
17171) -> Result<bool> {
17172 if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17173 return Ok(false);
17174 }
17175 let prefix_len = cursor.target_vals_buf.len();
17176 if !cursor.cursor.first(&cursor.cx).await? {
17177 return Ok(false);
17178 }
17179 if prefix_len == 0 {
17180 return Ok(true);
17181 }
17182
17183 loop {
17184 if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17185 return Ok(false);
17186 }
17187 if cursor.cur_vals_buf.len() >= prefix_len
17188 && compare_index_prefix_keys(
17189 &cursor.cur_vals_buf,
17190 &cursor.target_vals_buf,
17191 prefix_len,
17192 desc_flags,
17193 collations,
17194 collation_registry,
17195 ) == Ordering::Equal
17196 {
17197 return Ok(true);
17198 }
17199 if !cursor.cursor.next(&cursor.cx).await? {
17200 break;
17201 }
17202 }
17203
17204 Ok(false)
17205}
17206
17207async fn storage_cursor_exact_match_collated(
17208 cursor_id: i32,
17209 cursor: &mut StorageCursor,
17210 key_bytes: &[u8],
17211 desc_flags: &[bool],
17212 collations: &[Option<String>],
17213 collation_registry: &CollationRegistry,
17214) -> Result<bool> {
17215 if !try_decode_storage_cursor_target_index_record(cursor, key_bytes) {
17216 return Ok(false);
17217 }
17218 let key_len = cursor.target_vals_buf.len();
17219 if !cursor.cursor.first(&cursor.cx).await? {
17220 return Ok(false);
17221 }
17222
17223 loop {
17224 if !try_decode_storage_cursor_current_index_record(cursor_id, cursor).await? {
17225 return Ok(false);
17226 }
17227 if cursor.cur_vals_buf.len() == key_len
17228 && compare_index_prefix_keys(
17229 &cursor.cur_vals_buf,
17230 &cursor.target_vals_buf,
17231 key_len,
17232 desc_flags,
17233 collations,
17234 collation_registry,
17235 ) == Ordering::Equal
17236 {
17237 return Ok(true);
17238 }
17239 if !cursor.cursor.next(&cursor.cx).await? {
17240 break;
17241 }
17242 }
17243
17244 Ok(false)
17245}
17246
17247async fn storage_cursor_current_first_index_key_equals(
17248 cursor: &mut StorageCursor,
17249 probe_value: &SqliteValue,
17250 collect_vdbe_metrics: bool,
17251 malformed_detail: &'static str,
17252) -> Result<bool> {
17253 refresh_storage_cursor_first_key_state(cursor, collect_vdbe_metrics);
17254 storage_cursor_current_first_index_key_equals_at_current_row(
17255 cursor,
17256 probe_value,
17257 collect_vdbe_metrics,
17258 malformed_detail,
17259 )
17260 .await
17261}
17262
17263async fn storage_cursor_current_first_index_key_equals_at_current_row(
17264 cursor: &mut StorageCursor,
17265 probe_value: &SqliteValue,
17266 collect_vdbe_metrics: bool,
17267 malformed_detail: &'static str,
17268) -> Result<bool> {
17269 if cursor.row_decode.cached_value_ready(0) {
17270 note_decode_cache_hit(collect_vdbe_metrics);
17271 let cached = cursor
17272 .row_decode
17273 .cached_value(0)
17274 .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17275 if collect_vdbe_metrics {
17276 record_decoded_value_metrics(cached);
17277 }
17278 return Ok(storage_cursor_first_index_key_equals_probe(
17279 cursor,
17280 cached,
17281 probe_value,
17282 ));
17283 }
17284
17285 note_decode_cache_miss(collect_vdbe_metrics);
17286 if let SqliteValue::Integer(probe_int) = probe_value {
17287 match storage_cursor_probe_first_index_key_integer(cursor, *probe_int, malformed_detail)
17288 .await?
17289 {
17290 FirstIndexKeyIntegerProbe::Match => return Ok(true),
17291 FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17292 cursor
17293 .row_decode
17294 .cache_decoded(0, SqliteValue::Integer(current_value));
17295 return Ok(false);
17296 }
17297 FirstIndexKeyIntegerProbe::NeedsGenericCompare(fallback_col) => {
17298 return storage_cursor_current_first_index_key_equals_generic_fallback(
17299 cursor,
17300 probe_value,
17301 collect_vdbe_metrics,
17302 malformed_detail,
17303 fallback_col,
17304 );
17305 }
17306 }
17307 }
17308
17309 let col = storage_cursor_first_index_key_column_offset(cursor, malformed_detail).await?;
17310 storage_cursor_current_first_index_key_equals_generic_fallback(
17311 cursor,
17312 probe_value,
17313 collect_vdbe_metrics,
17314 malformed_detail,
17315 col,
17316 )
17317}
17318
17319async fn storage_cursor_current_first_index_key_compare(
17320 cursor: &mut StorageCursor,
17321 probe_value: &SqliteValue,
17322 collect_vdbe_metrics: bool,
17323 malformed_detail: &'static str,
17324 desc: bool,
17325 collation: Option<&str>,
17326 collation_registry: &CollationRegistry,
17327) -> Result<Ordering> {
17328 refresh_storage_cursor_first_key_state(cursor, collect_vdbe_metrics);
17329 storage_cursor_current_first_index_key_compare_at_current_row(
17330 cursor,
17331 probe_value,
17332 collect_vdbe_metrics,
17333 malformed_detail,
17334 desc,
17335 collation,
17336 collation_registry,
17337 )
17338 .await
17339}
17340
17341async fn storage_cursor_current_first_index_key_compare_at_current_row(
17342 cursor: &mut StorageCursor,
17343 probe_value: &SqliteValue,
17344 collect_vdbe_metrics: bool,
17345 malformed_detail: &'static str,
17346 desc: bool,
17347 collation: Option<&str>,
17348 collation_registry: &CollationRegistry,
17349) -> Result<Ordering> {
17350 let compare_ctx = FirstIndexKeyCompareContext {
17351 desc,
17352 collation,
17353 collation_registry,
17354 };
17355 if cursor.row_decode.cached_value_ready(0) {
17356 note_decode_cache_hit(collect_vdbe_metrics);
17357 let cached = cursor
17358 .row_decode
17359 .cached_value(0)
17360 .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17361 if collect_vdbe_metrics {
17362 record_decoded_value_metrics(cached);
17363 }
17364 return Ok(compare_first_index_key_values(
17365 cached,
17366 probe_value,
17367 compare_ctx,
17368 ));
17369 }
17370
17371 note_decode_cache_miss(collect_vdbe_metrics);
17372 if let SqliteValue::Integer(probe_int) = probe_value {
17373 match storage_cursor_probe_first_index_key_integer(cursor, *probe_int, malformed_detail)
17374 .await?
17375 {
17376 FirstIndexKeyIntegerProbe::Match => return Ok(Ordering::Equal),
17377 FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17378 let current_value = SqliteValue::Integer(current_value);
17379 let cmp = compare_first_index_key_values(¤t_value, probe_value, compare_ctx);
17380 cursor.row_decode.cache_decoded(0, current_value);
17381 return Ok(cmp);
17382 }
17383 FirstIndexKeyIntegerProbe::NeedsGenericCompare(fallback_col) => {
17384 return storage_cursor_current_first_index_key_compare_generic_fallback(
17385 cursor,
17386 probe_value,
17387 collect_vdbe_metrics,
17388 malformed_detail,
17389 fallback_col,
17390 compare_ctx,
17391 );
17392 }
17393 }
17394 }
17395
17396 let col = storage_cursor_first_index_key_column_offset(cursor, malformed_detail).await?;
17397 storage_cursor_current_first_index_key_compare_generic_fallback(
17398 cursor,
17399 probe_value,
17400 collect_vdbe_metrics,
17401 malformed_detail,
17402 col,
17403 compare_ctx,
17404 )
17405}
17406
17407fn refresh_storage_cursor_first_key_state(cursor: &mut StorageCursor, collect_vdbe_metrics: bool) {
17408 let position_stamp = cursor.cursor.position_stamp();
17409 if cursor.last_position_stamp == position_stamp {
17410 return;
17411 }
17412
17413 if let Some(previous_stamp) = cursor.last_position_stamp
17414 && let Some(current_stamp) = position_stamp
17415 {
17416 note_decode_cache_invalidation(
17417 collect_vdbe_metrics,
17418 decode_cache_invalidation_reason_from_stamps(previous_stamp, current_stamp),
17419 );
17420 } else if cursor.last_position_stamp.is_some() {
17421 note_decode_cache_invalidation(
17422 collect_vdbe_metrics,
17423 DecodeCacheInvalidationReason::PositionChange,
17424 );
17425 }
17426 cursor.row_decode.invalidate();
17427 cursor.payload_buf.clear();
17428 cursor.last_position_stamp = position_stamp;
17429 cursor.cached_rowid = None;
17430 cursor.payload_includes_rowid_alias = None;
17431}
17432
17433async fn storage_cursor_cached_rowid(cursor: &mut StorageCursor) -> Result<i64> {
17434 refresh_storage_cursor_first_key_state(cursor, false);
17435 if let Some(rowid) = cursor.cached_rowid {
17436 return Ok(rowid);
17437 }
17438 let rowid = cursor.cursor.rowid(&cursor.cx).await?;
17439 cursor.cached_rowid = Some(rowid);
17440 Ok(rowid)
17441}
17442
17443async fn storage_cursor_first_index_key_column_offset(
17444 cursor: &mut StorageCursor,
17445 malformed_detail: &'static str,
17446) -> Result<ColumnOffset> {
17447 const INITIAL_PREFIX_BYTES: usize = 16;
17448
17449 let mut requested_bytes = INITIAL_PREFIX_BYTES;
17450 loop {
17451 ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
17452 let loaded = cursor.payload_buf.len();
17453
17454 let (header_size_u64, hdr_varint_len) =
17455 read_varint(&cursor.payload_buf).ok_or_else(|| FrankenError::DatabaseCorrupt {
17456 detail: malformed_detail.to_owned(),
17457 })?;
17458 let header_size =
17459 usize::try_from(header_size_u64).map_err(|_| FrankenError::DatabaseCorrupt {
17460 detail: malformed_detail.to_owned(),
17461 })?;
17462 if header_size < hdr_varint_len {
17463 return Err(FrankenError::DatabaseCorrupt {
17464 detail: malformed_detail.to_owned(),
17465 });
17466 }
17467 if header_size > loaded {
17468 if loaded < requested_bytes {
17469 return Err(FrankenError::DatabaseCorrupt {
17470 detail: malformed_detail.to_owned(),
17471 });
17472 }
17473 requested_bytes = header_size.max(loaded.saturating_mul(2));
17474 continue;
17475 }
17476
17477 let (serial_type, _) = read_varint(&cursor.payload_buf[hdr_varint_len..header_size])
17478 .ok_or_else(|| FrankenError::DatabaseCorrupt {
17479 detail: malformed_detail.to_owned(),
17480 })?;
17481 let value_len = usize::try_from(serial_type_len(serial_type).ok_or_else(|| {
17482 FrankenError::DatabaseCorrupt {
17483 detail: malformed_detail.to_owned(),
17484 }
17485 })?)
17486 .map_err(|_| FrankenError::DatabaseCorrupt {
17487 detail: malformed_detail.to_owned(),
17488 })?;
17489 let body_offset = header_size;
17490 let col_end =
17491 body_offset
17492 .checked_add(value_len)
17493 .ok_or_else(|| FrankenError::DatabaseCorrupt {
17494 detail: malformed_detail.to_owned(),
17495 })?;
17496 if col_end > loaded {
17497 if loaded < requested_bytes {
17498 return Err(FrankenError::DatabaseCorrupt {
17499 detail: malformed_detail.to_owned(),
17500 });
17501 }
17502 requested_bytes = col_end.max(loaded.saturating_mul(2));
17503 continue;
17504 }
17505
17506 return Ok(ColumnOffset {
17507 serial_type,
17508 body_offset: u32::try_from(body_offset).map_err(|_| FrankenError::DatabaseCorrupt {
17509 detail: malformed_detail.to_owned(),
17510 })?,
17511 value_len: u32::try_from(value_len).map_err(|_| FrankenError::DatabaseCorrupt {
17512 detail: malformed_detail.to_owned(),
17513 })?,
17514 });
17515 }
17516}
17517
17518fn storage_cursor_current_first_index_key_equals_generic_fallback(
17519 cursor: &mut StorageCursor,
17520 probe_value: &SqliteValue,
17521 collect_vdbe_metrics: bool,
17522 malformed_detail: &'static str,
17523 col: ColumnOffset,
17524) -> Result<bool> {
17525 let hint = cursor.row_decode.cached_value(0);
17526 let value = fsqlite_types::record::decode_column_from_offset_reuse(
17527 &cursor.payload_buf,
17528 &col,
17529 hint,
17530 collect_vdbe_metrics,
17531 )
17532 .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17533 if collect_vdbe_metrics {
17534 record_decoded_value_metrics(&value);
17535 }
17536 let matches = storage_cursor_first_index_key_equals_probe(cursor, &value, probe_value);
17537 cursor.row_decode.cache_decoded(0, value);
17538 Ok(matches)
17539}
17540
17541fn storage_cursor_current_first_index_key_compare_generic_fallback(
17542 cursor: &mut StorageCursor,
17543 probe_value: &SqliteValue,
17544 collect_vdbe_metrics: bool,
17545 malformed_detail: &'static str,
17546 col: ColumnOffset,
17547 compare_ctx: FirstIndexKeyCompareContext<'_>,
17548) -> Result<Ordering> {
17549 let hint = cursor.row_decode.cached_value(0);
17550 let value = fsqlite_types::record::decode_column_from_offset_reuse(
17551 &cursor.payload_buf,
17552 &col,
17553 hint,
17554 collect_vdbe_metrics,
17555 )
17556 .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17557 if collect_vdbe_metrics {
17558 record_decoded_value_metrics(&value);
17559 }
17560 let cmp = compare_first_index_key_values(&value, probe_value, compare_ctx);
17561 cursor.row_decode.cache_decoded(0, value);
17562 Ok(cmp)
17563}
17564
17565fn storage_cursor_first_index_key_collation(cursor: &StorageCursor) -> Option<&str> {
17566 cursor
17567 .cursor
17568 .index_collations()
17569 .first()
17570 .and_then(|collation| collation.as_deref())
17571 .filter(|name| !name.eq_ignore_ascii_case("BINARY"))
17572}
17573
17574fn storage_cursor_first_index_key_equals_probe(
17575 cursor: &StorageCursor,
17576 current_value: &SqliteValue,
17577 probe_value: &SqliteValue,
17578) -> bool {
17579 let Some(collation) = storage_cursor_first_index_key_collation(cursor) else {
17580 return values_equal_without_collation(current_value, probe_value);
17581 };
17582
17583 if let (SqliteValue::Text(left), SqliteValue::Text(right)) = (current_value, probe_value)
17584 && let Some(ordering) = builtin_collation_compare_text(left, right, collation)
17585 {
17586 return ordering == Ordering::Equal;
17587 }
17588
17589 let registry = cursor.cursor.collation_registry();
17590 let guard = registry.lock().unwrap_or_else(|err| err.into_inner());
17591 cmp_values_collated(current_value, probe_value, Some(collation), &guard) == Ordering::Equal
17592}
17593
17594#[derive(Clone, Copy)]
17595struct FirstIndexKeyCompareContext<'a> {
17596 desc: bool,
17597 collation: Option<&'a str>,
17598 collation_registry: &'a CollationRegistry,
17599}
17600
17601fn compare_first_index_key_values(
17602 current_value: &SqliteValue,
17603 probe_value: &SqliteValue,
17604 compare_ctx: FirstIndexKeyCompareContext<'_>,
17605) -> Ordering {
17606 let mut cmp = cmp_values_collated(
17607 current_value,
17608 probe_value,
17609 compare_ctx.collation,
17610 compare_ctx.collation_registry,
17611 );
17612 if compare_ctx.desc {
17613 cmp = cmp.reverse();
17614 }
17615 cmp
17616}
17617
17618enum FirstIndexKeyIntegerProbe {
17619 Match,
17620 Mismatch(i64),
17621 NeedsGenericCompare(ColumnOffset),
17622}
17623
17624async fn storage_cursor_probe_first_index_key_integer(
17625 cursor: &mut StorageCursor,
17626 probe_value: i64,
17627 malformed_detail: &'static str,
17628) -> Result<FirstIndexKeyIntegerProbe> {
17629 use fsqlite_types::serial_type::{SerialTypeClass, classify_serial_type};
17630
17631 if let Some((matches, current_value)) = cursor
17632 .cursor
17633 .try_probe_current_first_index_key_integer_local(probe_value)?
17634 {
17635 return Ok(if matches {
17636 FirstIndexKeyIntegerProbe::Match
17637 } else {
17638 FirstIndexKeyIntegerProbe::Mismatch(current_value)
17639 });
17640 }
17641
17642 const INITIAL_PREFIX_BYTES: usize = 16;
17643
17644 let mut requested_bytes = INITIAL_PREFIX_BYTES;
17645 loop {
17646 ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
17647 let loaded = cursor.payload_buf.len();
17648
17649 let (header_size_u64, hdr_varint_len) =
17650 read_varint(&cursor.payload_buf).ok_or_else(|| FrankenError::DatabaseCorrupt {
17651 detail: malformed_detail.to_owned(),
17652 })?;
17653 let header_size =
17654 usize::try_from(header_size_u64).map_err(|_| FrankenError::DatabaseCorrupt {
17655 detail: malformed_detail.to_owned(),
17656 })?;
17657 if header_size < hdr_varint_len {
17658 return Err(FrankenError::DatabaseCorrupt {
17659 detail: malformed_detail.to_owned(),
17660 });
17661 }
17662 if header_size > loaded {
17663 if loaded < requested_bytes {
17664 return Err(FrankenError::DatabaseCorrupt {
17665 detail: malformed_detail.to_owned(),
17666 });
17667 }
17668 requested_bytes = header_size.max(loaded.saturating_mul(2));
17669 continue;
17670 }
17671
17672 let (serial_type, _) = read_varint(&cursor.payload_buf[hdr_varint_len..header_size])
17673 .ok_or_else(|| FrankenError::DatabaseCorrupt {
17674 detail: malformed_detail.to_owned(),
17675 })?;
17676 let value_len = usize::try_from(serial_type_len(serial_type).ok_or_else(|| {
17677 FrankenError::DatabaseCorrupt {
17678 detail: malformed_detail.to_owned(),
17679 }
17680 })?)
17681 .map_err(|_| FrankenError::DatabaseCorrupt {
17682 detail: malformed_detail.to_owned(),
17683 })?;
17684 let body_offset = header_size;
17685 let col_end =
17686 body_offset
17687 .checked_add(value_len)
17688 .ok_or_else(|| FrankenError::DatabaseCorrupt {
17689 detail: malformed_detail.to_owned(),
17690 })?;
17691 if col_end > loaded {
17692 if loaded < requested_bytes {
17693 return Err(FrankenError::DatabaseCorrupt {
17694 detail: malformed_detail.to_owned(),
17695 });
17696 }
17697 requested_bytes = col_end.max(loaded.saturating_mul(2));
17698 continue;
17699 }
17700
17701 let mismatch = |current_value| FirstIndexKeyIntegerProbe::Mismatch(current_value);
17702 return Ok(match classify_serial_type(serial_type) {
17703 SerialTypeClass::Zero => {
17704 if probe_value == 0 {
17705 FirstIndexKeyIntegerProbe::Match
17706 } else {
17707 mismatch(0)
17708 }
17709 }
17710 SerialTypeClass::One => {
17711 if probe_value == 1 {
17712 FirstIndexKeyIntegerProbe::Match
17713 } else {
17714 mismatch(1)
17715 }
17716 }
17717 SerialTypeClass::Integer => {
17718 let bytes = cursor
17719 .payload_buf
17720 .get(body_offset..col_end)
17721 .ok_or_else(|| FrankenError::internal(malformed_detail))?;
17722 let current_value = decode_big_endian_signed_fast(bytes);
17723 if current_value == probe_value {
17724 FirstIndexKeyIntegerProbe::Match
17725 } else {
17726 mismatch(current_value)
17727 }
17728 }
17729 _ => FirstIndexKeyIntegerProbe::NeedsGenericCompare(ColumnOffset {
17730 serial_type,
17731 body_offset: u32::try_from(body_offset).map_err(|_| {
17732 FrankenError::DatabaseCorrupt {
17733 detail: malformed_detail.to_owned(),
17734 }
17735 })?,
17736 value_len: u32::try_from(value_len).map_err(|_| FrankenError::DatabaseCorrupt {
17737 detail: malformed_detail.to_owned(),
17738 })?,
17739 }),
17740 });
17741 }
17742}
17743
17744#[allow(clippy::cast_possible_wrap)]
17745fn decode_big_endian_signed_fast(bytes: &[u8]) -> i64 {
17746 match bytes.len() {
17747 0 => 0,
17748 1 => bytes[0] as i8 as i64,
17749 2 => {
17750 let mut buf = [0_u8; 2];
17751 buf.copy_from_slice(bytes);
17752 i16::from_be_bytes(buf) as i64
17753 }
17754 3 => {
17755 let mut buf = [if bytes[0] & 0x80 != 0 { 0xFF } else { 0 }; 4];
17756 buf[1..4].copy_from_slice(bytes);
17757 i32::from_be_bytes(buf) as i64
17758 }
17759 4 => {
17760 let mut buf = [0_u8; 4];
17761 buf.copy_from_slice(bytes);
17762 i32::from_be_bytes(buf) as i64
17763 }
17764 6 => {
17765 let mut buf = [if bytes[0] & 0x80 != 0 { 0xFF } else { 0 }; 8];
17766 buf[2..8].copy_from_slice(bytes);
17767 i64::from_be_bytes(buf)
17768 }
17769 8 => {
17770 let mut buf = [0_u8; 8];
17771 buf.copy_from_slice(bytes);
17772 i64::from_be_bytes(buf)
17773 }
17774 _ => {
17775 let negative = bytes.first().is_some_and(|&b| b & 0x80 != 0);
17776 let mut value: u64 = if negative { u64::MAX } else { 0 };
17777 for &b in bytes {
17778 value = (value << 8) | u64::from(b);
17779 }
17780 value as i64
17781 }
17782 }
17783}
17784
17785async fn storage_cursor_count_equal_first_key_run(
17786 cursor: &mut StorageCursor,
17787 probe_value: &SqliteValue,
17788 collect_vdbe_metrics: bool,
17789) -> Result<i64> {
17790 if let SqliteValue::Integer(probe_int) = probe_value {
17791 return storage_cursor_count_equal_first_key_run_integer_probe(
17792 cursor,
17793 *probe_int,
17794 collect_vdbe_metrics,
17795 )
17796 .await;
17797 }
17798
17799 let mut matched = 0_i64;
17800 while !cursor.cursor.eof() {
17801 if !storage_cursor_current_first_index_key_equals(
17802 cursor,
17803 probe_value,
17804 collect_vdbe_metrics,
17805 "CountIndexEqRun: malformed index entry record",
17806 )
17807 .await?
17808 {
17809 break;
17810 }
17811 matched = matched.wrapping_add(1);
17812 if !cursor.cursor.next(&cursor.cx).await? {
17813 break;
17814 }
17815 }
17816 Ok(matched)
17817}
17818
17819async fn storage_cursor_count_equal_first_key_run_integer_probe(
17820 cursor: &mut StorageCursor,
17821 probe_int: i64,
17822 collect_vdbe_metrics: bool,
17823) -> Result<i64> {
17824 let mut matched = 0_i64;
17825 let probe_value = SqliteValue::Integer(probe_int);
17826 let mut payload_position_stamp = None;
17827 while !cursor.cursor.eof() {
17828 match cursor
17829 .cursor
17830 .count_equal_first_index_key_run_integer_local_segment(&cursor.cx, probe_int)
17831 .await?
17832 {
17833 FirstIndexKeyIntegerLocalRunSegment::Matched(local_matched) => {
17834 matched = matched.wrapping_add(local_matched);
17835 continue;
17836 }
17837 FirstIndexKeyIntegerLocalRunSegment::Mismatch {
17838 matched: local_matched,
17839 current_value,
17840 } => {
17841 matched = matched.wrapping_add(local_matched);
17842 cursor.row_decode.invalidate();
17843 cursor
17844 .row_decode
17845 .cache_decoded(0, SqliteValue::Integer(current_value));
17846 break;
17847 }
17848 FirstIndexKeyIntegerLocalRunSegment::NeedsFallback {
17849 matched: local_matched,
17850 } => {
17851 matched = matched.wrapping_add(local_matched);
17852 }
17853 }
17854
17855 if cursor.cursor.eof() {
17856 break;
17857 }
17858
17859 let position_stamp = cursor.cursor.position_stamp();
17860 if payload_position_stamp != position_stamp {
17861 cursor.payload_buf.clear();
17862 payload_position_stamp = position_stamp;
17863 }
17864 match storage_cursor_probe_first_index_key_integer(
17865 cursor,
17866 probe_int,
17867 "CountIndexEqRun: malformed index entry record",
17868 )
17869 .await?
17870 {
17871 FirstIndexKeyIntegerProbe::Match => {}
17872 FirstIndexKeyIntegerProbe::Mismatch(current_value) => {
17873 cursor.row_decode.invalidate();
17874 cursor
17875 .row_decode
17876 .cache_decoded(0, SqliteValue::Integer(current_value));
17877 break;
17878 }
17879 FirstIndexKeyIntegerProbe::NeedsGenericCompare(col) => {
17880 cursor.row_decode.invalidate();
17881 if !storage_cursor_current_first_index_key_equals_generic_fallback(
17882 cursor,
17883 &probe_value,
17884 collect_vdbe_metrics,
17885 "CountIndexEqRun: malformed index entry record",
17886 col,
17887 )? {
17888 break;
17889 }
17890 }
17891 }
17892
17893 matched = matched.wrapping_add(1);
17894 if !cursor.cursor.next(&cursor.cx).await? {
17895 break;
17896 }
17897 }
17898 Ok(matched)
17899}
17900
17901async fn find_conflicting_rowid_in_index(
17902 sc: &mut StorageCursor,
17903 key_bytes: &[u8],
17904 n_idx_cols: usize,
17905) -> Result<Option<i64>> {
17906 sc.cursor.index_move_to(&sc.cx, key_bytes).await?;
17908
17909 sc.cur_vals_buf.clear();
17910
17911 decode_storage_cursor_target_index_record_strict(
17913 sc,
17914 key_bytes,
17915 "find_conflicting_rowid: malformed new index key",
17916 )?;
17917
17918 for attempt in 0..2 {
17920 if sc.cursor.eof() {
17921 if attempt == 0 {
17922 sc.cursor.prev(&sc.cx).await?;
17924 continue;
17925 }
17926 break;
17927 }
17928
17929 decode_storage_cursor_current_index_record_strict(
17930 -1,
17931 sc,
17932 "find_conflicting_rowid: malformed index entry record",
17933 )
17934 .await?;
17935
17936 let mut prefix_match = true;
17939 let mut has_null = false;
17940 for i in 0..n_idx_cols {
17941 let new_val = sc.target_vals_buf.get(i);
17942 let entry_val = sc.cur_vals_buf.get(i);
17943 if matches!(new_val, Some(SqliteValue::Null) | None)
17944 || matches!(entry_val, Some(SqliteValue::Null) | None)
17945 {
17946 has_null = true;
17947 break;
17948 }
17949 if new_val != entry_val {
17950 prefix_match = false;
17951 break;
17952 }
17953 }
17954
17955 if prefix_match && !has_null {
17956 let old_rowid = index_entry_rowid_at(
17957 &sc.cur_vals_buf,
17958 n_idx_cols,
17959 "find_conflicting_rowid: index entry must end with exactly one integer rowid suffix",
17960 )?;
17961
17962 sc.cursor.delete(&sc.cx).await?;
17964 invalidate_storage_cursor_row_cache(sc);
17965
17966 return Ok(Some(old_rowid));
17967 }
17968
17969 if attempt == 0 {
17970 sc.cursor.prev(&sc.cx).await?;
17971 }
17972 }
17973
17974 Ok(None)
17975}
17976
17977async fn find_conflicting_rowid_in_index_collated(
17978 sc: &mut StorageCursor,
17979 key_bytes: &[u8],
17980 n_idx_cols: usize,
17981 desc_flags: &[bool],
17982 collations: &[Option<String>],
17983 collation_registry: &CollationRegistry,
17984) -> Result<Option<i64>> {
17985 let target_values = parse_record(key_bytes).ok_or_else(|| {
17986 FrankenError::internal("find_conflicting_rowid_in_index_collated: malformed new index key")
17987 })?;
17988 if target_values.len() < n_idx_cols
17989 || target_values
17990 .iter()
17991 .take(n_idx_cols)
17992 .any(SqliteValue::is_null)
17993 {
17994 return Ok(None);
17995 }
17996
17997 if !sc.cursor.first(&sc.cx).await? {
17998 return Ok(None);
17999 }
18000
18001 loop {
18002 let existing_key = sc.cursor.payload(&sc.cx).await?;
18003 let existing_values = parse_record(&existing_key).ok_or_else(|| {
18004 FrankenError::internal(
18005 "find_conflicting_rowid_in_index_collated: malformed index entry record",
18006 )
18007 })?;
18008 if existing_values.len() >= n_idx_cols
18009 && existing_values
18010 .iter()
18011 .take(n_idx_cols)
18012 .all(|value| !value.is_null())
18013 && compare_index_prefix_keys(
18014 &existing_values,
18015 &target_values,
18016 n_idx_cols,
18017 desc_flags,
18018 collations,
18019 collation_registry,
18020 ) == Ordering::Equal
18021 {
18022 let rowid = index_entry_rowid_at(
18023 &existing_values,
18024 n_idx_cols,
18025 "find_conflicting_rowid_in_index_collated: index entry must end with exactly one integer rowid suffix",
18026 )?;
18027 return Ok(Some(rowid));
18028 }
18029
18030 if !sc.cursor.next(&sc.cx).await? {
18031 break;
18032 }
18033 }
18034
18035 Ok(None)
18036}
18037
18038fn index_entry_rowid_at(
18039 values: &[SqliteValue],
18040 n_idx_cols: usize,
18041 corrupt_detail: &str,
18042) -> Result<i64> {
18043 let expected_len = n_idx_cols
18044 .checked_add(1)
18045 .ok_or_else(|| FrankenError::DatabaseCorrupt {
18046 detail: corrupt_detail.to_owned(),
18047 })?;
18048 if values.len() != expected_len {
18049 return Err(FrankenError::DatabaseCorrupt {
18050 detail: corrupt_detail.to_owned(),
18051 });
18052 }
18053 values[n_idx_cols]
18054 .as_integer()
18055 .ok_or_else(|| FrankenError::DatabaseCorrupt {
18056 detail: corrupt_detail.to_owned(),
18057 })
18058}
18059
18060fn encode_record(values: &[SqliteValue]) -> Vec<u8> {
18061 serialize_record(values)
18062}
18063
18064#[allow(dead_code)]
18065fn payload_includes_rowid_alias(
18066 payload_values: &[SqliteValue],
18067 _rowid: i64,
18068 ipk_col_idx: usize,
18069 table_column_count: Option<usize>,
18070 first_not_null_non_ipk_col_idx: Option<usize>,
18071) -> bool {
18072 let payload_cols = payload_values.len();
18073 if let Some(table_cols) = table_column_count {
18074 if payload_cols == table_cols {
18075 return true;
18076 }
18077 let one_column_short = payload_cols.checked_add(1) == Some(table_cols);
18078 if one_column_short && matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null)) {
18079 return true;
18080 }
18081 if let Some(not_null_col_idx) =
18082 first_not_null_non_ipk_col_idx.filter(|idx| *idx > ipk_col_idx)
18083 {
18084 let omitted_payload_idx = not_null_col_idx - 1;
18085 let present_payload_idx = not_null_col_idx;
18086 if omitted_payload_idx < payload_cols
18087 && present_payload_idx < payload_cols
18088 && matches!(
18089 payload_values.get(omitted_payload_idx),
18090 Some(SqliteValue::Null)
18091 )
18092 {
18093 return true;
18094 }
18095 }
18096 if matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null)) {
18099 return true;
18100 }
18101 return false;
18102 }
18103 if payload_cols <= ipk_col_idx {
18104 return false;
18105 }
18106
18107 matches!(payload_values.get(ipk_col_idx), Some(SqliteValue::Null))
18108}
18109
18110#[allow(dead_code)]
18111fn payload_includes_rowid_alias_lazy(
18112 row_decode: &RowDecodeScratch,
18113 _record: &[u8],
18114 _rowid: i64,
18115 ipk_col_idx: usize,
18116 table_column_count: Option<usize>,
18117 first_not_null_non_ipk_col_idx: Option<usize>,
18118) -> bool {
18119 payload_includes_rowid_alias_without_rowid(
18120 row_decode,
18121 ipk_col_idx,
18122 table_column_count,
18123 first_not_null_non_ipk_col_idx,
18124 )
18125}
18126
18127fn payload_includes_rowid_alias_without_rowid(
18133 row_decode: &RowDecodeScratch,
18134 ipk_col_idx: usize,
18135 table_column_count: Option<usize>,
18136 first_not_null_non_ipk_col_idx: Option<usize>,
18137) -> bool {
18138 use fsqlite_types::serial_type::{SerialTypeClass, classify_serial_type};
18139
18140 let payload_cols = row_decode.column_count();
18141 if let Some(table_cols) = table_column_count {
18142 if payload_cols == table_cols {
18143 return true;
18144 }
18145 let one_column_short = payload_cols.checked_add(1) == Some(table_cols);
18146 if let Some(not_null_col_idx) =
18147 first_not_null_non_ipk_col_idx.filter(|idx| *idx > ipk_col_idx)
18148 {
18149 let omitted_payload_idx = not_null_col_idx - 1;
18150 let present_payload_idx = not_null_col_idx;
18151 if omitted_payload_idx < payload_cols
18152 && present_payload_idx < payload_cols
18153 && let Some(col) = row_decode.column_offset(omitted_payload_idx)
18154 && matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null)
18155 {
18156 return true;
18157 }
18158 }
18159 if one_column_short {
18160 let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18161 return false;
18162 };
18163 return matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null);
18164 }
18165 if ipk_col_idx >= payload_cols {
18166 return false;
18167 }
18168 let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18169 return false;
18170 };
18171 return matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null);
18172 }
18173 if ipk_col_idx >= payload_cols {
18174 return false;
18175 }
18176 let Some(col) = row_decode.column_offset(ipk_col_idx) else {
18177 return false;
18178 };
18179 matches!(classify_serial_type(col.serial_type), SerialTypeClass::Null)
18180}
18181
18182fn column_payload_end(col: &ColumnOffset) -> Option<usize> {
18183 let start = usize::try_from(col.body_offset).ok()?;
18184 let len = usize::try_from(col.value_len).ok()?;
18185 start.checked_add(len)
18186}
18187
18188fn row_decode_payload_end(row_decode: &RowDecodeScratch) -> Option<usize> {
18189 let mut end = 0_usize;
18190 for idx in 0..row_decode.column_count() {
18191 end = end.max(column_payload_end(row_decode.column_offset(idx)?)?);
18192 }
18193 Some(end)
18194}
18195
18196#[derive(Debug, Clone, Copy, Default)]
18197struct DecodeCacheRefreshState {
18198 refreshed: bool,
18199 eager_values_ready: bool,
18200}
18201
18202fn decode_cache_invalidation_reason_from_stamps(
18203 previous: CursorPositionStamp,
18204 current: CursorPositionStamp,
18205) -> DecodeCacheInvalidationReason {
18206 if previous.same_logical_position(current) {
18207 DecodeCacheInvalidationReason::WriteMutation
18208 } else {
18209 DecodeCacheInvalidationReason::PositionChange
18210 }
18211}
18212
18213async fn ensure_storage_cursor_payload_prefix(
18214 cursor: &mut StorageCursor,
18215 min_payload_bytes: usize,
18216) -> Result<()> {
18217 if cursor.payload_buf.len() >= min_payload_bytes {
18218 return Ok(());
18219 }
18220 cursor
18221 .cursor
18222 .payload_prefix_into(&cursor.cx, min_payload_bytes, &mut cursor.payload_buf)
18223 .await
18224}
18225
18226async fn ensure_storage_cursor_row_layout(
18227 cursor: &mut StorageCursor,
18228 min_payload_bytes: usize,
18229 collect_vdbe_metrics: bool,
18230) -> Result<DecodeCacheRefreshState> {
18231 let position_stamp = cursor.cursor.position_stamp();
18232 let mut refreshed = false;
18233 if cursor.last_position_stamp != position_stamp {
18234 if let Some(previous_stamp) = cursor.last_position_stamp
18235 && let Some(current_stamp) = position_stamp
18236 {
18237 note_decode_cache_invalidation(
18238 collect_vdbe_metrics,
18239 decode_cache_invalidation_reason_from_stamps(previous_stamp, current_stamp),
18240 );
18241 } else if cursor.last_position_stamp.is_some() {
18242 note_decode_cache_invalidation(
18243 collect_vdbe_metrics,
18244 DecodeCacheInvalidationReason::PositionChange,
18245 );
18246 }
18247 cursor.row_decode.invalidate();
18248 cursor.payload_buf.clear();
18249 cursor.last_position_stamp = position_stamp;
18250 cursor.cached_rowid = None;
18251 cursor.payload_includes_rowid_alias = None;
18252 refreshed = true;
18253 }
18254
18255 if !refreshed && !cursor.row_decode.is_empty() && cursor.payload_buf.len() >= min_payload_bytes
18256 {
18257 return Ok(DecodeCacheRefreshState {
18258 refreshed: false,
18259 eager_values_ready: false,
18260 });
18261 }
18262
18263 let required_bytes = min_payload_bytes;
18264 let mut requested_bytes = min_payload_bytes.max(STORAGE_CURSOR_LAYOUT_PREFIX_BYTES);
18265
18266 loop {
18267 ensure_storage_cursor_payload_prefix(cursor, requested_bytes).await?;
18268
18269 if cursor.row_decode.is_empty() {
18270 if cursor
18271 .row_decode
18272 .prepare_for_record_prefix(&cursor.payload_buf)
18273 .is_none()
18274 {
18275 let loaded = cursor.payload_buf.len();
18276 if loaded < requested_bytes {
18277 return Err(FrankenError::DatabaseCorrupt {
18278 detail: "malformed record header or payload in cursor payload".to_owned(),
18279 });
18280 }
18281 requested_bytes = requested_bytes
18282 .saturating_mul(2)
18283 .max(loaded.saturating_add(1));
18284 continue;
18285 }
18286 }
18287
18288 if cursor.payload_buf.len() < required_bytes {
18289 let loaded = cursor.payload_buf.len();
18290 if loaded < requested_bytes {
18291 return Err(FrankenError::DatabaseCorrupt {
18292 detail: format!(
18293 "cursor payload shorter than required prefix: need {required_bytes}, have {loaded}"
18294 ),
18295 });
18296 }
18297 requested_bytes = required_bytes.max(loaded.saturating_mul(2));
18298 continue;
18299 }
18300
18301 let eager_values_ready = if cursor.row_decode.column_count() > 64
18302 && !cursor.row_decode.cached_value_ready(64)
18303 && row_decode_payload_end(&cursor.row_decode)
18304 .is_some_and(|record_len| cursor.payload_buf.len() >= record_len)
18305 {
18306 cursor
18307 .row_decode
18308 .prepare_for_record(&cursor.payload_buf)
18309 .ok_or_else(|| FrankenError::DatabaseCorrupt {
18310 detail: "malformed wide record in cursor payload".to_owned(),
18311 })?
18312 } else {
18313 false
18314 };
18315
18316 return Ok(DecodeCacheRefreshState {
18317 refreshed,
18318 eager_values_ready,
18319 });
18320 }
18321}
18322
18323fn ensure_sorter_row_cache(
18324 sorter: &mut SorterCursor,
18325 collect_vdbe_metrics: bool,
18326 position: usize,
18327) -> Result<DecodeCacheRefreshState> {
18328 let (rows, cached_row_position, cached_row_decode) = (
18329 &sorter.rows,
18330 &mut sorter.cached_row_position,
18331 &mut sorter.cached_row_decode,
18332 );
18333 if *cached_row_position == Some(position) {
18334 return Ok(DecodeCacheRefreshState::default());
18335 }
18336 let row = rows
18337 .get(position)
18338 .ok_or_else(|| FrankenError::DatabaseCorrupt {
18339 detail: format!("missing sorter row at position {position}"),
18340 })?;
18341 if cached_row_position.is_some() {
18342 note_decode_cache_invalidation(
18343 collect_vdbe_metrics,
18344 DecodeCacheInvalidationReason::PositionChange,
18345 );
18346 }
18347 let eager_values_ready = cached_row_decode
18348 .prepare_for_record(&row.blob)
18349 .ok_or_else(|| FrankenError::DatabaseCorrupt {
18350 detail: "malformed sorter record header or payload".to_owned(),
18351 })?;
18352 *cached_row_position = Some(position);
18353 Ok(DecodeCacheRefreshState {
18354 refreshed: true,
18355 eager_values_ready,
18356 })
18357}
18358
18359fn invalidate_storage_cursor_row_cache_with_reason(
18360 cursor: &mut StorageCursor,
18361 collect_vdbe_metrics: bool,
18362 reason: DecodeCacheInvalidationReason,
18363) {
18364 if cursor.last_position_stamp.is_some() || !cursor.row_decode.is_empty() {
18365 note_decode_cache_invalidation(collect_vdbe_metrics, reason);
18366 }
18367 cursor.row_decode.invalidate();
18368 cursor.last_position_stamp = None;
18369 cursor.cached_rowid = None;
18370 cursor.payload_includes_rowid_alias = None;
18371}
18372
18373fn invalidate_storage_cursor_row_cache(cursor: &mut StorageCursor) {
18374 invalidate_storage_cursor_row_cache_with_reason(
18375 cursor,
18376 false,
18377 DecodeCacheInvalidationReason::WriteMutation,
18378 );
18379}
18380
18381#[allow(dead_code)]
18382fn encode_record_refs(values: &[&SqliteValue]) -> Vec<u8> {
18383 fsqlite_types::record::serialize_record_refs(values)
18384}
18385
18386fn record_blob_bytes(val: &SqliteValue) -> &[u8] {
18388 match val {
18389 SqliteValue::Blob(bytes) => bytes,
18390 _ => &[],
18391 }
18392}
18393
18394fn decode_record_with_metrics(
18395 val: &SqliteValue,
18396 collect_vdbe_metrics: bool,
18397) -> Result<Vec<SqliteValue>> {
18398 let SqliteValue::Blob(bytes) = val else {
18399 return Ok(Vec::new());
18400 };
18401 decode_record_bytes_with_metrics(bytes, collect_vdbe_metrics)
18402}
18403
18404fn decode_record_bytes_with_metrics(
18405 bytes: &[u8],
18406 collect_vdbe_metrics: bool,
18407) -> Result<Vec<SqliteValue>> {
18408 let _profile_stage = enter_vdbe_decode_profile_stage();
18409 let values = parse_record(bytes)
18410 .ok_or_else(|| FrankenError::internal("malformed SQLite record blob"))?;
18411 if collect_vdbe_metrics {
18412 FSQLITE_VDBE_RECORD_DECODE_CALLS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
18413 for value in &values {
18414 record_decoded_value_metrics(value);
18415 }
18416 }
18417 Ok(values)
18418}
18419
18420#[cfg(test)]
18421fn decode_record(val: &SqliteValue) -> Result<Vec<SqliteValue>> {
18422 decode_record_with_metrics(val, vdbe_metrics_enabled())
18423}
18424
18425fn sorter_keys_equal(
18426 lhs: &[SqliteValue],
18427 rhs: &[SqliteValue],
18428 key_columns: usize,
18429 collations: &[Option<String>],
18430 collation_registry: &CollationRegistry,
18431) -> bool {
18432 compare_sorter_keys(lhs, rhs, key_columns, collations, collation_registry) == Ordering::Equal
18433}
18434
18435fn compare_sorter_keys(
18436 lhs: &[SqliteValue],
18437 rhs: &[SqliteValue],
18438 key_columns: usize,
18439 collations: &[Option<String>],
18440 collation_registry: &CollationRegistry,
18441) -> Ordering {
18442 let key_count = key_columns.max(1);
18443 for idx in 0..key_count {
18444 let Some(lhs_value) = lhs.get(idx) else {
18445 return if rhs.get(idx).is_some() {
18446 Ordering::Less
18447 } else {
18448 break;
18449 };
18450 };
18451 let Some(rhs_value) = rhs.get(idx) else {
18452 return Ordering::Greater;
18453 };
18454
18455 let coll = collations.get(idx).and_then(|c| c.as_deref());
18456 match cmp_values_collated(lhs_value, rhs_value, coll, collation_registry) {
18457 Ordering::Equal => {}
18458 non_equal => return non_equal,
18459 }
18460 }
18461 Ordering::Equal
18462}
18463
18464fn parse_non_null_index_prefix(key_bytes: &[u8], key_columns: usize) -> Option<Vec<SqliteValue>> {
18465 let fields = parse_record(key_bytes)?;
18466 if fields.len() < key_columns || fields.iter().take(key_columns).any(SqliteValue::is_null) {
18467 return None;
18468 }
18469 Some(fields.into_iter().take(key_columns).collect())
18470}
18471
18472fn compare_index_prefix_keys(
18473 lhs: &[SqliteValue],
18474 rhs: &[SqliteValue],
18475 key_columns: usize,
18476 desc_flags: &[bool],
18477 collations: &[Option<String>],
18478 collation_registry: &CollationRegistry,
18479) -> Ordering {
18480 let key_count = key_columns.max(1);
18481 for idx in 0..key_count {
18482 let Some(lhs_value) = lhs.get(idx) else {
18483 return if rhs.get(idx).is_some() {
18484 Ordering::Less
18485 } else {
18486 break;
18487 };
18488 };
18489 let Some(rhs_value) = rhs.get(idx) else {
18490 return Ordering::Greater;
18491 };
18492
18493 let coll = collations.get(idx).and_then(|c| c.as_deref());
18494 let mut ord = cmp_values_collated(lhs_value, rhs_value, coll, collation_registry);
18495 if desc_flags.get(idx).copied().unwrap_or(false) {
18496 ord = ord.reverse();
18497 }
18498 if ord != Ordering::Equal {
18499 return ord;
18500 }
18501 }
18502 Ordering::Equal
18503}
18504
18505fn idx_compare_condition_met(opcode: Opcode, cmp: Ordering) -> bool {
18506 match opcode {
18507 Opcode::IdxLE => cmp != Ordering::Greater,
18508 Opcode::IdxGT => cmp == Ordering::Greater,
18509 Opcode::IdxLT => cmp == Ordering::Less,
18510 Opcode::IdxGE => cmp != Ordering::Less,
18511 _ => unreachable!("idx_compare_condition_met called with non-Idx comparison opcode"),
18512 }
18513}
18514
18515fn cmp_values_collated(
18520 lhs: &SqliteValue,
18521 rhs: &SqliteValue,
18522 collation: Option<&str>,
18523 collation_registry: &CollationRegistry,
18524) -> Ordering {
18525 if let (Some(coll), SqliteValue::Text(lt), SqliteValue::Text(rt)) = (collation, lhs, rhs) {
18526 return compare_text_with_collation(
18527 lt.as_bytes_direct(),
18528 rt.as_bytes_direct(),
18529 coll,
18530 collation_registry,
18531 );
18532 }
18533 lhs.partial_cmp(rhs).unwrap_or(Ordering::Equal)
18534}
18535
18536fn compare_sorter_rows(
18537 lhs: &[SqliteValue],
18538 rhs: &[SqliteValue],
18539 key_columns: usize,
18540 sort_key_orders: &[SortKeyOrder],
18541 collations: &[Option<String>],
18542 collation_registry: &CollationRegistry,
18543) -> Ordering {
18544 let key_count = key_columns.max(1);
18545 for idx in 0..key_count {
18546 let order = sort_key_orders
18547 .get(idx)
18548 .copied()
18549 .unwrap_or(SortKeyOrder::Asc);
18550 let is_desc = matches!(order, SortKeyOrder::Desc | SortKeyOrder::DescNullsFirst);
18551 let nulls_last = matches!(order, SortKeyOrder::Desc | SortKeyOrder::AscNullsLast);
18552 let Some(lhs_value) = lhs.get(idx) else {
18553 return if rhs.get(idx).is_some() {
18554 if is_desc {
18555 Ordering::Greater
18556 } else {
18557 Ordering::Less
18558 }
18559 } else {
18560 break;
18561 };
18562 };
18563 let Some(rhs_value) = rhs.get(idx) else {
18564 return if is_desc {
18565 Ordering::Less
18566 } else {
18567 Ordering::Greater
18568 };
18569 };
18570
18571 let l_null = lhs_value.is_null();
18573 let r_null = rhs_value.is_null();
18574 if l_null || r_null {
18575 if l_null && r_null {
18576 continue;
18577 }
18578 return if l_null == nulls_last {
18583 Ordering::Greater
18584 } else {
18585 Ordering::Less
18586 };
18587 }
18588
18589 let coll = collations.get(idx).and_then(|c| c.as_deref());
18590 let mut ord = cmp_values_collated(lhs_value, rhs_value, coll, collation_registry);
18591 if ord == Ordering::Equal {
18592 continue;
18593 }
18594
18595 if is_desc {
18596 ord = ord.reverse();
18597 }
18598 return ord;
18599 }
18600
18601 Ordering::Equal
18604}
18605
18606fn opcode_trace_enabled() -> bool {
18607 let env_enabled = std::env::var(VDBE_TRACE_ENV).is_ok_and(|value| {
18608 let normalized = value.trim().to_ascii_lowercase();
18609 !normalized.is_empty() && normalized != "0" && normalized != "false" && normalized != "off"
18610 });
18611 env_enabled || cfg!(test)
18612}
18613
18614fn both_integer_numeric_type(a: &SqliteValue, b: &SqliteValue) -> bool {
18620 a.is_integer_numeric_type() && b.is_integer_numeric_type()
18621}
18622
18623#[allow(clippy::cast_precision_loss)]
18625fn sql_div(dividend: &SqliteValue, divisor: &SqliteValue) -> SqliteValue {
18626 if dividend.is_null() || divisor.is_null() {
18627 return SqliteValue::Null;
18628 }
18629 let both_int = both_integer_numeric_type(dividend, divisor);
18632 if both_int {
18633 let a = dividend.to_integer();
18634 let b = divisor.to_integer();
18635 if b == 0 {
18636 SqliteValue::Null
18637 } else {
18638 match a.checked_div(b) {
18639 Some(result) => SqliteValue::Integer(result),
18640 #[allow(clippy::cast_precision_loss)]
18643 None => {
18644 let result = a as f64 / b as f64;
18645 if result.is_nan() {
18646 SqliteValue::Null
18647 } else {
18648 SqliteValue::Float(result)
18649 }
18650 }
18651 }
18652 }
18653 } else {
18654 let b = divisor.to_float();
18655 if b == 0.0 {
18656 SqliteValue::Null
18657 } else {
18658 let result = dividend.to_float() / b;
18659 if result.is_nan() {
18660 SqliteValue::Null
18661 } else {
18662 SqliteValue::Float(result)
18663 }
18664 }
18665 }
18666}
18667
18668#[allow(clippy::cast_precision_loss)]
18676fn sql_rem(dividend: &SqliteValue, divisor: &SqliteValue) -> SqliteValue {
18677 if dividend.is_null() || divisor.is_null() {
18678 return SqliteValue::Null;
18679 }
18680 let both_int = both_integer_numeric_type(dividend, divisor);
18681 let a = dividend.to_integer();
18682 let b = divisor.to_integer();
18683 if b == 0 {
18684 return SqliteValue::Null;
18685 }
18686 let result = a.checked_rem(b).unwrap_or_default();
18688 if both_int {
18689 SqliteValue::Integer(result)
18690 } else {
18691 SqliteValue::Float(result as f64)
18692 }
18693}
18694
18695fn sql_shift_left(val: i64, amount: i64) -> SqliteValue {
18697 if amount < 0 {
18698 return sql_shift_right(val, amount.saturating_neg());
18699 }
18700 if amount >= 64 {
18701 return SqliteValue::Integer(0);
18702 }
18703 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18705 let shift = amount as u32;
18706 SqliteValue::Integer(val << shift)
18707}
18708
18709fn sql_shift_right(val: i64, amount: i64) -> SqliteValue {
18711 if amount < 0 {
18712 return sql_shift_left(val, amount.saturating_neg());
18713 }
18714 if amount >= 64 {
18715 return SqliteValue::Integer(if val < 0 { -1 } else { 0 });
18716 }
18717 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18719 let shift = amount as u32;
18720 SqliteValue::Integer(val >> shift)
18721}
18722
18723fn sql_and(a: &SqliteValue, b: &SqliteValue) -> SqliteValue {
18725 let a_val = if a.is_null() {
18728 None
18729 } else {
18730 Some(vdbe_real_is_truthy(a))
18731 };
18732 let b_val = if b.is_null() {
18733 None
18734 } else {
18735 Some(vdbe_real_is_truthy(b))
18736 };
18737
18738 match (a_val, b_val) {
18739 (Some(false), _) | (_, Some(false)) => SqliteValue::Integer(0),
18740 (Some(true), Some(true)) => SqliteValue::Integer(1),
18741 _ => SqliteValue::Null,
18742 }
18743}
18744
18745fn sql_or(a: &SqliteValue, b: &SqliteValue) -> SqliteValue {
18747 let a_val = if a.is_null() {
18748 None
18749 } else {
18750 Some(vdbe_real_is_truthy(a))
18751 };
18752 let b_val = if b.is_null() {
18753 None
18754 } else {
18755 Some(vdbe_real_is_truthy(b))
18756 };
18757
18758 match (a_val, b_val) {
18759 (Some(true), _) | (_, Some(true)) => SqliteValue::Integer(1),
18760 (Some(false), Some(false)) => SqliteValue::Integer(0),
18761 _ => SqliteValue::Null,
18762 }
18763}
18764
18765fn scan_numeric_prefix(bytes: &[u8]) -> usize {
18770 if bytes.is_empty() {
18771 return 0;
18772 }
18773 let mut i = 0;
18774 if bytes[i] == b'+' || bytes[i] == b'-' {
18776 i += 1;
18777 }
18778 let digit_start = i;
18779 while i < bytes.len() && bytes[i].is_ascii_digit() {
18781 i += 1;
18782 }
18783 if i < bytes.len() && bytes[i] == b'.' {
18785 i += 1;
18786 while i < bytes.len() && bytes[i].is_ascii_digit() {
18787 i += 1;
18788 }
18789 }
18790 if i == digit_start {
18792 return 0;
18793 }
18794 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
18796 let exp_start = i;
18797 i += 1;
18798 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
18799 i += 1;
18800 }
18801 if i < bytes.len() && bytes[i].is_ascii_digit() {
18802 while i < bytes.len() && bytes[i].is_ascii_digit() {
18803 i += 1;
18804 }
18805 } else {
18806 i = exp_start;
18808 }
18809 }
18810 i
18811}
18812
18813fn parse_cast_integer_prefix(s: &str) -> i64 {
18819 let trimmed = s.trim();
18820 if trimmed.is_empty() {
18821 return 0;
18822 }
18823
18824 let bytes = trimmed.as_bytes();
18825 let mut end = if matches!(bytes.first(), Some(b'+' | b'-')) {
18826 1
18827 } else {
18828 0
18829 };
18830 let digit_start = end;
18831 while end < bytes.len() && bytes[end].is_ascii_digit() {
18832 end += 1;
18833 }
18834 if end == digit_start {
18835 return 0;
18836 }
18837
18838 let prefix = &trimmed[..end];
18839 match prefix.parse::<i64>() {
18840 Ok(value) => value,
18841 Err(_) if prefix.starts_with('-') => i64::MIN,
18842 Err(_) => i64::MAX,
18843 }
18844}
18845
18846fn sql_cast(val: SqliteValue, target: i32) -> SqliteValue {
18848 if val.is_null() {
18849 return SqliteValue::Null;
18850 }
18851 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
18856 let target_byte = target as u8;
18857 let val = match (val, target_byte) {
18859 (SqliteValue::Blob(b), b'C' | b'c' | b'D' | b'd' | b'E' | b'e') => {
18860 SqliteValue::Text(SmallText::from_arc_bytes(b))
18861 }
18862 (other, _) => other,
18863 };
18864 match target_byte {
18865 b'A' | b'a' => SqliteValue::Blob(match val {
18866 SqliteValue::Blob(b) => b,
18867 SqliteValue::Text(s) => Arc::from(s.as_bytes_direct()),
18868 other => Arc::from(other.to_text().into_bytes()),
18869 }),
18870 b'B' | b'b' => {
18871 match val {
18875 SqliteValue::Blob(b) => SqliteValue::Text(SmallText::from_arc_bytes(b)),
18876 other => SqliteValue::Text(other.to_text().into()),
18877 }
18878 }
18879 b'C' | b'c' => val.cast_to_numeric(),
18880 b'D' | b'd' => {
18881 match &val {
18884 SqliteValue::Text(s) => SqliteValue::Integer(parse_cast_integer_prefix(s)),
18885 _ => SqliteValue::Integer(val.to_integer()),
18886 }
18887 }
18888 b'E' | b'e' => {
18889 match &val {
18892 SqliteValue::Text(s) => {
18893 let trimmed = s.trim();
18894 let end = scan_numeric_prefix(trimmed.as_bytes());
18895 if end == trimmed.len() && end > 0 {
18897 if let Ok(f) = trimmed.parse::<f64>() {
18898 return SqliteValue::Float(f);
18899 }
18900 }
18901 if end > 0 {
18903 if let Ok(f) = trimmed[..end].parse::<f64>() {
18904 return SqliteValue::Float(f);
18905 }
18906 }
18907 SqliteValue::Float(0.0)
18908 }
18909 _ => SqliteValue::Float(val.to_float()),
18910 }
18911 }
18912 _ => val, }
18914}
18915
18916fn char_to_affinity(ch: char) -> fsqlite_types::TypeAffinity {
18918 match ch {
18919 'B' | 'b' => fsqlite_types::TypeAffinity::Text,
18920 'C' | 'c' => fsqlite_types::TypeAffinity::Numeric,
18921 'D' | 'd' => fsqlite_types::TypeAffinity::Integer,
18922 'E' | 'e' => fsqlite_types::TypeAffinity::Real,
18923 _ => fsqlite_types::TypeAffinity::Blob,
18924 }
18925}
18926
18927#[cfg(test)]
18930mod tests {
18931 use std::sync::Arc;
18932 use std::time::Instant;
18933
18934 use super::*;
18935 use crate::{Label, ProgramBuilder};
18936 use asupersync::runtime::{Runtime, RuntimeBuilder};
18937 use fsqlite_func::vtab::{IndexInfo, VirtualTable, VirtualTableCursor};
18938 use fsqlite_func::{FunctionRegistry, ScalarFunction, register_builtins};
18939 use fsqlite_mvcc::ConcurrentRegistry;
18940 use fsqlite_types::limits::MAX_COLUMN;
18941 use fsqlite_types::opcode::{IndexCursorMeta, Opcode, P4, VdbeOp};
18942 use fsqlite_types::record::{parse_record, parse_record_into, serialize_record_iter_into};
18943 use fsqlite_types::serial_type::{
18944 serial_type_for_blob, serial_type_for_integer, serial_type_for_text, varint_len,
18945 write_varint,
18946 };
18947 use fsqlite_types::{SmallText, Snapshot};
18948 use proptest::prelude::*;
18949 use proptest::test_runner::{Config as ProptestConfig, TestCaseError, TestRunner};
18950 use rusqlite::params_from_iter;
18951 use rusqlite::types::Value as RusqliteValue;
18952
18953 struct RecordProfileThreadOverrideGuard {
18954 previous: Option<bool>,
18955 }
18956
18957 impl RecordProfileThreadOverrideGuard {
18958 fn enabled() -> Self {
18959 let previous = fsqlite_types::record::record_profile_thread_override();
18960 fsqlite_types::record::set_record_profile_thread_override(Some(true));
18961 Self { previous }
18962 }
18963 }
18964
18965 impl Drop for RecordProfileThreadOverrideGuard {
18966 fn drop(&mut self) {
18967 fsqlite_types::record::set_record_profile_thread_override(self.previous);
18968 }
18969 }
18970
18971 struct CancelExecutionFunc {
18972 cx: Cx,
18973 }
18974
18975 fn test_runtime() -> Runtime {
18976 RuntimeBuilder::current_thread()
18977 .blocking_threads(1, 1)
18978 .build()
18979 .expect("test runtime should build")
18980 }
18981
18982 fn run_async<F: std::future::Future>(future: F) -> F::Output {
18983 test_runtime().block_on(future)
18984 }
18985
18986 impl ScalarFunction for CancelExecutionFunc {
18987 fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
18988 self.cx.cancel();
18989 Ok(SqliteValue::Null)
18990 }
18991
18992 fn num_args(&self) -> i32 {
18993 0
18994 }
18995
18996 fn name(&self) -> &str {
18997 "cancel_exec"
18998 }
18999 }
19000
19001 fn run_program(build: impl FnOnce(&mut ProgramBuilder)) -> Vec<Vec<SqliteValue>> {
19003 let mut b = ProgramBuilder::new();
19004 build(&mut b);
19005 let prog = b.finish().expect("program should build");
19006 let mut engine = VdbeEngine::new(prog.register_count());
19007 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
19008 assert_eq!(outcome, ExecOutcome::Done);
19009 engine
19010 .take_results()
19011 .into_iter()
19012 .map(|v| v.into_vec())
19013 .collect()
19014 }
19015
19016 #[test]
19017 fn test_schema_evaluation_context_function_policy_and_diagnostics() {
19018 let fixed_datetime = [SqliteValue::Text("2000-01-01".into())];
19019 let current_datetime = [SqliteValue::Text("now".into())];
19020
19021 for context in [
19022 SchemaEvaluationContext::Index,
19023 SchemaEvaluationContext::GeneratedColumn,
19024 ] {
19025 let error = validate_schema_function_invocation(
19026 context,
19027 "RANDOM",
19028 fsqlite_func::ScalarSchemaSafety::Never,
19029 &[],
19030 )
19031 .expect_err("indexes and generated columns must reject Never functions");
19032 let expected_owner = match context {
19033 SchemaEvaluationContext::Index => "an index",
19034 SchemaEvaluationContext::GeneratedColumn => "a generated column",
19035 SchemaEvaluationContext::CheckConstraint => unreachable!(),
19036 };
19037 assert!(
19038 error.to_string().contains(&format!(
19039 "non-deterministic use of random() in {expected_owner}"
19040 )),
19041 "unexpected diagnostic: {error}"
19042 );
19043 }
19044
19045 validate_schema_function_invocation(
19046 SchemaEvaluationContext::CheckConstraint,
19047 "RANDOM",
19048 fsqlite_func::ScalarSchemaSafety::Never,
19049 &[],
19050 )
19051 .expect("ordinary nondeterministic functions are legal in CHECK constraints");
19052
19053 for (context, expected_owner) in [
19054 (SchemaEvaluationContext::Index, "an index"),
19055 (
19056 SchemaEvaluationContext::GeneratedColumn,
19057 "a generated column",
19058 ),
19059 (
19060 SchemaEvaluationContext::CheckConstraint,
19061 "a CHECK constraint",
19062 ),
19063 ] {
19064 validate_schema_function_invocation(
19065 context,
19066 "DATE",
19067 fsqlite_func::ScalarSchemaSafety::DateTimeConditional,
19068 &fixed_datetime,
19069 )
19070 .expect("fixed date/time arguments are safe in every schema context");
19071
19072 let error = validate_schema_function_invocation(
19073 context,
19074 "DATE",
19075 fsqlite_func::ScalarSchemaSafety::DateTimeConditional,
19076 ¤t_datetime,
19077 )
19078 .expect_err("current-time date() must fail in every schema context");
19079 assert!(
19080 error.to_string().contains(&format!(
19081 "non-deterministic use of date() in {expected_owner}"
19082 )),
19083 "unexpected diagnostic: {error}"
19084 );
19085 }
19086 }
19087
19088 #[test]
19089 fn test_payload_includes_rowid_alias_does_not_false_positive_when_first_stored_value_matches_rowid()
19090 {
19091 let payload_values = vec![
19092 SqliteValue::Integer(1),
19093 SqliteValue::Text("local".into()),
19094 SqliteValue::Text("dup-session".into()),
19095 ];
19096
19097 assert!(
19098 !payload_includes_rowid_alias(&payload_values, 1, 0, Some(4), Some(1)),
19099 "omitted INTEGER PRIMARY KEY aliases must be detected by payload width, not by a coincidental value match"
19100 );
19101 assert!(
19102 !payload_includes_rowid_alias(&payload_values, 1, 0, None, Some(1)),
19103 "countless fallback must not treat a matching first stored integer as proof that the rowid alias is present"
19104 );
19105 }
19106
19107 #[test]
19108 fn test_payload_includes_rowid_alias_lazy_does_not_false_positive_when_first_stored_value_matches_rowid()
19109 {
19110 let record = serialize_record(&[
19111 SqliteValue::Integer(1),
19112 SqliteValue::Text("local".into()),
19113 SqliteValue::Text("dup-session".into()),
19114 ]);
19115 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19116 scratch
19117 .prepare_for_record(&record)
19118 .expect("payload should decode");
19119
19120 assert!(
19121 !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, Some(4), Some(1)),
19122 "lazy rowid-alias detection must not shift columns when the first stored value happens to equal the rowid"
19123 );
19124 assert!(
19125 !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, None, Some(1)),
19126 "countless lazy fallback must stay conservative instead of trusting value equality"
19127 );
19128 }
19129
19130 #[test]
19131 fn test_payload_includes_rowid_alias_lazy_accepts_explicit_null_placeholder() {
19132 let record = serialize_record(&[
19133 SqliteValue::Null,
19134 SqliteValue::Integer(1),
19135 SqliteValue::Text("local".into()),
19136 ]);
19137 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19138 scratch
19139 .prepare_for_record(&record)
19140 .expect("payload should decode");
19141
19142 assert!(payload_includes_rowid_alias_lazy(
19143 &scratch,
19144 &record,
19145 1,
19146 0,
19147 Some(3),
19148 Some(1)
19149 ));
19150 assert!(payload_includes_rowid_alias_lazy(
19151 &scratch,
19152 &record,
19153 1,
19154 0,
19155 None,
19156 Some(1)
19157 ));
19158 assert!(payload_includes_rowid_alias_lazy(
19159 &scratch,
19160 &record,
19161 1,
19162 0,
19163 Some(4),
19164 Some(1)
19165 ));
19166 }
19167
19168 #[test]
19169 fn test_payload_includes_rowid_alias_without_rowid_resolves_common_width_cases() {
19170 let record = serialize_record(&[
19171 SqliteValue::Integer(1),
19172 SqliteValue::Text("local".into()),
19173 SqliteValue::Text("dup-session".into()),
19174 ]);
19175 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19176 scratch
19177 .prepare_for_record(&record)
19178 .expect("payload should decode");
19179
19180 assert!(
19181 payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(3), Some(1)),
19182 "matching payload/table width proves the payload already carries the IPK alias"
19183 );
19184 assert!(
19185 !payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(4), Some(1)),
19186 "one-column-short integer rows must not compare against rowid because the first stored user column may match by coincidence"
19187 );
19188 }
19189
19190 #[test]
19191 fn test_payload_includes_rowid_alias_without_rowid_accepts_short_null_placeholder() {
19192 let record = serialize_record(&[
19193 SqliteValue::Null,
19194 SqliteValue::Text("local".into()),
19195 SqliteValue::Text("dup-session".into()),
19196 ]);
19197 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19198 scratch
19199 .prepare_for_record(&record)
19200 .expect("payload should decode");
19201
19202 assert!(
19203 payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(4), Some(1)),
19204 "SQLite-format rows keep a NULL IPK placeholder when later ALTER TABLE ADD COLUMN makes the payload one column short"
19205 );
19206 }
19207
19208 #[test]
19209 fn test_payload_includes_rowid_alias_without_rowid_rejects_ambiguous_integer_match() {
19210 let record = serialize_record(&[
19211 SqliteValue::Integer(2),
19212 SqliteValue::Text("local".into()),
19213 SqliteValue::Text("dup-session".into()),
19214 ]);
19215 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19216 scratch
19217 .prepare_for_record(&record)
19218 .expect("payload should decode");
19219
19220 assert!(
19221 !payload_includes_rowid_alias_without_rowid(&scratch, 0, Some(5), Some(1)),
19222 "ambiguous integer-at-IPK rows must not be accepted by rowid equality"
19223 );
19224 assert!(
19225 !payload_includes_rowid_alias_without_rowid(&scratch, 0, None, Some(1)),
19226 "the metadata-free fallback remains conservative"
19227 );
19228 }
19229
19230 #[test]
19231 fn test_payload_includes_rowid_alias_lazy_rejects_multi_column_short_integer_match() {
19232 let record = serialize_record(&[
19233 SqliteValue::Integer(1),
19234 SqliteValue::Text("local".into()),
19235 SqliteValue::Text("dup-session".into()),
19236 ]);
19237 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19238 scratch
19239 .prepare_for_record(&record)
19240 .expect("payload should decode");
19241
19242 assert!(
19243 !payload_includes_rowid_alias_lazy(&scratch, &record, 1, 0, Some(5), Some(1)),
19244 "multi-column-short rows must not treat a shifted user integer equal to rowid as an IPK placeholder"
19245 );
19246 }
19247
19248 #[test]
19249 fn test_payload_includes_rowid_alias_detects_short_alter_row_via_not_null_column_metadata() {
19250 let payload_values = vec![
19251 SqliteValue::Null,
19252 SqliteValue::Integer(2),
19253 SqliteValue::Integer(2),
19254 SqliteValue::Text("remote-ci".into()),
19255 SqliteValue::Text("/sessions/b.jsonl".into()),
19256 SqliteValue::Integer(1_700_000_001_000),
19257 ];
19258
19259 assert!(payload_includes_rowid_alias(
19260 &payload_values,
19261 2,
19262 0,
19263 Some(7),
19264 Some(1)
19265 ));
19266 }
19267
19268 #[test]
19269 fn test_payload_includes_rowid_alias_lazy_detects_short_alter_row_via_not_null_column_metadata()
19270 {
19271 let record = serialize_record(&[
19272 SqliteValue::Null,
19273 SqliteValue::Integer(2),
19274 SqliteValue::Integer(2),
19275 SqliteValue::Text("remote-ci".into()),
19276 SqliteValue::Text("/sessions/b.jsonl".into()),
19277 SqliteValue::Integer(1_700_000_001_000),
19278 ]);
19279 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19280 scratch
19281 .prepare_for_record(&record)
19282 .expect("payload should decode");
19283
19284 assert!(payload_includes_rowid_alias_lazy(
19285 &scratch,
19286 &record,
19287 2,
19288 0,
19289 Some(7),
19290 Some(1)
19291 ));
19292 }
19293
19294 #[test]
19295 fn test_payload_includes_rowid_alias_accepts_short_null_placeholder() {
19296 let payload_values = vec![
19297 SqliteValue::Null,
19298 SqliteValue::Integer(5),
19299 SqliteValue::Text("tail".into()),
19300 ];
19301
19302 assert!(payload_includes_rowid_alias(
19303 &payload_values,
19304 9,
19305 0,
19306 Some(4),
19307 Some(2)
19308 ));
19309 }
19310
19311 #[test]
19312 fn test_payload_includes_rowid_alias_accepts_multi_column_short_null_placeholder() {
19313 let payload_values = vec![SqliteValue::Null, SqliteValue::Text("old".into())];
19314
19315 assert!(payload_includes_rowid_alias(
19316 &payload_values,
19317 9,
19318 0,
19319 Some(4),
19320 None
19321 ));
19322 }
19323
19324 #[test]
19325 fn test_payload_includes_rowid_alias_lazy_accepts_short_null_placeholder() {
19326 let record = serialize_record(&[
19327 SqliteValue::Null,
19328 SqliteValue::Integer(5),
19329 SqliteValue::Text("tail".into()),
19330 ]);
19331 let mut scratch = fsqlite_types::record::RecordDecodeScratch::default();
19332 scratch
19333 .prepare_for_record(&record)
19334 .expect("payload should decode");
19335
19336 assert!(payload_includes_rowid_alias_lazy(
19337 &scratch,
19338 &record,
19339 9,
19340 0,
19341 Some(4),
19342 Some(2)
19343 ));
19344 }
19345
19346 fn install_duplicate_prefix_index_fixture(
19347 engine: &mut VdbeEngine,
19348 cursor_id: i32,
19349 ) -> (Vec<u8>, Vec<u8>) {
19350 let mut db = MemDatabase::new();
19351 let index_root = db.allocate_root_page();
19352
19353 engine.enable_storage_cursors(true);
19354 engine.set_database(db);
19355 engine.set_reject_mem_fallback(false);
19356
19357 assert!(
19358 run_async(engine.open_storage_cursor(cursor_id, index_root, true)),
19359 "index storage cursor should open"
19360 );
19361
19362 let sc = engine
19363 .storage_cursors
19364 .get_mut(&cursor_id)
19365 .expect("storage cursor should exist");
19366 for rowid in 1_i64..=128 {
19367 let key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(rowid)]);
19368 run_async(sc.cursor.index_insert(&sc.cx, &key))
19369 .expect("duplicate-prefix index key should insert");
19370 }
19371 run_async(sc.cursor.index_insert(
19372 &sc.cx,
19373 &encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(999)]),
19374 ))
19375 .expect("next-prefix index key should insert");
19376
19377 let conflict_key = encode_record(&[SqliteValue::Integer(7)]);
19378 let miss_key = encode_record(&[SqliteValue::Integer(9)]);
19379
19380 (conflict_key, miss_key)
19381 }
19382
19383 fn build_storage_index_engine_with_duplicate_prefixes() -> (VdbeEngine, Vec<u8>, Vec<u8>) {
19384 let mut engine = VdbeEngine::new(8);
19385 let (conflict_key, miss_key) = install_duplicate_prefix_index_fixture(&mut engine, 0);
19386 (engine, conflict_key, miss_key)
19387 }
19388
19389 fn build_storage_index_engine_with_nocase_prefixes() -> (VdbeEngine, Vec<u8>, Vec<u8>) {
19390 let mut engine = VdbeEngine::new(8);
19391 let mut db = MemDatabase::new();
19392 let index_root = db.allocate_root_page();
19393
19394 engine.enable_storage_cursors(true);
19395 engine.set_database(db);
19396 engine.set_reject_mem_fallback(false);
19397 engine.set_index_collations_by_root_page(HashMap::from([(
19398 index_root,
19399 vec![Some("NOCASE".to_owned())],
19400 )]));
19401
19402 assert!(
19403 run_async(engine.open_storage_cursor(0, index_root, true)),
19404 "NOCASE index storage cursor should open"
19405 );
19406
19407 let sc = engine
19408 .storage_cursors
19409 .get_mut(&0)
19410 .expect("NOCASE storage cursor should exist");
19411 for (text, rowid) in [("alpha", 1_i64), ("beta", 2_i64)] {
19412 let key = encode_record(&[
19413 SqliteValue::Text(text.to_owned().into()),
19414 SqliteValue::Integer(rowid),
19415 ]);
19416 run_async(sc.cursor.index_insert(&sc.cx, &key))
19417 .expect("NOCASE fixture key should insert");
19418 }
19419
19420 let conflict_key = encode_record(&[SqliteValue::Text("ALPHA".into())]);
19421 let miss_key = encode_record(&[SqliteValue::Text("gamma".into())]);
19422
19423 (engine, conflict_key, miss_key)
19424 }
19425
19426 fn build_storage_index_engine_with_nocase_mixed_case_ordering() -> VdbeEngine {
19427 let mut engine = VdbeEngine::new(8);
19428 let mut db = MemDatabase::new();
19429 let index_root = db.allocate_root_page();
19430
19431 engine.enable_storage_cursors(true);
19432 engine.set_database(db);
19433 engine.set_reject_mem_fallback(false);
19434 engine.set_index_collations_by_root_page(HashMap::from([(
19435 index_root,
19436 vec![Some("NOCASE".to_owned())],
19437 )]));
19438
19439 assert!(
19440 run_async(engine.open_storage_cursor(0, index_root, true)),
19441 "NOCASE index storage cursor should open"
19442 );
19443
19444 let sc = engine
19445 .storage_cursors
19446 .get_mut(&0)
19447 .expect("NOCASE storage cursor should exist");
19448 for key in [
19449 encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(1)]),
19450 encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(2)]),
19451 encode_record(&[SqliteValue::Text("beta".into()), SqliteValue::Integer(3)]),
19452 ] {
19453 run_async(sc.cursor.index_insert(&sc.cx, &key))
19454 .expect("mixed-case NOCASE fixture key should insert");
19455 }
19456
19457 engine
19458 }
19459
19460 #[test]
19461 fn test_find_conflicting_rowid_rejects_index_entry_without_rowid_suffix() {
19462 let mut engine = VdbeEngine::new(8);
19463 let mut db = MemDatabase::new();
19464 let index_root = db.allocate_root_page();
19465
19466 engine.enable_storage_cursors(true);
19467 engine.set_database(db);
19468 engine.set_reject_mem_fallback(false);
19469 assert!(
19470 run_async(engine.open_storage_cursor(0, index_root, true)),
19471 "index storage cursor should open"
19472 );
19473
19474 let sc = engine
19475 .storage_cursors
19476 .get_mut(&0)
19477 .expect("storage cursor should exist");
19478 run_async(
19479 sc.cursor
19480 .index_insert(&sc.cx, &encode_record(&[SqliteValue::Integer(7)])),
19481 )
19482 .expect("malformed index fixture should insert for corruption regression");
19483
19484 let probe_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(42)]);
19485 let err = run_async(find_conflicting_rowid_in_index(sc, &probe_key, 1))
19486 .expect_err("missing rowid suffix should be reported as corruption");
19487
19488 assert!(matches!(
19489 err,
19490 FrankenError::DatabaseCorrupt { detail }
19491 if detail.contains("integer rowid suffix")
19492 ));
19493 }
19494
19495 #[test]
19496 fn test_find_conflicting_rowid_rejects_index_entry_with_extra_suffix_fields() {
19497 let mut engine = VdbeEngine::new(8);
19498 let mut db = MemDatabase::new();
19499 let index_root = db.allocate_root_page();
19500
19501 engine.enable_storage_cursors(true);
19502 engine.set_database(db);
19503 engine.set_reject_mem_fallback(false);
19504 assert!(
19505 run_async(engine.open_storage_cursor(0, index_root, true)),
19506 "index storage cursor should open"
19507 );
19508
19509 let sc = engine
19510 .storage_cursors
19511 .get_mut(&0)
19512 .expect("storage cursor should exist");
19513 run_async(sc.cursor.index_insert(
19514 &sc.cx,
19515 &encode_record(&[
19516 SqliteValue::Integer(7),
19517 SqliteValue::Integer(1),
19518 SqliteValue::Text("extra".into()),
19519 ]),
19520 ))
19521 .expect("malformed index fixture should insert for corruption regression");
19522
19523 let probe_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(42)]);
19524 let err = run_async(find_conflicting_rowid_in_index(sc, &probe_key, 1))
19525 .expect_err("extra suffix fields should be reported as corruption");
19526
19527 assert!(matches!(
19528 err,
19529 FrankenError::DatabaseCorrupt { detail }
19530 if detail.contains("integer rowid suffix")
19531 ));
19532 }
19533
19534 #[test]
19535 fn test_collated_conflicting_rowid_rejects_index_entry_without_rowid_suffix() {
19536 let mut engine = VdbeEngine::new(8);
19537 let mut db = MemDatabase::new();
19538 let index_root = db.allocate_root_page();
19539
19540 engine.enable_storage_cursors(true);
19541 engine.set_database(db);
19542 engine.set_reject_mem_fallback(false);
19543 engine.set_index_collations_by_root_page(HashMap::from([(
19544 index_root,
19545 vec![Some("NOCASE".to_owned())],
19546 )]));
19547 assert!(
19548 run_async(engine.open_storage_cursor(0, index_root, true)),
19549 "NOCASE index storage cursor should open"
19550 );
19551
19552 let sc = engine
19553 .storage_cursors
19554 .get_mut(&0)
19555 .expect("NOCASE storage cursor should exist");
19556 run_async(
19557 sc.cursor
19558 .index_insert(&sc.cx, &encode_record(&[SqliteValue::Text("alpha".into())])),
19559 )
19560 .expect("malformed NOCASE index fixture should insert for corruption regression");
19561
19562 let probe_key =
19563 encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(42)]);
19564 let err = run_async(find_conflicting_rowid_in_index_collated(
19565 sc,
19566 &probe_key,
19567 1,
19568 &[false],
19569 &[Some("NOCASE".to_owned())],
19570 &BUILTIN_COLLATION_REGISTRY,
19571 ))
19572 .expect_err("missing rowid suffix should be reported as corruption");
19573
19574 assert!(matches!(
19575 err,
19576 FrankenError::DatabaseCorrupt { detail }
19577 if detail.contains("integer rowid suffix")
19578 ));
19579 }
19580
19581 #[test]
19582 fn test_collated_conflicting_rowid_rejects_index_entry_with_extra_suffix_fields() {
19583 let mut engine = VdbeEngine::new(8);
19584 let mut db = MemDatabase::new();
19585 let index_root = db.allocate_root_page();
19586
19587 engine.enable_storage_cursors(true);
19588 engine.set_database(db);
19589 engine.set_reject_mem_fallback(false);
19590 engine.set_index_collations_by_root_page(HashMap::from([(
19591 index_root,
19592 vec![Some("NOCASE".to_owned())],
19593 )]));
19594 assert!(
19595 run_async(engine.open_storage_cursor(0, index_root, true)),
19596 "NOCASE index storage cursor should open"
19597 );
19598
19599 let sc = engine
19600 .storage_cursors
19601 .get_mut(&0)
19602 .expect("NOCASE storage cursor should exist");
19603 run_async(sc.cursor.index_insert(
19604 &sc.cx,
19605 &encode_record(&[
19606 SqliteValue::Text("alpha".into()),
19607 SqliteValue::Integer(1),
19608 SqliteValue::Text("extra".into()),
19609 ]),
19610 ))
19611 .expect("malformed NOCASE index fixture should insert for corruption regression");
19612
19613 let probe_key =
19614 encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(42)]);
19615 let err = run_async(find_conflicting_rowid_in_index_collated(
19616 sc,
19617 &probe_key,
19618 1,
19619 &[false],
19620 &[Some("NOCASE".to_owned())],
19621 &BUILTIN_COLLATION_REGISTRY,
19622 ))
19623 .expect_err("extra suffix fields should be reported as corruption");
19624
19625 assert!(matches!(
19626 err,
19627 FrankenError::DatabaseCorrupt { detail }
19628 if detail.contains("integer rowid suffix")
19629 ));
19630 }
19631
19632 #[test]
19633 fn test_idxinsert_consumes_make_record_sideband_for_storage_cursor() {
19634 let mut engine = VdbeEngine::new(8);
19635 let mut db = MemDatabase::new();
19636 let index_root = db.allocate_root_page();
19637 engine.enable_storage_cursors(true);
19638 engine.set_database(db);
19639 engine.set_reject_mem_fallback(false);
19640 assert!(
19641 run_async(engine.open_storage_cursor(0, index_root, true)),
19642 "index storage cursor should open"
19643 );
19644
19645 let mut b = ProgramBuilder::new();
19646 let end = b.emit_label();
19647 let found = b.emit_label();
19648 let done = b.emit_label();
19649 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19650
19651 let r_insert_val = b.alloc_reg();
19652 let r_insert_key = b.alloc_reg();
19653 let r_probe_val = b.alloc_reg();
19654 let r_probe_key = b.alloc_reg();
19655 let r_out = b.alloc_reg();
19656
19657 b.emit_op(Opcode::Integer, 7, r_insert_val, 0, P4::None, 0);
19658 b.emit_op(
19659 Opcode::MakeRecord,
19660 r_insert_val,
19661 1,
19662 r_insert_key,
19663 P4::None,
19664 0,
19665 );
19666 b.emit_op(Opcode::IdxInsert, 0, r_insert_key, 0, P4::None, 0);
19667
19668 b.emit_op(Opcode::Integer, 7, r_probe_val, 0, P4::None, 0);
19669 b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
19670 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
19671 b.emit_jump_to_label(Opcode::Found, 0, r_probe_key, found, P4::None, 0);
19672 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
19673 b.resolve_label(found);
19674 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
19675 b.resolve_label(done);
19676
19677 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
19678 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19679 b.resolve_label(end);
19680
19681 let program = b.finish().expect("program should build");
19682 let rows = execute_program_with_engine(&mut engine, &program);
19683 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
19684 }
19685
19686 #[test]
19687 fn test_idxinsert_conflict_skip_preserves_make_record_sideband_buffer() {
19688 let _guard = VDBE_OBSERVABILITY_LOCK
19689 .lock()
19690 .unwrap_or_else(|e| e.into_inner());
19691 reset_vdbe_test_sideband_materialization_count();
19692
19693 let mut builder = ProgramBuilder::new();
19694 let end = builder.emit_label();
19695 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19696
19697 let r_value = builder.alloc_reg();
19698 let r_record = builder.alloc_reg();
19699 builder.emit_op(
19700 Opcode::String8,
19701 0,
19702 r_value,
19703 0,
19704 P4::Str("idx-skip-sideband".repeat(32)),
19705 0,
19706 );
19707 builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19708 builder.emit_op(Opcode::IdxInsert, 0, r_record, 0, P4::None, 0);
19709 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19710 builder.resolve_label(end);
19711
19712 let program = builder.finish().expect("program should build");
19713 let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19714 let mut engine = VdbeEngine::new(program.register_count());
19715 engine.set_conflict_skip_idx(true);
19716
19717 let before = vdbe_test_sideband_materialization_count_snapshot();
19718 let outcome = run_async(engine.execute(&program)).expect("program should execute");
19719 let after = vdbe_test_sideband_materialization_count_snapshot();
19720
19721 assert_eq!(outcome, ExecOutcome::Done);
19722 assert_eq!(
19723 after - before,
19724 0,
19725 "skipped IdxInsert should not materialize a sideband-backed key"
19726 );
19727 assert!(
19728 engine.make_record_lookaside.sideband_is_armed_for(r_record),
19729 "skipped IdxInsert should leave the MakeRecord sideband attached to its register"
19730 );
19731 assert!(
19732 !engine.make_record_lookaside.is_empty(),
19733 "skipped IdxInsert should retain the sideband bytes for reuse/materialization"
19734 );
19735 assert!(
19736 engine.make_record_lookaside.capacity() >= expected_capacity,
19737 "skipped IdxInsert should not drop the reusable sideband allocation"
19738 );
19739 }
19740
19741 #[test]
19742 fn test_idxinsert_returns_make_record_sideband_buffer_after_storage_insert() {
19743 let _guard = VDBE_OBSERVABILITY_LOCK
19744 .lock()
19745 .unwrap_or_else(|e| e.into_inner());
19746 reset_vdbe_test_sideband_materialization_count();
19747
19748 let index_value = "idx-insert-sideband".repeat(32);
19749 let expected_key = encode_record(&[SqliteValue::Text(index_value.clone().into())]);
19750
19751 let mut engine = VdbeEngine::new(8);
19752 let mut db = MemDatabase::new();
19753 let index_root = db.allocate_root_page();
19754 engine.enable_storage_cursors(true);
19755 engine.set_database(db);
19756 engine.set_reject_mem_fallback(false);
19757 assert!(
19758 run_async(engine.open_storage_cursor(0, index_root, true)),
19759 "index storage cursor should open"
19760 );
19761
19762 let mut builder = ProgramBuilder::new();
19763 let end = builder.emit_label();
19764 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19765
19766 let r_value = builder.alloc_reg();
19767 let r_record = builder.alloc_reg();
19768 builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(index_value), 0);
19769 builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19770 builder.emit_op(Opcode::IdxInsert, 0, r_record, 0, P4::None, 0);
19771 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19772 builder.resolve_label(end);
19773
19774 let program = builder.finish().expect("program should build");
19775 let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19776 let before = vdbe_test_sideband_materialization_count_snapshot();
19777 let outcome = run_async(engine.execute(&program)).expect("program should execute");
19778 let after = vdbe_test_sideband_materialization_count_snapshot();
19779
19780 assert_eq!(outcome, ExecOutcome::Done);
19781 assert_eq!(
19782 after - before,
19783 0,
19784 "IdxInsert should consume MakeRecord sideband bytes without materializing"
19785 );
19786 assert!(
19787 engine.make_record_lookaside.is_empty(),
19788 "successful IdxInsert should return a cleared scratch buffer"
19789 );
19790 assert!(
19791 engine.make_record_lookaside.capacity() >= expected_capacity,
19792 "successful IdxInsert should keep the reusable sideband allocation"
19793 );
19794
19795 let index_cursor = engine
19796 .storage_cursors
19797 .get_mut(&0)
19798 .expect("index cursor should remain available");
19799 assert!(
19800 run_async(
19801 index_cursor
19802 .cursor
19803 .index_move_to(&index_cursor.cx, &expected_key)
19804 )
19805 .expect("index seek should succeed")
19806 .is_found(),
19807 "IdxInsert should still insert the key while recycling the sideband buffer"
19808 );
19809 }
19810
19811 #[test]
19812 fn test_idxinsert_unique_abort_returns_make_record_sideband_buffer_on_error() {
19813 let _guard = VDBE_OBSERVABILITY_LOCK
19814 .lock()
19815 .unwrap_or_else(|e| e.into_inner());
19816 reset_vdbe_test_sideband_materialization_count();
19817
19818 let conflict_value = "idx-abort-sideband".repeat(32);
19819 let existing_key = encode_record(&[
19820 SqliteValue::Text(conflict_value.clone().into()),
19821 SqliteValue::Integer(1),
19822 ]);
19823
19824 let mut db = MemDatabase::new();
19825 let index_root = db.allocate_root_page();
19826 let table_root = db.create_table(1);
19827 let mut engine = VdbeEngine::new(8);
19828 engine.enable_storage_cursors(true);
19829 engine.set_database(db);
19830 engine.set_reject_mem_fallback(false);
19831 assert!(run_async(engine.open_storage_cursor(0, index_root, true)));
19832 assert!(run_async(engine.open_storage_cursor(1, table_root, true)));
19833
19834 {
19835 let index_cursor = engine
19836 .storage_cursors
19837 .get_mut(&0)
19838 .expect("index cursor should exist");
19839 run_async(
19840 index_cursor
19841 .cursor
19842 .index_insert(&index_cursor.cx, &existing_key),
19843 )
19844 .expect("existing unique index key should insert");
19845 }
19846
19847 let provisional_payload = encode_record(&[SqliteValue::Integer(99)]);
19848 {
19849 let table_cursor = engine
19850 .storage_cursors
19851 .get_mut(&1)
19852 .expect("table cursor should exist");
19853 run_async(
19854 table_cursor
19855 .cursor
19856 .table_insert(&table_cursor.cx, 2, &provisional_payload),
19857 )
19858 .expect("provisional table row should insert");
19859 }
19860 engine.changes = 1;
19861 engine.set_pending_insert_rollback(Some(PendingInsertRollback {
19862 cursor_id: 1,
19863 rowid: 2,
19864 previous_last_insert_rowid: 0,
19865 previous_last_insert_rowid_valid: false,
19866 update_restore: None,
19867 }));
19868
19869 let mut builder = ProgramBuilder::new();
19870 let end = builder.emit_label();
19871 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19872
19873 let r_value = builder.alloc_reg();
19874 let r_record = builder.alloc_reg();
19875 builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(conflict_value), 0);
19876 builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19877 builder.emit_op(
19878 Opcode::IdxInsert,
19879 0,
19880 r_record,
19881 1,
19882 P4::Table("idx_col".to_owned()),
19883 1,
19884 );
19885 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19886 builder.resolve_label(end);
19887
19888 let program = builder.finish().expect("program should build");
19889 let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19890 let before = vdbe_test_sideband_materialization_count_snapshot();
19891 let error = run_async(engine.execute(&program))
19892 .expect_err("unique conflict should abort the statement");
19893 let after = vdbe_test_sideband_materialization_count_snapshot();
19894
19895 assert!(
19896 matches!(error, FrankenError::UniqueViolation { .. }),
19897 "expected unique violation, got {error:?}"
19898 );
19899 assert_eq!(
19900 after - before,
19901 0,
19902 "unique abort should not materialize a sideband-backed key"
19903 );
19904 assert!(
19905 engine.make_record_lookaside.is_empty(),
19906 "aborted IdxInsert should return a cleared scratch buffer"
19907 );
19908 assert!(
19909 engine.make_record_lookaside.capacity() >= expected_capacity,
19910 "aborted IdxInsert should keep the reusable sideband allocation"
19911 );
19912 }
19913
19914 #[test]
19915 fn test_idxinsert_ignore_restores_sideband_when_rollback_errors() {
19916 let _guard = VDBE_OBSERVABILITY_LOCK
19917 .lock()
19918 .unwrap_or_else(|e| e.into_inner());
19919 reset_vdbe_test_sideband_materialization_count();
19920
19921 let conflict_value = "idx-ignore-rollback-error-sideband".repeat(16);
19922 let existing_key = encode_record(&[
19923 SqliteValue::Text(conflict_value.clone().into()),
19924 SqliteValue::Integer(1),
19925 ]);
19926
19927 let mut db = MemDatabase::new();
19928 let index_root = db.allocate_root_page();
19929 let mut engine = VdbeEngine::new(8);
19930 engine.enable_storage_cursors(true);
19931 engine.set_database(db);
19932 engine.set_reject_mem_fallback(false);
19933 assert!(run_async(engine.open_storage_cursor(0, index_root, true)));
19934
19935 {
19936 let index_cursor = engine
19937 .storage_cursors
19938 .get_mut(&0)
19939 .expect("index cursor should exist");
19940 run_async(
19941 index_cursor
19942 .cursor
19943 .index_insert(&index_cursor.cx, &existing_key),
19944 )
19945 .expect("existing unique index key should insert");
19946 }
19947
19948 let mut builder = ProgramBuilder::new();
19949 let end = builder.emit_label();
19950 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
19951
19952 let r_value = builder.alloc_reg();
19953 let r_record = builder.alloc_reg();
19954 builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str(conflict_value), 0);
19955 builder.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
19956 builder.emit_op(
19957 Opcode::IdxInsert,
19958 0,
19959 r_record,
19960 1,
19961 P4::Table("idx_col".to_owned()),
19962 1 | (4 << 1),
19963 );
19964 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
19965 builder.resolve_label(end);
19966
19967 let program = builder.finish().expect("program should build");
19968 let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
19969 let before = vdbe_test_sideband_materialization_count_snapshot();
19970 let error = run_async(engine.execute(&program))
19971 .expect_err("missing provisional insert rollback should error");
19972 let after = vdbe_test_sideband_materialization_count_snapshot();
19973
19974 assert!(
19975 matches!(&error, FrankenError::Internal(message) if message.contains("secondary-index conflict without pending table insert")),
19976 "expected rollback bookkeeping error, got {error:?}"
19977 );
19978 assert_eq!(
19979 after - before,
19980 0,
19981 "rollback error path should not materialize a sideband-backed key"
19982 );
19983 assert!(
19984 engine.make_record_lookaside.is_empty(),
19985 "failed IGNORE rollback should return a cleared scratch buffer"
19986 );
19987 assert!(
19988 engine.make_record_lookaside.capacity() >= expected_capacity,
19989 "failed IGNORE rollback should keep the reusable sideband allocation"
19990 );
19991 }
19992
19993 #[test]
19994 fn test_no_conflict_consumes_make_record_sideband_probe_key() {
19995 let _guard = VDBE_OBSERVABILITY_LOCK
19996 .lock()
19997 .unwrap_or_else(|e| e.into_inner());
19998 let (mut engine, _, _) = build_storage_index_engine_with_duplicate_prefixes();
19999
20000 let mut b = ProgramBuilder::new();
20001 let end = b.emit_label();
20002 let no_conflict = b.emit_label();
20003 let done = b.emit_label();
20004 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20005
20006 let r_probe = b.alloc_reg();
20007 let r_key = b.alloc_reg();
20008 let r_out = b.alloc_reg();
20009
20010 b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
20011 b.emit_op(Opcode::MakeRecord, r_probe, 1, r_key, P4::None, 0);
20012 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20013 b.emit_jump_to_label(Opcode::NoConflict, 0, r_key, no_conflict, P4::None, 0);
20014 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20015 b.resolve_label(no_conflict);
20016 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
20017 b.resolve_label(done);
20018
20019 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20020 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20021 b.resolve_label(end);
20022
20023 let program = b.finish().expect("program should build");
20024 let rows = execute_program_with_engine(&mut engine, &program);
20025
20026 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
20027 }
20028
20029 fn build_no_conflict_opcode_probe_program(
20030 conflict_key: Vec<u8>,
20031 miss_key: Vec<u8>,
20032 malformed_key: Vec<u8>,
20033 ) -> crate::VdbeProgram {
20034 let mut b = ProgramBuilder::new();
20035 let end = b.emit_label();
20036 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20037
20038 let r_key = b.alloc_reg();
20039 let r_out = b.alloc_reg();
20040
20041 for (probe_key, expected_insertable) in [
20042 (conflict_key, false),
20043 (miss_key, true),
20044 (malformed_key, true),
20045 ] {
20046 let no_conflict = b.emit_label();
20047 let done = b.emit_label();
20048 b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20049 b.emit_jump_to_label(Opcode::NoConflict, 0, r_key, no_conflict, P4::None, 0);
20050 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20051 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20052 b.resolve_label(no_conflict);
20053 b.emit_op(
20054 Opcode::Integer,
20055 i32::from(expected_insertable),
20056 r_out,
20057 0,
20058 P4::None,
20059 0,
20060 );
20061 b.resolve_label(done);
20062 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20063 }
20064
20065 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20066 b.resolve_label(end);
20067 b.finish().expect("program should build")
20068 }
20069
20070 fn build_found_opcode_probe_program(
20071 conflict_key: Vec<u8>,
20072 miss_key: Vec<u8>,
20073 ) -> crate::VdbeProgram {
20074 let mut b = ProgramBuilder::new();
20075 let end = b.emit_label();
20076 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20077
20078 let r_key = b.alloc_reg();
20079 let r_out = b.alloc_reg();
20080
20081 for (probe_key, expected_found) in [(conflict_key, true), (miss_key, false)] {
20082 let found = b.emit_label();
20083 let done = b.emit_label();
20084 b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20085 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20086 b.emit_jump_to_label(Opcode::Found, 0, r_key, found, P4::None, 0);
20087 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20088 b.resolve_label(found);
20089 b.emit_op(
20090 Opcode::Integer,
20091 i32::from(expected_found),
20092 r_out,
20093 0,
20094 P4::None,
20095 0,
20096 );
20097 b.resolve_label(done);
20098 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20099 }
20100
20101 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20102 b.resolve_label(end);
20103 b.finish().expect("program should build")
20104 }
20105
20106 fn build_not_found_opcode_probe_program(
20107 conflict_key: Vec<u8>,
20108 miss_key: Vec<u8>,
20109 ) -> crate::VdbeProgram {
20110 let mut b = ProgramBuilder::new();
20111 let end = b.emit_label();
20112 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20113
20114 let r_key = b.alloc_reg();
20115 let r_out = b.alloc_reg();
20116
20117 for probe_key in [conflict_key, miss_key] {
20118 let missing = b.emit_label();
20119 let done = b.emit_label();
20120 b.emit_op(Opcode::Blob, 0, r_key, 0, P4::Blob(probe_key), 0);
20121 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
20122 b.emit_jump_to_label(Opcode::NotFound, 0, r_key, missing, P4::None, 0);
20123 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
20124 b.resolve_label(missing);
20125 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
20126 b.resolve_label(done);
20127 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
20128 }
20129
20130 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20131 b.resolve_label(end);
20132 b.finish().expect("program should build")
20133 }
20134
20135 fn execute_program_with_engine(
20136 engine: &mut VdbeEngine,
20137 program: &crate::VdbeProgram,
20138 ) -> Vec<Vec<SqliteValue>> {
20139 let outcome = run_async(engine.execute(program)).expect("execution should succeed");
20140 assert_eq!(outcome, ExecOutcome::Done);
20141 engine
20142 .take_results()
20143 .into_iter()
20144 .map(|row| row.into_vec())
20145 .collect::<Vec<_>>()
20146 }
20147
20148 fn legacy_storage_cursor_no_conflict_prefix_match(
20149 cursor: &mut StorageCursor,
20150 key_bytes: &[u8],
20151 ) -> Result<bool> {
20152 let probe_fields = match parse_record(key_bytes) {
20153 Some(fields) => fields,
20154 None => return Ok(false),
20155 };
20156 if probe_fields.iter().any(SqliteValue::is_null) {
20157 return Ok(false);
20158 }
20159 run_async(cursor.cursor.index_move_to(&cursor.cx, key_bytes))?;
20160 if let Ok(entry_bytes) = run_async(cursor.cursor.payload(&cursor.cx)) {
20161 if let Some(entry_fields) = parse_record(&entry_bytes) {
20162 let prefix_len = probe_fields.len();
20163 return Ok(entry_fields.len() >= prefix_len
20164 && entry_fields[..prefix_len] == probe_fields[..]);
20165 }
20166 }
20167 Ok(false)
20168 }
20169
20170 #[test]
20171 fn test_take_results_preserves_result_buffer_capacity_for_reuse() {
20172 let mut b = ProgramBuilder::new();
20173 let end = b.emit_label();
20174 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20175 let r = b.alloc_reg();
20176 b.emit_op(Opcode::Integer, 1, r, 0, P4::None, 0);
20177 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
20178 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20179 b.resolve_label(end);
20180 let program = b.finish().expect("program should build");
20181
20182 let mut engine = VdbeEngine::new(program.register_count());
20183 assert_eq!(
20184 engine.result_buffer_capacity(),
20185 64,
20186 "new engines should keep the preallocated result-row buffer"
20187 );
20188
20189 let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20190 assert_eq!(outcome, ExecOutcome::Done);
20191 let rows = engine.take_results();
20192 assert_eq!(rows.len(), 1);
20193 assert_eq!(
20194 engine.result_buffer_capacity(),
20195 64,
20196 "taking results should preserve buffer capacity for the next execution"
20197 );
20198 }
20199
20200 #[test]
20201 fn test_take_exactly_one_result_row_preserves_result_buffer_capacity_for_reuse() {
20202 let mut one_row_builder = ProgramBuilder::new();
20203 let one_row_end = one_row_builder.emit_label();
20204 one_row_builder.emit_jump_to_label(Opcode::Init, 0, 0, one_row_end, P4::None, 0);
20205 let one_row_reg = one_row_builder.alloc_reg();
20206 one_row_builder.emit_op(Opcode::Integer, 7, one_row_reg, 0, P4::None, 0);
20207 one_row_builder.emit_op(Opcode::ResultRow, one_row_reg, 1, 0, P4::None, 0);
20208 one_row_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20209 one_row_builder.resolve_label(one_row_end);
20210 let one_row_program = one_row_builder.finish().expect("program should build");
20211
20212 let mut engine = VdbeEngine::new(one_row_program.register_count());
20213 let initial_capacity = engine.result_buffer_capacity();
20214 let outcome =
20215 run_async(engine.execute(&one_row_program)).expect("one-row execution should succeed");
20216 assert_eq!(outcome, ExecOutcome::Done);
20217 assert_eq!(
20218 engine.take_exactly_one_result_row(),
20219 ExactResultRowOutcome::Row(Box::new(smallvec::smallvec![SqliteValue::Integer(7)])),
20220 );
20221 assert_eq!(
20222 engine.result_buffer_capacity(),
20223 initial_capacity,
20224 "exact-one-row drain should preserve the reusable result buffer"
20225 );
20226
20227 let mut many_rows_builder = ProgramBuilder::new();
20228 let many_rows_end = many_rows_builder.emit_label();
20229 many_rows_builder.emit_jump_to_label(Opcode::Init, 0, 0, many_rows_end, P4::None, 0);
20230 let many_rows_reg = many_rows_builder.alloc_reg();
20231 many_rows_builder.emit_op(Opcode::Integer, 1, many_rows_reg, 0, P4::None, 0);
20232 many_rows_builder.emit_op(Opcode::ResultRow, many_rows_reg, 1, 0, P4::None, 0);
20233 many_rows_builder.emit_op(Opcode::Integer, 2, many_rows_reg, 0, P4::None, 0);
20234 many_rows_builder.emit_op(Opcode::ResultRow, many_rows_reg, 1, 0, P4::None, 0);
20235 many_rows_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20236 many_rows_builder.resolve_label(many_rows_end);
20237 let many_rows_program = many_rows_builder.finish().expect("program should build");
20238
20239 let outcome = run_async(engine.execute(&many_rows_program))
20240 .expect("multi-row execution should succeed");
20241 assert_eq!(outcome, ExecOutcome::Done);
20242 assert_eq!(
20243 engine.take_exactly_one_result_row(),
20244 ExactResultRowOutcome::MultipleRows,
20245 );
20246 assert_eq!(
20247 engine.result_buffer_capacity(),
20248 initial_capacity,
20249 "multiple-row drain should also preserve the reusable result buffer"
20250 );
20251 }
20252
20253 #[test]
20254 fn test_disabling_result_row_collection_still_clears_result_registers() {
20255 let mut b = ProgramBuilder::new();
20256 let end = b.emit_label();
20257 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20258 let r1 = b.alloc_reg();
20259 let r2 = b.alloc_reg();
20260 b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20261 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20262 b.emit_op(Opcode::IntCopy, r1, r2, 0, P4::None, 0);
20263 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20264 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20265 b.resolve_label(end);
20266 let program = b.finish().expect("program should build");
20267
20268 let mut engine = VdbeEngine::new(program.register_count());
20269 engine.set_collect_result_rows(false);
20270 let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20271 assert_eq!(outcome, ExecOutcome::Done);
20272 assert!(
20273 engine.results().is_empty(),
20274 "rowless execution should not retain ResultRow payloads"
20275 );
20276 assert_eq!(
20277 engine.get_reg(r1),
20278 &SqliteValue::Null,
20279 "discarded ResultRow should still clear its source registers"
20280 );
20281 assert_eq!(
20282 engine.get_reg(r2),
20283 &SqliteValue::Null,
20284 "later ResultRow opcodes should observe the same cleared-register semantics"
20285 );
20286 }
20287
20288 #[test]
20289 fn test_result_row_collection_cap_discards_rows_after_limit_and_clears_registers() {
20290 let mut b = ProgramBuilder::new();
20291 let end = b.emit_label();
20292 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20293 let r1 = b.alloc_reg();
20294 let r2 = b.alloc_reg();
20295 let r3 = b.alloc_reg();
20296 let r4 = b.alloc_reg();
20297 let r5 = b.alloc_reg();
20298 let r6 = b.alloc_reg();
20299 b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20300 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20301 b.emit_op(Opcode::Integer, 8, r2, 0, P4::None, 0);
20302 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20303 b.emit_op(Opcode::Integer, 9, r3, 0, P4::None, 0);
20304 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
20305 b.emit_op(Opcode::IntCopy, r1, r4, 0, P4::None, 0);
20306 b.emit_op(Opcode::IntCopy, r2, r5, 0, P4::None, 0);
20307 b.emit_op(Opcode::IntCopy, r3, r6, 0, P4::None, 0);
20308 b.emit_op(Opcode::ResultRow, r4, 3, 0, P4::None, 0);
20309 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20310 b.resolve_label(end);
20311 let program = b.finish().expect("program should build");
20312
20313 let mut engine = VdbeEngine::new(program.register_count());
20314 engine.set_max_collected_result_rows(Some(2));
20315 let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20316 assert_eq!(outcome, ExecOutcome::Done);
20317 assert_eq!(
20318 engine
20319 .results()
20320 .iter()
20321 .map(|row| row.clone().into_vec())
20322 .collect::<Vec<_>>(),
20323 vec![vec![SqliteValue::Integer(7)], vec![SqliteValue::Integer(8)],],
20324 "rows after the retention cap should be discarded"
20325 );
20326 assert_eq!(engine.get_reg(r3), &SqliteValue::Null);
20327 assert_eq!(engine.get_reg(r4), &SqliteValue::Null);
20328 assert_eq!(engine.get_reg(r5), &SqliteValue::Null);
20329 assert_eq!(engine.get_reg(r6), &SqliteValue::Null);
20330 }
20331
20332 #[test]
20333 fn test_result_row_handler_streams_rows_without_retaining_results() {
20334 let mut b = ProgramBuilder::new();
20335 let end = b.emit_label();
20336 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20337 let r1 = b.alloc_reg();
20338 let r2 = b.alloc_reg();
20339 b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
20340 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
20341 b.emit_op(Opcode::Integer, 8, r2, 0, P4::None, 0);
20342 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
20343 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20344 b.resolve_label(end);
20345 let program = b.finish().expect("program should build");
20346
20347 let mut engine = VdbeEngine::new(program.register_count());
20348 let mut streamed_rows = Vec::new();
20349 let outcome = run_async(engine.execute_with_borrowed_bindings_and_row_handler(
20350 &program,
20351 None,
20352 &mut |row| {
20353 streamed_rows.push(row.into_vec());
20354 Ok(())
20355 },
20356 ))
20357 .expect("execution should succeed");
20358
20359 assert_eq!(outcome, ExecOutcome::Done);
20360 assert_eq!(
20361 streamed_rows,
20362 vec![vec![SqliteValue::Integer(7)], vec![SqliteValue::Integer(8)],],
20363 "row handler should observe result rows in program order"
20364 );
20365 assert!(
20366 engine.results().is_empty(),
20367 "streaming callback should avoid retaining result rows in the engine buffer"
20368 );
20369 assert_eq!(engine.get_reg(r1), &SqliteValue::Null);
20370 assert_eq!(engine.get_reg(r2), &SqliteValue::Null);
20371 }
20372
20373 #[test]
20374 fn test_execute_swaps_shared_table_index_meta_per_program() {
20375 let first_program = {
20376 let mut b = ProgramBuilder::new();
20377 let end = b.emit_label();
20378 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20379 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20380 b.resolve_label(end);
20381 b.register_table_indexes(
20382 7,
20383 vec![IndexCursorMeta {
20384 cursor_id: 8,
20385 column_indices: vec![0, 2],
20386 }],
20387 );
20388 b.finish().expect("first program should build")
20389 };
20390 let second_program = {
20391 let mut b = ProgramBuilder::new();
20392 let end = b.emit_label();
20393 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20394 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20395 b.resolve_label(end);
20396 b.finish().expect("second program should build")
20397 };
20398
20399 let mut engine = VdbeEngine::new(
20400 first_program
20401 .register_count()
20402 .max(second_program.register_count()),
20403 );
20404 assert!(engine.table_index_meta.is_empty());
20405
20406 let first_outcome =
20407 run_async(engine.execute(&first_program)).expect("first execution should succeed");
20408 assert_eq!(first_outcome, ExecOutcome::Done);
20409 assert!(Arc::ptr_eq(
20410 &engine.table_index_meta,
20411 first_program.shared_table_index_meta()
20412 ));
20413 let first_meta = engine
20414 .table_index_meta
20415 .get(&7)
20416 .expect("first program metadata should be visible to the engine");
20417 assert_eq!(first_meta.len(), 1);
20418 assert_eq!(first_meta[0].cursor_id, 8);
20419 assert_eq!(first_meta[0].column_indices, vec![0, 2]);
20420
20421 let second_outcome =
20422 run_async(engine.execute(&second_program)).expect("second execution should succeed");
20423 assert_eq!(second_outcome, ExecOutcome::Done);
20424 assert!(Arc::ptr_eq(
20425 &engine.table_index_meta,
20426 second_program.shared_table_index_meta()
20427 ));
20428 assert!(
20429 engine.table_index_meta.is_empty(),
20430 "executing a program without REPLACE metadata must clear prior program metadata"
20431 );
20432 }
20433
20434 fn run_program_with_bindings(
20436 build: impl FnOnce(&mut ProgramBuilder),
20437 bindings: Vec<SqliteValue>,
20438 ) -> Vec<Vec<SqliteValue>> {
20439 let mut b = ProgramBuilder::new();
20440 build(&mut b);
20441 let prog = b.finish().expect("program should build");
20442 let mut engine = VdbeEngine::new(prog.register_count());
20443 engine.set_bindings(bindings);
20444 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
20445 assert_eq!(outcome, ExecOutcome::Done);
20446 engine
20447 .take_results()
20448 .into_iter()
20449 .map(|v| v.into_vec())
20450 .collect()
20451 }
20452
20453 fn track_r_log_metrics(
20454 test_name: &str,
20455 alloc_count: usize,
20456 scratch_capacity_bytes: usize,
20457 encode_time_ns: u128,
20458 ) {
20459 eprintln!(
20460 "track_r test={test_name} alloc_count={alloc_count} scratch_capacity_bytes={scratch_capacity_bytes} encode_time_ns={encode_time_ns}"
20461 );
20462 }
20463
20464 fn track_r_values_bitwise_eq(left: &SqliteValue, right: &SqliteValue) -> bool {
20465 match (left, right) {
20466 (SqliteValue::Null, SqliteValue::Null) => true,
20467 (SqliteValue::Integer(lhs), SqliteValue::Integer(rhs)) => lhs == rhs,
20468 (SqliteValue::Float(lhs), SqliteValue::Float(rhs)) => lhs.to_bits() == rhs.to_bits(),
20469 (SqliteValue::Text(lhs), SqliteValue::Text(rhs)) => lhs == rhs,
20470 (SqliteValue::Blob(lhs), SqliteValue::Blob(rhs)) => lhs == rhs,
20471 _ => false,
20472 }
20473 }
20474
20475 fn track_r_assert_rows_eq(actual: &[SqliteValue], expected: &[SqliteValue], context: &str) {
20476 assert_eq!(
20477 actual.len(),
20478 expected.len(),
20479 "{context}: column count mismatch"
20480 );
20481 for (idx, (actual_value, expected_value)) in actual.iter().zip(expected.iter()).enumerate()
20482 {
20483 assert!(
20484 track_r_values_bitwise_eq(actual_value, expected_value),
20485 "{context}: mismatch at column {idx}: actual={actual_value:?} expected={expected_value:?}",
20486 );
20487 }
20488 }
20489
20490 fn track_r_serialized_value_layout(value: &SqliteValue) -> (u64, usize) {
20491 match value {
20492 SqliteValue::Null => (0, 0),
20493 SqliteValue::Integer(integer) => {
20494 let serial_type = serial_type_for_integer(*integer);
20495 let payload_len = match serial_type {
20496 8 | 9 => 0,
20497 1 => 1,
20498 2 => 2,
20499 3 => 3,
20500 4 => 4,
20501 5 => 6,
20502 6 => 8,
20503 _ => unreachable!("integer serial type must be in 1..=9"),
20504 };
20505 (serial_type, payload_len)
20506 }
20507 SqliteValue::Float(float) => {
20508 if float.is_nan() {
20509 (0, 0)
20510 } else {
20511 (7, 8)
20512 }
20513 }
20514 SqliteValue::Text(text) => {
20515 let len = text.len();
20516 (
20517 serial_type_for_text(u64::try_from(len).unwrap_or(u64::MAX)),
20518 len,
20519 )
20520 }
20521 SqliteValue::Blob(blob) => {
20522 let len = blob.len();
20523 (
20524 serial_type_for_blob(u64::try_from(len).unwrap_or(u64::MAX)),
20525 len,
20526 )
20527 }
20528 }
20529 }
20530
20531 fn track_r_compute_header_size(content_size: usize) -> usize {
20532 let mut header_size = content_size + 1;
20533 loop {
20534 let needed = varint_len(header_size as u64) + content_size;
20535 if needed <= header_size {
20536 return header_size;
20537 }
20538 header_size = needed;
20539 }
20540 }
20541
20542 fn track_r_encode_serialized_value(value: &SqliteValue, payload_len: usize, buf: &mut [u8]) {
20543 match value {
20544 SqliteValue::Null => {}
20545 SqliteValue::Integer(integer) => {
20546 if payload_len == 0 {
20547 return;
20548 }
20549 let bytes = integer.to_be_bytes();
20550 buf.copy_from_slice(&bytes[8 - payload_len..]);
20551 }
20552 SqliteValue::Float(float) => {
20553 if float.is_nan() {
20554 return;
20555 }
20556 buf.copy_from_slice(&float.to_bits().to_be_bytes());
20557 }
20558 SqliteValue::Text(text) => buf.copy_from_slice(text.as_bytes_direct()),
20559 SqliteValue::Blob(blob) => buf.copy_from_slice(blob),
20560 }
20561 }
20562
20563 fn track_r_reference_three_pass_record(values: &[SqliteValue]) -> Vec<u8> {
20564 let layouts: Vec<_> = values.iter().map(track_r_serialized_value_layout).collect();
20565 let header_content_size = layouts
20566 .iter()
20567 .map(|(serial_type, _)| varint_len(*serial_type))
20568 .sum::<usize>();
20569 let body_size = layouts
20570 .iter()
20571 .map(|(_, payload_len)| *payload_len)
20572 .sum::<usize>();
20573 let header_size = track_r_compute_header_size(header_content_size);
20574 let total_size = header_size + body_size;
20575 let mut buf = vec![0; total_size];
20576
20577 let mut header_offset = write_varint(
20578 buf.as_mut_slice(),
20579 u64::try_from(header_size).unwrap_or(u64::MAX),
20580 );
20581 let mut body_offset = header_size;
20582 for (value, (serial_type, payload_len)) in values.iter().zip(layouts.iter().copied()) {
20583 header_offset += write_varint(&mut buf[header_offset..], serial_type);
20584 track_r_encode_serialized_value(
20585 value,
20586 payload_len,
20587 &mut buf[body_offset..body_offset + payload_len],
20588 );
20589 body_offset += payload_len;
20590 }
20591
20592 assert_eq!(header_offset, header_size, "reference header width drifted");
20593 assert_eq!(body_offset, total_size, "reference body width drifted");
20594 buf
20595 }
20596
20597 fn track_r_arb_sqlite_value() -> BoxedStrategy<SqliteValue> {
20598 prop_oneof![
20599 4 => Just(SqliteValue::Null),
20600 8 => any::<i64>().prop_map(SqliteValue::Integer),
20601 4 => (-1.0e12_f64..1.0e12_f64).prop_map(SqliteValue::Float),
20602 4 => proptest::collection::vec(any::<char>(), 0..64).prop_map(|chars| {
20603 let text = chars.into_iter().collect::<String>();
20604 SqliteValue::Text(SmallText::from_string(text))
20605 }),
20606 4 => proptest::collection::vec(any::<u8>(), 0..96)
20607 .prop_map(|bytes| SqliteValue::Blob(Arc::<[u8]>::from(bytes))),
20608 ]
20609 .boxed()
20610 }
20611
20612 fn track_r_generated_record(seed: usize) -> Vec<SqliteValue> {
20613 let integer = i64::try_from(seed).unwrap_or(i64::MAX) - 5_000;
20614 let tail = match seed % 5 {
20615 0 => SqliteValue::Null,
20616 1 => SqliteValue::Integer(0),
20617 2 => SqliteValue::Integer(1),
20618 3 => SqliteValue::Integer(-1),
20619 _ => SqliteValue::Integer(i64::try_from(seed % 97).unwrap_or(0) - 48),
20620 };
20621 let text = match seed % 4 {
20622 0 => String::new(),
20623 1 => format!("row-{seed:05}"),
20624 2 => "x".repeat((seed % 32) + 1),
20625 _ => format!("record-{seed:05}-tail"),
20626 };
20627 let blob_len = match seed % 6 {
20628 0 => 0,
20629 1 => 1,
20630 2 => 7,
20631 3 => 19,
20632 4 => 33,
20633 _ => (seed % 48) + 1,
20634 };
20635 let blob = (0..blob_len)
20636 .map(|offset| {
20637 let byte = (seed.wrapping_mul(17)).wrapping_add(offset.wrapping_mul(29)) % 251;
20638 u8::try_from(byte).unwrap_or(0)
20639 })
20640 .collect::<Vec<_>>();
20641
20642 vec![
20643 SqliteValue::Integer(integer),
20644 SqliteValue::Float((seed as f64).mul_add(1.625, -777.25)),
20645 SqliteValue::Text(SmallText::from_string(text)),
20646 SqliteValue::Blob(Arc::<[u8]>::from(blob)),
20647 tail,
20648 ]
20649 }
20650
20651 fn track_r_large_scratch_record(iteration: usize) -> Vec<SqliteValue> {
20652 if iteration == 0 {
20653 return vec![
20654 SqliteValue::Integer(-9_223_372_036_854_775_000),
20655 SqliteValue::Text(SmallText::from_string("x".repeat(2_048))),
20656 SqliteValue::Blob(Arc::<[u8]>::from(vec![0xAB; 4_096])),
20657 ];
20658 }
20659
20660 vec![
20661 SqliteValue::Integer(i64::try_from(iteration).unwrap_or(i64::MAX)),
20662 SqliteValue::Text(SmallText::from_string(format!("row-{iteration:04}"))),
20663 SqliteValue::Blob(Arc::<[u8]>::from(vec![
20664 u8::try_from(iteration % 251)
20665 .unwrap_or(0);
20666 (iteration % 24) + 1
20667 ])),
20668 ]
20669 }
20670
20671 fn track_r_make_record_op(source_reg: i32, column_count: usize, target_reg: i32) -> VdbeOp {
20672 VdbeOp {
20673 opcode: Opcode::MakeRecord,
20674 p1: source_reg,
20675 p2: i32::try_from(column_count).expect("column count should fit in i32"),
20676 p3: target_reg,
20677 p4: P4::None,
20678 p5: 0,
20679 }
20680 }
20681
20682 fn track_r_decode_make_record_buf(engine: &VdbeEngine) -> Vec<SqliteValue> {
20683 parse_record(engine.make_record_lookaside.as_slice())
20684 .expect("MakeRecord sideband should decode")
20685 }
20686
20687 fn track_r_emit_value(builder: &mut ProgramBuilder, reg: i32, value: &SqliteValue) {
20688 match value {
20689 SqliteValue::Null => {
20690 builder.emit_op(Opcode::Null, 0, reg, 0, P4::None, 0);
20691 }
20692 SqliteValue::Integer(integer) => {
20693 builder.emit_op(Opcode::Int64, 0, reg, 0, P4::Int64(*integer), 0);
20694 }
20695 SqliteValue::Float(float) => {
20696 builder.emit_op(Opcode::Real, 0, reg, 0, P4::Real(*float), 0);
20697 }
20698 SqliteValue::Text(text) => {
20699 builder.emit_op(
20700 Opcode::String8,
20701 0,
20702 reg,
20703 0,
20704 P4::Str(text.as_str().to_owned()),
20705 0,
20706 );
20707 }
20708 SqliteValue::Blob(blob) => {
20709 builder.emit_op(Opcode::Blob, 0, reg, 0, P4::Blob(blob.to_vec()), 0);
20710 }
20711 }
20712 }
20713
20714 fn track_r_run_write_with_memdb(
20715 db: MemDatabase,
20716 build: impl FnOnce(&mut ProgramBuilder),
20717 ) -> (Vec<Vec<SqliteValue>>, MemDatabase) {
20718 let mut builder = ProgramBuilder::new();
20719 build(&mut builder);
20720 let program = builder.finish().expect("program should build");
20721 let mut engine = VdbeEngine::new(program.register_count());
20722 engine.set_database(db);
20723 engine.set_reject_mem_fallback(false);
20724 let outcome = run_async(engine.execute(&program)).expect("execution should succeed");
20725 assert_eq!(outcome, ExecOutcome::Done);
20726 let results = engine
20727 .take_results()
20728 .into_iter()
20729 .map(|row| row.into_vec())
20730 .collect::<Vec<_>>();
20731 let final_db = engine.take_database().expect("database should exist");
20732 (results, final_db)
20733 }
20734
20735 fn track_r_to_rusqlite_value(value: &SqliteValue) -> RusqliteValue {
20736 match value {
20737 SqliteValue::Null => RusqliteValue::Null,
20738 SqliteValue::Integer(integer) => RusqliteValue::Integer(*integer),
20739 SqliteValue::Float(float) => RusqliteValue::Real(*float),
20740 SqliteValue::Text(text) => RusqliteValue::Text(text.as_str().to_owned()),
20741 SqliteValue::Blob(blob) => RusqliteValue::Blob(blob.to_vec()),
20742 }
20743 }
20744
20745 fn track_r_from_rusqlite_value(value: RusqliteValue) -> SqliteValue {
20746 match value {
20747 RusqliteValue::Null => SqliteValue::Null,
20748 RusqliteValue::Integer(integer) => SqliteValue::Integer(integer),
20749 RusqliteValue::Real(float) => SqliteValue::Float(float),
20750 RusqliteValue::Text(text) => SqliteValue::Text(SmallText::from_string(text)),
20751 RusqliteValue::Blob(blob) => SqliteValue::Blob(Arc::<[u8]>::from(blob)),
20752 }
20753 }
20754
20755 #[test]
20756 fn test_track_r_make_record_scratch_reuse_1000_records_no_realloc_after_first() {
20757 let mut engine = VdbeEngine::new(8);
20758 let op = track_r_make_record_op(1, 3, 4);
20759 let mut scratch_capacity = None;
20760 let mut alloc_count = 0usize;
20761 let start = Instant::now();
20762
20763 for iteration in 0..1_000 {
20764 let values = track_r_large_scratch_record(iteration);
20765 for (offset, value) in values.iter().enumerate() {
20766 let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20767 engine.set_reg(reg, value.clone());
20768 }
20769
20770 engine.execute_make_record_hot(&op, false);
20771
20772 let observed_capacity = engine.make_record_lookaside.capacity();
20773 if let Some(expected_capacity) = scratch_capacity {
20774 assert_eq!(
20775 observed_capacity, expected_capacity,
20776 "MakeRecord scratch reallocated after warmup at iteration {iteration}",
20777 );
20778 } else {
20779 assert!(
20780 observed_capacity > 0,
20781 "scratch capacity should grow on first encode"
20782 );
20783 scratch_capacity = Some(observed_capacity);
20784 alloc_count += 1;
20785 }
20786
20787 let decoded = track_r_decode_make_record_buf(&engine);
20788 track_r_assert_rows_eq(
20789 &decoded,
20790 &values,
20791 "track_r scratch reuse MakeRecord decode mismatch",
20792 );
20793 }
20794
20795 track_r_log_metrics(
20796 "test_track_r_make_record_scratch_reuse_1000_records_no_realloc_after_first",
20797 alloc_count,
20798 scratch_capacity.unwrap_or(0),
20799 start.elapsed().as_nanos(),
20800 );
20801 }
20802
20803 #[test]
20804 fn test_track_r_two_pass_matches_three_pass_proptest_10k_random() {
20805 let strategy = proptest::collection::vec(track_r_arb_sqlite_value(), 0..48);
20806 let mut runner = TestRunner::new(ProptestConfig {
20807 cases: 10_000,
20808 ..ProptestConfig::default()
20809 });
20810 let two_pass = std::cell::RefCell::new(Vec::new());
20811 let alloc_count = std::cell::Cell::new(0usize);
20812 let encode_time_ns = std::cell::Cell::new(0_u128);
20813 let scratch_capacity_bytes = std::cell::Cell::new(0usize);
20814
20815 runner
20816 .run(&strategy, |values| {
20817 let start = Instant::now();
20818 let mut two_pass_buf = two_pass.borrow_mut();
20819 serialize_record_iter_into(values.iter(), &mut two_pass_buf);
20820 let current_capacity = two_pass_buf.capacity();
20821 if current_capacity != scratch_capacity_bytes.get() {
20822 alloc_count.set(alloc_count.get().saturating_add(1));
20823 scratch_capacity_bytes.set(current_capacity);
20824 }
20825 let three_pass = track_r_reference_three_pass_record(&values);
20826 encode_time_ns.set(
20827 encode_time_ns
20828 .get()
20829 .saturating_add(start.elapsed().as_nanos()),
20830 );
20831 if *two_pass_buf != three_pass {
20832 return Err(TestCaseError::fail(format!(
20833 "two-pass and three-pass record encoders diverged for values={values:?}"
20834 )));
20835 }
20836 Ok(())
20837 })
20838 .expect("two-pass encoder should match the three-pass reference");
20839
20840 track_r_log_metrics(
20841 "test_track_r_two_pass_matches_three_pass_proptest_10k_random",
20842 alloc_count.get(),
20843 scratch_capacity_bytes
20844 .get()
20845 .max(two_pass.borrow().capacity()),
20846 encode_time_ns.get(),
20847 );
20848 }
20849
20850 #[test]
20851 fn test_track_r_make_record_all_sqlite_types() {
20852 let values = vec![
20853 SqliteValue::Null,
20854 SqliteValue::Integer(-9_223_372_036_854_775_000),
20855 SqliteValue::Float(-1234.5),
20856 SqliteValue::Text(SmallText::from_string("hello-track-r".to_owned())),
20857 SqliteValue::Blob(Arc::<[u8]>::from(vec![0x00, 0x01, 0xFE, 0xFF])),
20858 ];
20859 let mut engine = VdbeEngine::new(8);
20860 let op = track_r_make_record_op(1, values.len(), 7);
20861 let start = Instant::now();
20862
20863 for (offset, value) in values.iter().enumerate() {
20864 let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20865 engine.set_reg(reg, value.clone());
20866 }
20867 engine.execute_make_record_hot(&op, false);
20868
20869 let decoded = track_r_decode_make_record_buf(&engine);
20870 track_r_assert_rows_eq(
20871 &decoded,
20872 &values,
20873 "track_r all-types MakeRecord decode mismatch",
20874 );
20875 track_r_log_metrics(
20876 "test_track_r_make_record_all_sqlite_types",
20877 1,
20878 engine.make_record_lookaside.capacity(),
20879 start.elapsed().as_nanos(),
20880 );
20881 }
20882
20883 #[test]
20884 fn test_track_r_make_record_empty_record() {
20885 let mut engine = VdbeEngine::new(2);
20886 let op = track_r_make_record_op(1, 0, 1);
20887 let start = Instant::now();
20888
20889 engine.execute_make_record_hot(&op, false);
20890
20891 let decoded = track_r_decode_make_record_buf(&engine);
20892 assert!(
20893 decoded.is_empty(),
20894 "empty MakeRecord should decode to no columns"
20895 );
20896 track_r_log_metrics(
20897 "test_track_r_make_record_empty_record",
20898 1,
20899 engine.make_record_lookaside.capacity(),
20900 start.elapsed().as_nanos(),
20901 );
20902 }
20903
20904 #[test]
20905 fn test_track_r_make_record_max_columns() {
20906 let max_columns = usize::from(MAX_COLUMN);
20907 let mut engine = VdbeEngine::new(i32::try_from(max_columns + 2).expect("register count"));
20908 let op = track_r_make_record_op(1, max_columns, i32::try_from(max_columns + 1).unwrap());
20909 let expected = (0..max_columns)
20910 .map(|idx| SqliteValue::Integer(i64::try_from(idx).unwrap_or(i64::MAX) - 1_000))
20911 .collect::<Vec<_>>();
20912 let start = Instant::now();
20913
20914 for (offset, value) in expected.iter().enumerate() {
20915 let reg = i32::try_from(offset + 1).expect("register index should fit in i32");
20916 engine.set_reg(reg, value.clone());
20917 }
20918 engine.execute_make_record_hot(&op, false);
20919
20920 let decoded = track_r_decode_make_record_buf(&engine);
20921 track_r_assert_rows_eq(
20922 &decoded,
20923 &expected,
20924 "track_r max-column MakeRecord decode mismatch",
20925 );
20926 track_r_log_metrics(
20927 "test_track_r_make_record_max_columns",
20928 1,
20929 engine.make_record_lookaside.capacity(),
20930 start.elapsed().as_nanos(),
20931 );
20932 }
20933
20934 #[test]
20935 fn test_track_r_make_record_insert_10k_oracle() {
20936 let row_count = 10_000usize;
20937 let expected_rows = (0..row_count)
20938 .map(track_r_generated_record)
20939 .collect::<Vec<_>>();
20940 let mut db = MemDatabase::new();
20941 let root = db.create_table(5);
20942 let start = Instant::now();
20943
20944 let (observed_rows, _) = track_r_run_write_with_memdb(db, |b| {
20945 let end = b.emit_label();
20946 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
20947 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(5), 0);
20948
20949 for (idx, row) in expected_rows.iter().enumerate() {
20950 let rowid = i64::try_from(idx + 1).expect("rowid should fit in i64");
20951 b.emit_op(Opcode::Int64, 0, 1, 0, P4::Int64(rowid), 0);
20952 for (offset, value) in row.iter().enumerate() {
20953 let reg = i32::try_from(offset + 2).expect("register index should fit in i32");
20954 track_r_emit_value(b, reg, value);
20955 }
20956 b.emit_op(Opcode::MakeRecord, 2, 5, 7, P4::None, 0);
20957 b.emit_op(Opcode::Insert, 0, 7, 1, P4::None, 0);
20958 }
20959
20960 let done = b.emit_label();
20961 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
20962 let body = b.emit_label();
20963 b.resolve_label(body);
20964 for column in 0_i32..5 {
20965 let target_reg = column + 1;
20966 b.emit_op(Opcode::Column, 0, column, target_reg, P4::None, 0);
20967 }
20968 b.emit_op(Opcode::ResultRow, 1, 5, 0, P4::None, 0);
20969 b.emit_jump_to_label(Opcode::Next, 0, 0, body, P4::None, 0);
20970
20971 b.resolve_label(done);
20972 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
20973 b.resolve_label(end);
20974 });
20975
20976 assert_eq!(
20977 observed_rows.len(),
20978 row_count,
20979 "MakeRecord INSERT path should persist every generated row",
20980 );
20981
20982 let oracle = rusqlite::Connection::open_in_memory().expect("oracle should open");
20983 oracle
20984 .execute(
20985 "CREATE TABLE t (c0 INTEGER, c1 REAL, c2 TEXT, c3 BLOB, c4 NUMERIC)",
20986 [],
20987 )
20988 .expect("oracle schema should create");
20989 let mut insert = oracle
20990 .prepare("INSERT INTO t (c0, c1, c2, c3, c4) VALUES (?1, ?2, ?3, ?4, ?5)")
20991 .expect("oracle insert should prepare");
20992 for row in &expected_rows {
20993 let params = row
20994 .iter()
20995 .map(track_r_to_rusqlite_value)
20996 .collect::<Vec<_>>();
20997 insert
20998 .execute(params_from_iter(params))
20999 .expect("oracle insert should succeed");
21000 }
21001
21002 let mut query = oracle
21003 .prepare("SELECT c0, c1, c2, c3, c4 FROM t ORDER BY rowid")
21004 .expect("oracle query should prepare");
21005 let oracle_rows = query
21006 .query_map([], |row| {
21007 (0..5)
21008 .map(|idx| {
21009 row.get::<_, RusqliteValue>(idx)
21010 .map(track_r_from_rusqlite_value)
21011 })
21012 .collect::<rusqlite::Result<Vec<_>>>()
21013 })
21014 .expect("oracle query should execute")
21015 .collect::<rusqlite::Result<Vec<_>>>()
21016 .expect("oracle rows should collect");
21017
21018 assert_eq!(
21019 observed_rows.len(),
21020 oracle_rows.len(),
21021 "VDBE and oracle should expose the same row count",
21022 );
21023 for (idx, (observed, oracle_row)) in
21024 observed_rows.iter().zip(oracle_rows.iter()).enumerate()
21025 {
21026 track_r_assert_rows_eq(
21027 observed,
21028 oracle_row,
21029 &format!("track_r 10k insert oracle row {idx} mismatch"),
21030 );
21031 }
21032
21033 track_r_log_metrics(
21034 "test_track_r_make_record_insert_10k_oracle",
21035 1,
21036 0,
21037 start.elapsed().as_nanos(),
21038 );
21039 }
21040
21041 #[test]
21042 fn test_track_r_make_record_roundtrip_10k_records() {
21043 let row_count = 10_000usize;
21044 let mut encoded = Vec::new();
21045 let mut decoded = Vec::new();
21046 let mut alloc_count = 0usize;
21047 let mut encoded_capacity = 0usize;
21048 let start = Instant::now();
21049
21050 for seed in 0..row_count {
21051 let values = track_r_generated_record(seed);
21052 serialize_record_iter_into(values.iter(), &mut encoded);
21053 parse_record_into(&encoded, &mut decoded).expect("roundtrip decode should succeed");
21054 track_r_assert_rows_eq(&decoded, &values, "track_r roundtrip decode mismatch");
21055
21056 let current_capacity = encoded.capacity();
21057 if current_capacity != encoded_capacity {
21058 encoded_capacity = current_capacity;
21059 alloc_count = alloc_count.saturating_add(1);
21060 }
21061 }
21062
21063 track_r_log_metrics(
21064 "test_track_r_make_record_roundtrip_10k_records",
21065 alloc_count,
21066 encoded_capacity.max(encoded.capacity()),
21067 start.elapsed().as_nanos(),
21068 );
21069 }
21070
21071 #[test]
21072 fn test_set_bindings_slice_keeps_small_binding_sets_inline() {
21073 let mut engine = VdbeEngine::new(1);
21074 engine.set_bindings_slice(&[SqliteValue::Integer(7)]);
21075
21076 assert_eq!(engine.bindings.len(), 1);
21077 assert!(
21078 !engine.bindings.spilled(),
21079 "single-parameter statements should keep bindings inline"
21080 );
21081 assert_eq!(engine.bindings[0], SqliteValue::Integer(7));
21082 }
21083
21084 #[test]
21085 fn test_execute_with_borrowed_bindings_uses_override_for_single_run() {
21086 let mut builder = ProgramBuilder::new();
21087 builder.emit_op(Opcode::Variable, 1, 1, 0, P4::None, 0);
21088 builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21089 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21090 let program = builder.finish().expect("program should build");
21091
21092 let mut engine = VdbeEngine::new(program.register_count());
21093 engine.set_bindings_slice(&[SqliteValue::Integer(7)]);
21094
21095 let outcome = run_async(
21096 engine.execute_with_borrowed_bindings(&program, Some(&[SqliteValue::Integer(11)])),
21097 )
21098 .expect("execution should succeed");
21099
21100 assert_eq!(outcome, ExecOutcome::Done);
21101 assert_eq!(
21102 engine.take_results().into_iter().next().unwrap().into_vec(),
21103 vec![SqliteValue::Integer(11)]
21104 );
21105 assert_eq!(
21106 engine.bindings[0],
21107 SqliteValue::Integer(7),
21108 "borrowed bindings should not overwrite the cached owned binding set"
21109 );
21110 }
21111
21112 #[test]
21113 fn test_register_value_i64_store_and_retrieve() {
21114 let mut engine = VdbeEngine::new(2);
21115 engine.set_reg_int(1, -9_876_543_210);
21116
21117 assert_eq!(engine.get_reg(1), &SqliteValue::Integer(-9_876_543_210));
21118 engine.set_register_subtype(1, 74);
21119 engine.set_reg_int(1, 9_876_543_210);
21120
21121 assert_eq!(engine.get_reg(1), &SqliteValue::Integer(9_876_543_210));
21122 assert!(
21123 engine.register_subtype(1).is_none(),
21124 "an in-place integer write must clear stale subtype metadata"
21125 );
21126 assert_eq!(engine.take_reg(1), SqliteValue::Integer(9_876_543_210));
21127 assert_eq!(engine.get_reg(1), &SqliteValue::Null);
21128 }
21129
21130 #[test]
21131 fn test_register_value_f64_store_and_retrieve() {
21132 let mut engine = VdbeEngine::new(2);
21133 engine.set_reg_real(1, 1.25);
21134
21135 assert_eq!(engine.get_reg(1), &SqliteValue::Float(1.25));
21136 engine.set_register_subtype(1, 74);
21137 engine.set_reg_real(1, -0.0);
21138
21139 let SqliteValue::Float(value) = engine.get_reg(1) else {
21140 panic!("register should contain a float");
21141 };
21142 assert_eq!(value.to_bits(), (-0.0_f64).to_bits());
21143 assert!(
21144 engine.register_subtype(1).is_none(),
21145 "an in-place real write must clear stale subtype metadata"
21146 );
21147
21148 engine.set_reg_real(1, f64::NAN);
21149 assert_eq!(engine.get_reg(1), &SqliteValue::Null);
21150 }
21151
21152 #[test]
21153 fn test_register_text_cache_stays_valid_while_storage_cursor_row_is_pinned() {
21154 let _guard = VDBE_OBSERVABILITY_LOCK
21155 .lock()
21156 .unwrap_or_else(|e| e.into_inner());
21157 let prev_metrics_enabled = vdbe_metrics_enabled();
21158 reset_vdbe_metrics();
21159 set_vdbe_metrics_enabled(true);
21160
21161 let mut db = MemDatabase::new();
21162 let root = db.create_table(1);
21163 db.get_table_mut(root)
21164 .expect("table should exist")
21165 .insert(1, vec![SqliteValue::Text("alpha-track-s".into())]);
21166
21167 let mut engine = VdbeEngine::new(2);
21168 engine.collect_vdbe_metrics = true;
21169 engine.enable_storage_cursors(true);
21170 engine.set_database(db);
21171 engine.set_reject_mem_fallback(false);
21172 assert!(run_async(engine.open_storage_cursor(0, root, false)));
21173 engine.cursor_root_pages.insert(0, root);
21174 {
21175 let sc = engine
21176 .storage_cursors
21177 .get_mut(&0)
21178 .expect("storage cursor should exist");
21179 assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21180 }
21181
21182 let before = vdbe_metrics_snapshot();
21183 let first =
21184 run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed");
21185 let second =
21186 run_async(engine.cursor_column(0, 0)).expect("pinned-row cache hit should succeed");
21187 let after = vdbe_metrics_snapshot();
21188 let sc = engine
21189 .storage_cursors
21190 .get(&0)
21191 .expect("storage cursor should exist");
21192
21193 assert_eq!(first, SqliteValue::Text("alpha-track-s".into()));
21194 assert_eq!(second, first);
21195 assert!(sc.row_decode.cached_value_ready(0));
21196 assert_eq!(
21197 sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21198 Some("alpha-track-s")
21199 );
21200 assert_eq!(
21201 after.decode_cache_misses_total - before.decode_cache_misses_total,
21202 1
21203 );
21204 assert_eq!(
21205 after.decode_cache_hits_total - before.decode_cache_hits_total,
21206 1
21207 );
21208 assert_eq!(
21209 after.decode_cache_invalidations_position_total
21210 - before.decode_cache_invalidations_position_total,
21211 0
21212 );
21213 assert_eq!(
21214 after.decode_cache_invalidations_write_total
21215 - before.decode_cache_invalidations_write_total,
21216 0
21217 );
21218
21219 set_vdbe_metrics_enabled(prev_metrics_enabled);
21220 }
21221
21222 #[test]
21223 fn test_register_value_make_record_sideband_materializes_into_sqlite_blob_value() {
21224 let _guard = VDBE_OBSERVABILITY_LOCK
21225 .lock()
21226 .unwrap_or_else(|e| e.into_inner());
21227 reset_vdbe_test_sideband_materialization_count();
21228
21229 let mut engine = VdbeEngine::new(4);
21230 engine.set_reg_int(1, 7);
21231 engine.set_reg(2, SqliteValue::Text("payload-track-s".into()));
21232
21233 let make_record = VdbeOp {
21234 opcode: Opcode::MakeRecord,
21235 p1: 1,
21236 p2: 2,
21237 p3: 3,
21238 p4: P4::None,
21239 p5: 0,
21240 };
21241 engine.execute_make_record_hot(&make_record, false);
21242
21243 assert_eq!(
21244 engine.get_reg(3),
21245 &SqliteValue::Null,
21246 "MakeRecord keeps bytes in sideband until a consumer asks for an owned SqliteValue"
21247 );
21248
21249 let before = vdbe_test_sideband_materialization_count_snapshot();
21250 let converted = engine.clone_reg_materialized(3);
21251 let after = vdbe_test_sideband_materialization_count_snapshot();
21252
21253 assert_eq!(after - before, 1);
21254 assert_eq!(
21255 decode_record(&converted).expect("sideband value should decode"),
21256 vec![
21257 SqliteValue::Integer(7),
21258 SqliteValue::Text("payload-track-s".into()),
21259 ]
21260 );
21261 assert_eq!(
21262 decode_record(engine.get_reg(3)).expect("register should now hold the blob"),
21263 vec![
21264 SqliteValue::Integer(7),
21265 SqliteValue::Text("payload-track-s".into()),
21266 ]
21267 );
21268 }
21269
21270 #[test]
21271 fn test_copy_materializes_make_record_sideband_before_copying() {
21272 let _guard = VDBE_OBSERVABILITY_LOCK
21273 .lock()
21274 .unwrap_or_else(|e| e.into_inner());
21275 reset_vdbe_test_sideband_materialization_count();
21276
21277 let rows = run_program(|b| {
21278 let end = b.emit_label();
21279 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21280 b.emit_op(Opcode::Integer, 33, 1, 0, P4::None, 0);
21281 b.emit_op(
21282 Opcode::String8,
21283 0,
21284 2,
21285 0,
21286 P4::Str("copy-sideband".to_owned()),
21287 0,
21288 );
21289 b.emit_op(Opcode::MakeRecord, 1, 2, 3, P4::None, 0);
21290 b.emit_op(Opcode::Copy, 3, 4, 0, P4::None, 0);
21291 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
21292 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21293 b.resolve_label(end);
21294 });
21295
21296 assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 1);
21297 assert_eq!(
21298 decode_record(&rows[0][0]).expect("copied record blob should decode"),
21299 vec![
21300 SqliteValue::Integer(33),
21301 SqliteValue::Text("copy-sideband".into()),
21302 ]
21303 );
21304 }
21305
21306 #[test]
21307 fn test_scopy_materializes_make_record_sideband_before_copying() {
21308 let _guard = VDBE_OBSERVABILITY_LOCK
21309 .lock()
21310 .unwrap_or_else(|e| e.into_inner());
21311 reset_vdbe_test_sideband_materialization_count();
21312
21313 let rows = run_program(|b| {
21314 let end = b.emit_label();
21315 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21316 b.emit_op(Opcode::Integer, 44, 1, 0, P4::None, 0);
21317 b.emit_op(
21318 Opcode::String8,
21319 0,
21320 2,
21321 0,
21322 P4::Str("scopy-sideband".to_owned()),
21323 0,
21324 );
21325 b.emit_op(Opcode::MakeRecord, 1, 2, 3, P4::None, 0);
21326 b.emit_op(Opcode::SCopy, 3, 4, 0, P4::None, 0);
21327 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
21328 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21329 b.resolve_label(end);
21330 });
21331
21332 assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 1);
21333 assert_eq!(
21334 decode_record(&rows[0][0]).expect("shallow-copied record blob should decode"),
21335 vec![
21336 SqliteValue::Integer(44),
21337 SqliteValue::Text("scopy-sideband".into()),
21338 ]
21339 );
21340 }
21341
21342 #[test]
21343 fn test_second_make_record_preserves_unconsumed_prior_sideband() {
21344 let _guard = VDBE_OBSERVABILITY_LOCK
21345 .lock()
21346 .unwrap_or_else(|e| e.into_inner());
21347 reset_vdbe_test_sideband_materialization_count();
21348
21349 let rows = run_program(|b| {
21350 let end = b.emit_label();
21351 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21352 let r_first_value = b.alloc_reg();
21353 let r_first_record = b.alloc_reg();
21354 let r_second_value = b.alloc_reg();
21355 let r_second_record = b.alloc_reg();
21356 b.emit_op(Opcode::Integer, 11, r_first_value, 0, P4::None, 0);
21357 b.emit_op(
21358 Opcode::MakeRecord,
21359 r_first_value,
21360 1,
21361 r_first_record,
21362 P4::None,
21363 0,
21364 );
21365 b.emit_op(Opcode::Integer, 22, r_second_value, 0, P4::None, 0);
21366 b.emit_op(
21367 Opcode::MakeRecord,
21368 r_second_value,
21369 1,
21370 r_second_record,
21371 P4::None,
21372 0,
21373 );
21374 b.emit_op(Opcode::ResultRow, r_first_record, 1, 0, P4::None, 0);
21375 b.emit_op(Opcode::ResultRow, r_second_record, 1, 0, P4::None, 0);
21376 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21377 b.resolve_label(end);
21378 });
21379
21380 assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 2);
21381 assert_eq!(
21382 decode_record(&rows[0][0]).expect("first pending record should decode"),
21383 vec![SqliteValue::Integer(11)]
21384 );
21385 assert_eq!(
21386 decode_record(&rows[1][0]).expect("second pending record should decode"),
21387 vec![SqliteValue::Integer(22)]
21388 );
21389 }
21390
21391 #[test]
21392 fn test_make_record_materializes_sideband_when_source_overlaps_target() {
21393 let _guard = VDBE_OBSERVABILITY_LOCK
21394 .lock()
21395 .unwrap_or_else(|e| e.into_inner());
21396 reset_vdbe_test_sideband_materialization_count();
21397
21398 let rows = run_program(|b| {
21399 let end = b.emit_label();
21400 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21401 let r_value = b.alloc_reg();
21402 let r_record = b.alloc_reg();
21403 b.emit_op(Opcode::Integer, 33, r_value, 0, P4::None, 0);
21404 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
21405 b.emit_op(Opcode::MakeRecord, r_record, 1, r_record, P4::None, 0);
21406 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
21407 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21408 b.resolve_label(end);
21409 });
21410
21411 assert_eq!(vdbe_test_sideband_materialization_count_snapshot(), 2);
21412 let outer = decode_record(&rows[0][0]).expect("outer record should decode");
21413 assert_eq!(outer.len(), 1);
21414 let inner = decode_record(&outer[0]).expect("nested record should decode");
21415 assert_eq!(inner, vec![SqliteValue::Integer(33)]);
21416 }
21417
21418 #[test]
21419 fn test_copy_range_uses_source_snapshot_when_ranges_overlap() {
21420 let rows = run_program(|b| {
21421 let end = b.emit_label();
21422 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21423 b.emit_op(Opcode::Integer, 10, 1, 0, P4::None, 0);
21424 b.emit_op(Opcode::Integer, 20, 2, 0, P4::None, 0);
21425 b.emit_op(Opcode::Integer, 30, 3, 0, P4::None, 0);
21426 b.emit_op(Opcode::Copy, 1, 2, 2, P4::None, 0);
21427 b.emit_op(Opcode::ResultRow, 1, 4, 0, P4::None, 0);
21428 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21429 b.resolve_label(end);
21430 });
21431
21432 assert_eq!(
21433 rows[0],
21434 vec![
21435 SqliteValue::Integer(10),
21436 SqliteValue::Integer(10),
21437 SqliteValue::Integer(20),
21438 SqliteValue::Integer(30),
21439 ]
21440 );
21441 }
21442
21443 #[test]
21444 fn test_register_value_take_reg_materializes_sideband_once_and_clears_slot() {
21445 let _guard = VDBE_OBSERVABILITY_LOCK
21446 .lock()
21447 .unwrap_or_else(|e| e.into_inner());
21448 reset_vdbe_test_sideband_materialization_count();
21449
21450 let mut engine = VdbeEngine::new(4);
21451 engine.set_reg_int(1, 17);
21452 engine.set_reg(2, SqliteValue::Text("take-track-s".into()));
21453
21454 let make_record = VdbeOp {
21455 opcode: Opcode::MakeRecord,
21456 p1: 1,
21457 p2: 2,
21458 p3: 3,
21459 p4: P4::None,
21460 p5: 0,
21461 };
21462 engine.execute_make_record_hot(&make_record, false);
21463
21464 let before = vdbe_test_sideband_materialization_count_snapshot();
21465 let taken = engine.take_reg(3);
21466 let after = vdbe_test_sideband_materialization_count_snapshot();
21467
21468 assert_eq!(
21469 after - before,
21470 1,
21471 "taking a sideband-backed register should materialize it exactly once"
21472 );
21473 assert_eq!(
21474 decode_record(&taken).expect("taken register value should decode"),
21475 vec![
21476 SqliteValue::Integer(17),
21477 SqliteValue::Text("take-track-s".into()),
21478 ]
21479 );
21480 assert_eq!(
21481 engine.get_reg(3),
21482 &SqliteValue::Null,
21483 "take_reg must clear the register slot after moving the value out"
21484 );
21485 assert_eq!(
21486 engine.take_reg(3),
21487 SqliteValue::Null,
21488 "subsequent reads after take_reg should observe the cleared slot"
21489 );
21490 assert_eq!(
21491 vdbe_test_sideband_materialization_count_snapshot(),
21492 after,
21493 "cleared register should not trigger a second sideband materialization"
21494 );
21495 }
21496
21497 #[test]
21498 fn test_register_value_insert_avoids_make_record_blob_materialization() {
21499 let _guard = VDBE_OBSERVABILITY_LOCK
21500 .lock()
21501 .unwrap_or_else(|e| e.into_inner());
21502 reset_vdbe_test_sideband_materialization_count();
21503
21504 let mut db = MemDatabase::new();
21505 let root = db.create_table(2);
21506 let before = vdbe_test_sideband_materialization_count_snapshot();
21507 let (rows, _) = run_write_with_storage_cursors(db, |b| {
21508 let end = b.emit_label();
21509 let done = b.emit_label();
21510 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
21511 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
21512 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
21513 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
21514 b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("alpha".to_owned()), 0);
21515 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
21516 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
21517 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
21518
21519 let body = b.current_addr();
21520 b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
21521 b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
21522 b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
21523
21524 let next_target =
21525 i32::try_from(body).expect("program counter should fit into i32 for tests");
21526 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
21527 b.resolve_label(done);
21528 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21529 b.resolve_label(end);
21530 });
21531 let after = vdbe_test_sideband_materialization_count_snapshot();
21532
21533 assert_eq!(
21534 rows,
21535 vec![vec![
21536 SqliteValue::Integer(42),
21537 SqliteValue::Text("alpha".into()),
21538 ]]
21539 );
21540 assert_eq!(
21541 after - before,
21542 0,
21543 "INSERT should consume MakeRecord sideband bytes directly instead of materializing an Arc-backed record blob"
21544 );
21545 }
21546
21547 #[test]
21548 fn test_register_value_cursor_move_invalidates_cached_text() {
21549 let _guard = VDBE_OBSERVABILITY_LOCK
21550 .lock()
21551 .unwrap_or_else(|e| e.into_inner());
21552 let prev_metrics_enabled = vdbe_metrics_enabled();
21553 reset_vdbe_metrics();
21554 set_vdbe_metrics_enabled(true);
21555
21556 let mut db = MemDatabase::new();
21557 let root = db.create_table(1);
21558 let table = db.get_table_mut(root).expect("table should exist");
21559 table.insert(1, vec![SqliteValue::Text("alpha-track-s".into())]);
21560 table.insert(2, vec![SqliteValue::Text("beta-track-s".into())]);
21561
21562 let mut engine = VdbeEngine::new(2);
21563 engine.collect_vdbe_metrics = true;
21564 engine.enable_storage_cursors(true);
21565 engine.set_database(db);
21566 engine.set_reject_mem_fallback(false);
21567 assert!(run_async(engine.open_storage_cursor(0, root, false)));
21568 engine.cursor_root_pages.insert(0, root);
21569 {
21570 let sc = engine
21571 .storage_cursors
21572 .get_mut(&0)
21573 .expect("storage cursor should exist");
21574 assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21575 }
21576
21577 let before = vdbe_metrics_snapshot();
21578 assert_eq!(
21579 run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed"),
21580 SqliteValue::Text("alpha-track-s".into())
21581 );
21582 {
21583 let sc = engine
21584 .storage_cursors
21585 .get_mut(&0)
21586 .expect("storage cursor should exist");
21587 assert!(run_async(sc.cursor.next(&sc.cx)).expect("cursor should advance"));
21588 }
21589 assert_eq!(
21590 run_async(engine.cursor_column(0, 0)).expect("post-move text decode should succeed"),
21591 SqliteValue::Text("beta-track-s".into())
21592 );
21593 let after = vdbe_metrics_snapshot();
21594 let sc = engine
21595 .storage_cursors
21596 .get(&0)
21597 .expect("storage cursor should exist");
21598
21599 assert!(sc.row_decode.cached_value_ready(0));
21600 assert_eq!(
21601 sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21602 Some("beta-track-s")
21603 );
21604 assert_eq!(
21605 after.decode_cache_misses_total - before.decode_cache_misses_total,
21606 2
21607 );
21608 assert_eq!(
21609 after.decode_cache_hits_total - before.decode_cache_hits_total,
21610 0
21611 );
21612 assert_eq!(
21613 after.decode_cache_invalidations_position_total
21614 - before.decode_cache_invalidations_position_total,
21615 1
21616 );
21617 assert_eq!(
21618 after.decode_cache_invalidations_write_total
21619 - before.decode_cache_invalidations_write_total,
21620 0
21621 );
21622 assert_eq!(
21623 after.decode_cache_invalidations_pseudo_total
21624 - before.decode_cache_invalidations_pseudo_total,
21625 0
21626 );
21627
21628 set_vdbe_metrics_enabled(prev_metrics_enabled);
21629 }
21630
21631 #[test]
21632 fn test_storage_cursor_same_slot_write_mutation_invalidates_cached_text() {
21633 let _guard = VDBE_OBSERVABILITY_LOCK
21634 .lock()
21635 .unwrap_or_else(|e| e.into_inner());
21636 let prev_metrics_enabled = vdbe_metrics_enabled();
21637 reset_vdbe_metrics();
21638 set_vdbe_metrics_enabled(true);
21639
21640 let mut db = MemDatabase::new();
21641 let root = db.create_table(1);
21642 let table = db.get_table_mut(root).expect("table should exist");
21643 table.insert(1, vec![SqliteValue::Text("alpha-track-j".into())]);
21644 table.insert(2, vec![SqliteValue::Text("beta-track-j".into())]);
21645
21646 let mut engine = VdbeEngine::new(2);
21647 engine.collect_vdbe_metrics = true;
21648 engine.enable_storage_cursors(true);
21649 engine.set_database(db);
21650 engine.set_reject_mem_fallback(false);
21651 assert!(run_async(engine.open_storage_cursor(0, root, true)));
21652 engine.cursor_root_pages.insert(0, root);
21653 {
21654 let sc = engine
21655 .storage_cursors
21656 .get_mut(&0)
21657 .expect("storage cursor should exist");
21658 assert!(run_async(sc.cursor.first(&sc.cx)).expect("cursor should rewind"));
21659 }
21660
21661 let before = vdbe_metrics_snapshot();
21662 assert_eq!(
21663 run_async(engine.cursor_column(0, 0)).expect("first text decode should succeed"),
21664 SqliteValue::Text("alpha-track-j".into())
21665 );
21666 {
21667 let sc = engine
21668 .storage_cursors
21669 .get_mut(&0)
21670 .expect("storage cursor should exist");
21671 run_async(sc.cursor.delete(&sc.cx))
21672 .expect("direct delete should succeed without helper invalidation");
21673 }
21674 assert_eq!(
21675 run_async(engine.cursor_column(0, 0))
21676 .expect("same-slot successor should force a fresh decode"),
21677 SqliteValue::Text("beta-track-j".into())
21678 );
21679 let after = vdbe_metrics_snapshot();
21680 let sc = engine
21681 .storage_cursors
21682 .get(&0)
21683 .expect("storage cursor should exist");
21684
21685 assert!(sc.row_decode.cached_value_ready(0));
21686 assert_eq!(
21687 sc.row_decode.cached_value(0).and_then(SqliteValue::as_text),
21688 Some("beta-track-j")
21689 );
21690 assert_eq!(
21691 after.decode_cache_invalidations_write_total
21692 - before.decode_cache_invalidations_write_total,
21693 1
21694 );
21695 assert_eq!(
21696 after.decode_cache_invalidations_position_total
21697 - before.decode_cache_invalidations_position_total,
21698 0
21699 );
21700 assert_eq!(
21701 after.decode_cache_hits_total - before.decode_cache_hits_total,
21702 0
21703 );
21704 assert_eq!(
21705 after.decode_cache_misses_total - before.decode_cache_misses_total,
21706 2
21707 );
21708
21709 set_vdbe_metrics_enabled(prev_metrics_enabled);
21710 }
21711
21712 #[test]
21713 fn test_execute_honors_cancelled_execution_context() {
21714 let mut builder = ProgramBuilder::new();
21715 for _ in 0..=VDBE_EXECUTION_CHECKPOINT_INTERVAL {
21716 builder.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
21717 }
21718 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21719 let program = builder.finish().expect("program should build");
21720
21721 let cx = Cx::new();
21722 cx.transition_to_running();
21723 cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
21724
21725 let mut engine =
21726 VdbeEngine::new_with_execution_cx(program.register_count(), &cx, PageSize::DEFAULT);
21727 let err = run_async(engine.execute(&program)).unwrap_err();
21728 assert!(matches!(err, FrankenError::Abort));
21729 assert!(
21730 engine.results().is_empty(),
21731 "cancelled execute should not emit rows"
21732 );
21733 }
21734
21735 #[test]
21736 fn test_execute_reuse_clears_prior_results() {
21737 let mut first_builder = ProgramBuilder::new();
21738 first_builder.emit_op(Opcode::Integer, 11, 1, 0, P4::None, 0);
21739 first_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21740 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21741 let first_program = first_builder.finish().expect("first program should build");
21742
21743 let mut second_builder = ProgramBuilder::new();
21744 second_builder.emit_op(Opcode::Integer, 22, 1, 0, P4::None, 0);
21745 second_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21746 second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21747 let second_program = second_builder
21748 .finish()
21749 .expect("second program should build");
21750
21751 let mut engine = VdbeEngine::new(
21752 first_program
21753 .register_count()
21754 .max(second_program.register_count()),
21755 );
21756 assert_eq!(
21757 run_async(engine.execute(&first_program)).expect("first execution"),
21758 ExecOutcome::Done
21759 );
21760 assert_eq!(
21761 run_async(engine.execute(&second_program)).expect("second execution"),
21762 ExecOutcome::Done
21763 );
21764
21765 let results = engine
21766 .results()
21767 .iter()
21768 .map(|row| row.clone().into_vec())
21769 .collect::<Vec<_>>();
21770 assert_eq!(results, vec![vec![SqliteValue::Integer(22)]]);
21771 }
21772
21773 #[test]
21774 fn test_execute_reuse_resets_statement_accounting() {
21775 let mut db = MemDatabase::new();
21776 let root = db.create_table(1);
21777
21778 let mut insert_builder = ProgramBuilder::new();
21779 let insert_end = insert_builder.emit_label();
21780 insert_builder.emit_jump_to_label(Opcode::Init, 0, 0, insert_end, P4::None, 0);
21781 insert_builder.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
21782 insert_builder.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
21783 insert_builder.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
21784 insert_builder.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
21785 insert_builder.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
21786 insert_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21787 insert_builder.resolve_label(insert_end);
21788 let insert_program = insert_builder
21789 .finish()
21790 .expect("insert program should build");
21791
21792 let mut noop_builder = ProgramBuilder::new();
21793 noop_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21794 let noop_program = noop_builder.finish().expect("noop program should build");
21795
21796 let mut engine = VdbeEngine::new(
21797 insert_program
21798 .register_count()
21799 .max(noop_program.register_count()),
21800 );
21801 engine.set_database(db);
21802 engine.set_reject_mem_fallback(false);
21803
21804 assert_eq!(
21805 run_async(engine.execute(&insert_program)).expect("insert execution"),
21806 ExecOutcome::Done
21807 );
21808 assert_eq!(engine.changes(), 1);
21809 assert_eq!(engine.last_insert_rowid(), Some(1));
21810
21811 assert_eq!(
21812 run_async(engine.execute(&noop_program)).expect("noop execution"),
21813 ExecOutcome::Done
21814 );
21815 assert_eq!(engine.changes(), 0);
21816 assert_eq!(engine.last_insert_rowid(), None);
21817 }
21818
21819 #[test]
21820 fn test_execute_reuse_empty_program_clears_prior_statement_state() {
21821 let mut first_builder = ProgramBuilder::new();
21822 first_builder.emit_op(Opcode::Integer, 11, 1, 0, P4::None, 0);
21823 first_builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
21824 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21825 let first_program = first_builder.finish().expect("first program should build");
21826
21827 let empty_program = ProgramBuilder::new()
21828 .finish()
21829 .expect("empty program should build");
21830
21831 let mut engine = VdbeEngine::new(
21832 first_program
21833 .register_count()
21834 .max(empty_program.register_count()),
21835 );
21836 assert_eq!(
21837 run_async(engine.execute(&first_program)).expect("first execution"),
21838 ExecOutcome::Done
21839 );
21840 assert_eq!(engine.results().len(), 1);
21841
21842 assert_eq!(
21843 run_async(engine.execute(&empty_program)).expect("empty execution"),
21844 ExecOutcome::Done
21845 );
21846 assert!(engine.results().is_empty());
21847 assert_eq!(engine.changes(), 0);
21848 assert_eq!(engine.last_insert_rowid(), None);
21849 }
21850
21851 #[test]
21852 fn test_reset_for_reuse_keeps_cached_engine_results_clean() {
21853 let mut first_builder = ProgramBuilder::new();
21854 let first_reg = first_builder.alloc_reg();
21855 first_builder.emit_op(Opcode::Integer, 11, first_reg, 0, P4::None, 0);
21856 first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21857 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21858 let first_program = first_builder.finish().expect("first program should build");
21859
21860 let mut second_builder = ProgramBuilder::new();
21861 let second_reg = second_builder.alloc_reg();
21862 second_builder.emit_op(Opcode::Integer, 22, second_reg, 0, P4::None, 0);
21863 second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21864 second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21865 let second_program = second_builder
21866 .finish()
21867 .expect("second program should build");
21868
21869 let mut engine = VdbeEngine::new(
21870 first_program
21871 .register_count()
21872 .max(second_program.register_count()),
21873 );
21874 assert_eq!(
21875 run_async(engine.execute(&first_program)).expect("first execution"),
21876 ExecOutcome::Done
21877 );
21878
21879 let reset_cx = Cx::new();
21880 engine.reset_for_reuse(
21881 first_program
21882 .register_count()
21883 .max(second_program.register_count()),
21884 &reset_cx,
21885 PageSize::DEFAULT,
21886 );
21887
21888 assert_eq!(
21889 run_async(engine.execute(&second_program)).expect("second execution"),
21890 ExecOutcome::Done
21891 );
21892 assert_eq!(
21893 engine
21894 .results()
21895 .iter()
21896 .map(|row| row.clone().into_vec())
21897 .collect::<Vec<_>>(),
21898 vec![vec![SqliteValue::Integer(22)]]
21899 );
21900 }
21901
21902 #[test]
21903 fn test_reset_for_reuse_preserves_engine_trace_setting() {
21904 let reset_cx = Cx::new();
21905 let mut engine = VdbeEngine::new_with_execution_cx(4, &reset_cx, PageSize::DEFAULT);
21906
21907 engine.trace_opcodes = false;
21908 engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
21909 assert!(!engine.trace_opcodes);
21910
21911 engine.trace_opcodes = true;
21912 engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
21913 assert!(engine.trace_opcodes);
21914 }
21915
21916 #[test]
21917 fn test_reset_for_reuse_clears_result_row_collection_cap() {
21918 let mut first_builder = ProgramBuilder::new();
21919 let first_reg = first_builder.alloc_reg();
21920 first_builder.emit_op(Opcode::Integer, 11, first_reg, 0, P4::None, 0);
21921 first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21922 first_builder.emit_op(Opcode::Integer, 12, first_reg, 0, P4::None, 0);
21923 first_builder.emit_op(Opcode::ResultRow, first_reg, 1, 0, P4::None, 0);
21924 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21925 let first_program = first_builder.finish().expect("first program should build");
21926
21927 let mut second_builder = ProgramBuilder::new();
21928 let second_reg = second_builder.alloc_reg();
21929 second_builder.emit_op(Opcode::Integer, 21, second_reg, 0, P4::None, 0);
21930 second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21931 second_builder.emit_op(Opcode::Integer, 22, second_reg, 0, P4::None, 0);
21932 second_builder.emit_op(Opcode::ResultRow, second_reg, 1, 0, P4::None, 0);
21933 second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
21934 let second_program = second_builder
21935 .finish()
21936 .expect("second program should build");
21937
21938 let mut engine = VdbeEngine::new(
21939 first_program
21940 .register_count()
21941 .max(second_program.register_count()),
21942 );
21943 engine.set_max_collected_result_rows(Some(1));
21944 assert_eq!(
21945 run_async(engine.execute(&first_program)).expect("first execution"),
21946 ExecOutcome::Done
21947 );
21948 assert_eq!(engine.results().len(), 1);
21949
21950 let reset_cx = Cx::new();
21951 engine.reset_for_reuse(
21952 first_program
21953 .register_count()
21954 .max(second_program.register_count()),
21955 &reset_cx,
21956 PageSize::DEFAULT,
21957 );
21958
21959 assert_eq!(
21960 run_async(engine.execute(&second_program)).expect("second execution"),
21961 ExecOutcome::Done
21962 );
21963 assert_eq!(
21964 engine
21965 .results()
21966 .iter()
21967 .map(|row| row.clone().into_vec())
21968 .collect::<Vec<_>>(),
21969 vec![
21970 vec![SqliteValue::Integer(21)],
21971 vec![SqliteValue::Integer(22)],
21972 ],
21973 "reset_for_reuse should restore uncapped row collection"
21974 );
21975 }
21976
21977 #[test]
21978 fn test_reusable_table_execution_state_skips_identical_rebinds_after_preserving_reset() {
21979 let mut engine = VdbeEngine::new(4);
21980 let func_registry = Arc::new(FunctionRegistry::new());
21981 let collation_registry = Arc::new(std::sync::Mutex::new(CollationRegistry::new()));
21982 let state = ReusableTableExecutionState {
21983 func_registry: Arc::clone(&func_registry),
21984 collation_registry: Arc::clone(&collation_registry),
21985 schema_cookie: 7,
21986 autoincrement_seq_by_root_page: HashMap::from([(5, 11)]),
21987 rowid_alias_col_by_root_page: Arc::new(HashMap::from([(5, 0)])),
21988 table_column_count_by_root_page: Arc::new(HashMap::from([(5, 2)])),
21989 first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::from([(5, 1)])),
21990 column_defaults_by_root_page: Arc::new(HashMap::new()),
21991 index_desc_flags_by_root_page: Arc::new(HashMap::from([(9, vec![false, true])])),
21992 index_collations_by_root_page: Arc::new(HashMap::from([(
21993 9,
21994 vec![Some("BINARY".to_owned()), None],
21995 )])),
21996 reject_mem_fallback: false,
21997 memdb_rows_loaded: true,
21998 storage_cursor_memdb_count_shortcuts_safe: true,
21999 version_store: None,
22000 collect_result_rows: false,
22001 max_collected_result_rows: Some(1),
22002 };
22003
22004 let first_outcome = engine.apply_reusable_table_execution_state(state.clone());
22005 assert!(
22006 first_outcome.metadata_rebind_count > 0,
22007 "fresh engines should bind the reusable execution state"
22008 );
22009
22010 let reset_cx = Cx::new();
22011 engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22012 let second_outcome = engine.apply_reusable_table_execution_state(state.clone());
22013 assert_eq!(
22014 second_outcome.metadata_rebind_count, 0,
22015 "preserving reset should let identical reusable table state skip rebinding"
22016 );
22017
22018 engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
22019 let third_outcome = engine.apply_reusable_table_execution_state(state);
22020 assert!(
22021 third_outcome.metadata_rebind_count > 0,
22022 "legacy reset_for_reuse should still clear runtime bindings"
22023 );
22024 }
22025
22026 #[test]
22027 fn test_w1_function_cache_preserved_when_func_registry_unchanged() {
22028 let mut engine = VdbeEngine::new(4);
22032 let func_registry = Arc::new(FunctionRegistry::new());
22033 let collation_registry = Arc::new(std::sync::Mutex::new(CollationRegistry::new()));
22034 let state = ReusableTableExecutionState {
22035 func_registry: Arc::clone(&func_registry),
22036 collation_registry: Arc::clone(&collation_registry),
22037 schema_cookie: 1,
22038 autoincrement_seq_by_root_page: HashMap::new(),
22039 rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
22040 table_column_count_by_root_page: Arc::new(HashMap::new()),
22041 first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
22042 column_defaults_by_root_page: Arc::new(HashMap::new()),
22043 index_desc_flags_by_root_page: Arc::new(HashMap::new()),
22044 index_collations_by_root_page: Arc::new(HashMap::new()),
22045 reject_mem_fallback: false,
22046 memdb_rows_loaded: false,
22047 storage_cursor_memdb_count_shortcuts_safe: false,
22048 version_store: None,
22049 collect_result_rows: true,
22050 max_collected_result_rows: None,
22051 };
22052
22053 let first_outcome = engine.apply_reusable_table_execution_state(state.clone());
22055 assert!(
22056 first_outcome.function_cache_cleared,
22057 "first apply should clear function caches (func_registry was None)"
22058 );
22059
22060 let reset_cx = Cx::new();
22062 engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22063 let second_outcome = engine.apply_reusable_table_execution_state(state.clone());
22064 assert!(
22065 !second_outcome.function_cache_cleared,
22066 "W1: function caches should NOT be cleared when func_registry is unchanged"
22067 );
22068
22069 let new_func_registry = Arc::new(FunctionRegistry::new());
22071 let changed_state = ReusableTableExecutionState {
22072 func_registry: Arc::clone(&new_func_registry),
22073 ..state
22074 };
22075 engine.reset_for_reuse_preserving_runtime_setup(4, &reset_cx, PageSize::DEFAULT, false);
22076 let third_outcome = engine.apply_reusable_table_execution_state(changed_state);
22077 assert!(
22078 third_outcome.function_cache_cleared,
22079 "function caches should be cleared when func_registry changes"
22080 );
22081
22082 engine.reset_for_reuse(4, &reset_cx, PageSize::DEFAULT);
22084 let fourth_outcome =
22085 engine.apply_reusable_table_execution_state(ReusableTableExecutionState {
22086 func_registry: Arc::clone(&new_func_registry),
22087 collation_registry: Arc::clone(&collation_registry),
22088 schema_cookie: 1,
22089 autoincrement_seq_by_root_page: HashMap::new(),
22090 rowid_alias_col_by_root_page: Arc::new(HashMap::new()),
22091 table_column_count_by_root_page: Arc::new(HashMap::new()),
22092 first_not_null_non_ipk_col_by_root_page: Arc::new(HashMap::new()),
22093 column_defaults_by_root_page: Arc::new(HashMap::new()),
22094 index_desc_flags_by_root_page: Arc::new(HashMap::new()),
22095 index_collations_by_root_page: Arc::new(HashMap::new()),
22096 reject_mem_fallback: false,
22097 memdb_rows_loaded: false,
22098 storage_cursor_memdb_count_shortcuts_safe: false,
22099 version_store: None,
22100 collect_result_rows: true,
22101 max_collected_result_rows: None,
22102 });
22103 assert!(
22104 fourth_outcome.function_cache_cleared,
22105 "legacy reset_for_reuse clears caches, so next apply should clear them again"
22106 );
22107 }
22108
22109 #[test]
22110 fn test_execute_clears_cold_subtype_state_between_statements() {
22111 let mut subtype_builder = ProgramBuilder::new();
22112 let subtype_reg = subtype_builder.alloc_reg();
22113 let tagged_value_reg = subtype_builder.alloc_reg();
22114 subtype_builder.emit_op(Opcode::Integer, 74, subtype_reg, 0, P4::None, 0);
22115 subtype_builder.emit_op(
22116 Opcode::SetSubtype,
22117 subtype_reg,
22118 tagged_value_reg,
22119 0,
22120 P4::None,
22121 0,
22122 );
22123 subtype_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22124 let subtype_program = subtype_builder
22125 .finish()
22126 .expect("subtype program should build");
22127
22128 let mut probe_builder = ProgramBuilder::new();
22129 let probe_result_reg = probe_builder.alloc_reg();
22130 let probe_value_reg = probe_builder.alloc_reg();
22131 probe_builder.emit_op(
22132 Opcode::GetSubtype,
22133 probe_value_reg,
22134 probe_result_reg,
22135 0,
22136 P4::None,
22137 0,
22138 );
22139 probe_builder.emit_op(Opcode::ResultRow, probe_result_reg, 1, 0, P4::None, 0);
22140 probe_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22141 let probe_program = probe_builder.finish().expect("probe program should build");
22142
22143 let mut engine = VdbeEngine::new(
22144 subtype_program
22145 .register_count()
22146 .max(probe_program.register_count()),
22147 );
22148 assert_eq!(
22149 run_async(engine.execute(&subtype_program)).expect("subtype execution"),
22150 ExecOutcome::Done
22151 );
22152 assert_eq!(engine.register_subtype(tagged_value_reg), Some(74));
22153 assert!(
22154 engine
22155 .statement_cold_state
22156 .contains(StatementColdState::REGISTER_SUBTYPES)
22157 );
22158
22159 assert_eq!(
22160 run_async(engine.execute(&probe_program)).expect("probe execution"),
22161 ExecOutcome::Done
22162 );
22163 assert!(engine.register_subtype(tagged_value_reg).is_none());
22164 assert!(engine.statement_cold_state.is_empty());
22165 assert_eq!(
22166 engine
22167 .results()
22168 .iter()
22169 .map(|row| row.clone().into_vec())
22170 .collect::<Vec<_>>(),
22171 vec![vec![SqliteValue::Integer(0)]]
22172 );
22173 }
22174
22175 #[test]
22176 fn test_secondary_index_rollback_removes_tracked_replace_entry() {
22177 let mut db = MemDatabase::new();
22178 let table_root = db.create_table(1);
22179 let index_root = db.allocate_root_page();
22180
22181 let mut engine = VdbeEngine::new(8);
22182 engine.enable_storage_cursors(true);
22183 engine.set_database(db);
22184 engine.set_reject_mem_fallback(false);
22185
22186 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
22187 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
22188
22189 let payload = encode_record(&[SqliteValue::Integer(99)]);
22190 {
22191 let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
22192 run_async(
22193 table_cursor
22194 .cursor
22195 .table_insert(&table_cursor.cx, 2, &payload),
22196 )
22197 .expect("provisional table row should insert");
22198 assert!(
22199 run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 2),)
22200 .expect("table seek should succeed")
22201 .is_found()
22202 );
22203 }
22204
22205 let index_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(2)]);
22206 {
22207 let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
22208 run_async(
22209 index_cursor
22210 .cursor
22211 .index_insert(&index_cursor.cx, &index_key),
22212 )
22213 .expect("index entry should insert");
22214 assert!(
22215 run_async(
22216 index_cursor
22217 .cursor
22218 .index_move_to(&index_cursor.cx, &index_key),
22219 )
22220 .expect("index seek should succeed")
22221 .is_found()
22222 );
22223 }
22224
22225 engine.changes = 1;
22226 engine.set_pending_insert_rollback(Some(PendingInsertRollback {
22227 cursor_id: 0,
22228 rowid: 2,
22229 previous_last_insert_rowid: 0,
22230 previous_last_insert_rowid_valid: false,
22231 update_restore: None,
22232 }));
22233 engine.push_pending_idx_entry(1, index_key.clone());
22234
22235 run_async(engine.rollback_pending_insert_after_index_conflict(true))
22236 .expect("rollback should remove provisional row and tracked index entries");
22237
22238 let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
22239 assert!(
22240 !run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 2),)
22241 .expect("post-rollback table seek should succeed")
22242 .is_found()
22243 );
22244
22245 let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
22246 assert!(
22247 !run_async(
22248 index_cursor
22249 .cursor
22250 .index_move_to(&index_cursor.cx, &index_key),
22251 )
22252 .expect("post-rollback index seek should succeed")
22253 .is_found()
22254 );
22255 assert_eq!(engine.changes(), 0);
22256 }
22257
22258 #[test]
22259 fn test_execute_insert_with_explicit_rowid_zero_tracks_last_insert_rowid() {
22260 let mut db = MemDatabase::new();
22261 let root = db.create_table(1);
22262
22263 let mut builder = ProgramBuilder::new();
22264 let end = builder.emit_label();
22265 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22266 builder.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
22267 builder.emit_op(Opcode::Integer, 0, 1, 0, P4::None, 0);
22268 builder.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
22269 builder.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
22270 builder.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
22271 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22272 builder.resolve_label(end);
22273 let program = builder.finish().expect("program should build");
22274
22275 let mut engine = VdbeEngine::new(program.register_count());
22276 engine.set_database(db);
22277 engine.set_reject_mem_fallback(false);
22278
22279 assert_eq!(
22280 run_async(engine.execute(&program)).expect("insert execution"),
22281 ExecOutcome::Done
22282 );
22283 assert_eq!(engine.changes(), 1);
22284 assert_eq!(engine.last_insert_rowid(), Some(0));
22285 }
22286
22287 #[test]
22288 fn test_execute_observes_cancelled_execution_context_before_first_opcode() {
22289 let cx = Cx::new();
22290 cx.cancel();
22291
22292 let mut builder = ProgramBuilder::new();
22293 builder.emit_op(Opcode::Integer, 7, 1, 0, P4::None, 0);
22294 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22295 let program = builder.finish().expect("program should build");
22296
22297 let mut engine =
22298 VdbeEngine::new_with_execution_cx(program.register_count(), &cx, PageSize::DEFAULT);
22299 let err = run_async(engine.execute(&program))
22300 .expect_err("cancelled execution context should abort before opcode dispatch");
22301
22302 assert!(matches!(err, FrankenError::Abort));
22303 }
22304
22305 #[test]
22306 fn test_execute_observes_execution_cx_cancellation_immediately_after_function_opcode() {
22307 let root_cx = Cx::new();
22308
22309 let mut builder = ProgramBuilder::new();
22310 let result_reg = builder.alloc_reg();
22311 builder.emit_op(
22312 Opcode::Function,
22313 0,
22314 0,
22315 result_reg,
22316 P4::FuncName("cancel_exec".to_owned()),
22317 0,
22318 );
22319 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22320 let program = builder.finish().expect("program should build");
22321
22322 let mut engine = VdbeEngine::new_with_execution_cx(
22323 program.register_count(),
22324 &root_cx,
22325 PageSize::DEFAULT,
22326 );
22327 let mut registry = FunctionRegistry::new();
22328 registry.register_scalar(CancelExecutionFunc {
22329 cx: root_cx.clone(),
22330 });
22331 engine.set_function_registry(Arc::new(registry));
22332
22333 let err = run_async(engine.execute(&program))
22334 .expect_err("cancellation should be observed before dispatch continues");
22335 assert!(matches!(err, FrankenError::Abort));
22336 assert!(engine.results().is_empty());
22337 }
22338
22339 fn run_program_with_functions(
22341 build: impl FnOnce(&mut ProgramBuilder),
22342 ) -> Vec<Vec<SqliteValue>> {
22343 let mut b = ProgramBuilder::new();
22344 build(&mut b);
22345 let prog = b.finish().expect("program should build");
22346 let mut engine = VdbeEngine::new(prog.register_count());
22347 let mut registry = FunctionRegistry::new();
22348 register_builtins(&mut registry);
22349 engine.set_function_registry(Arc::new(registry));
22350 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22351 assert_eq!(outcome, ExecOutcome::Done);
22352 engine
22353 .take_results()
22354 .into_iter()
22355 .map(|v| v.into_vec())
22356 .collect()
22357 }
22358
22359 #[test]
22360 fn test_opcode_register_spans_for_variable() {
22361 let op = VdbeOp {
22362 opcode: Opcode::Variable,
22363 p1: 2,
22364 p2: 9,
22365 p3: 0,
22366 p4: P4::None,
22367 p5: 0,
22368 };
22369 let spans = opcode_register_spans(&op);
22370 assert_eq!(spans.read_start, -1);
22371 assert_eq!(spans.read_len, 0);
22372 assert_eq!(spans.write_start, 9);
22373 assert_eq!(spans.write_len, 1);
22374 }
22375
22376 #[test]
22377 fn test_opcode_register_spans_for_result_row() {
22378 let op = VdbeOp {
22379 opcode: Opcode::ResultRow,
22380 p1: 4,
22381 p2: 3,
22382 p3: 0,
22383 p4: P4::None,
22384 p5: 0,
22385 };
22386 let spans = opcode_register_spans(&op);
22387 assert_eq!(spans.read_start, 4);
22388 assert_eq!(spans.read_len, 3);
22389 assert_eq!(spans.write_start, -1);
22390 assert_eq!(spans.write_len, 0);
22391 }
22392
22393 #[test]
22394 fn test_opcode_register_spans_for_copy_range() {
22395 let op = VdbeOp {
22396 opcode: Opcode::Copy,
22397 p1: 4,
22398 p2: 9,
22399 p3: 2,
22400 p4: P4::None,
22401 p5: 0,
22402 };
22403 let spans = opcode_register_spans(&op);
22404 assert_eq!(spans.read_start, 4);
22405 assert_eq!(spans.read_len, 3);
22406 assert_eq!(spans.write_start, 9);
22407 assert_eq!(spans.write_len, 3);
22408 }
22409
22410 #[test]
22411 fn test_variable_uses_bound_parameter_value() {
22412 let rows = run_program_with_bindings(
22413 |b| {
22414 let end = b.emit_label();
22415 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22416 let r1 = b.alloc_reg();
22417 b.emit_op(Opcode::Variable, 2, r1, 0, P4::None, 0);
22418 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22419 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22420 b.resolve_label(end);
22421 },
22422 vec![SqliteValue::Integer(11), SqliteValue::Text("bound".into())],
22423 );
22424 assert_eq!(rows, vec![vec![SqliteValue::Text("bound".into())]]);
22425 }
22426
22427 #[test]
22428 fn test_variable_integer_binding_in_place_fast_path() {
22429 let rows = run_program_with_bindings(
22434 |b| {
22435 let end = b.emit_label();
22436 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22437 let r1 = b.alloc_reg();
22438 b.emit_op(Opcode::Variable, 1, r1, 0, P4::None, 0);
22439 b.emit_op(Opcode::Variable, 1, r1, 0, P4::None, 0);
22440 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22441 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22442 b.resolve_label(end);
22443 },
22444 vec![SqliteValue::Integer(-7)],
22445 );
22446 assert_eq!(rows, vec![vec![SqliteValue::Integer(-7)]]);
22447 }
22448
22449 #[test]
22450 fn test_arith_integer_result_in_place_fast_path() {
22451 let rows = run_program_with_bindings(
22456 |b| {
22457 let end = b.emit_label();
22458 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22459 let lhs = b.alloc_reg();
22460 let rhs = b.alloc_reg();
22461 let out = b.alloc_reg();
22462 b.emit_op(Opcode::Integer, 17, lhs, 0, P4::None, 0);
22463 b.emit_op(Opcode::Integer, 25, rhs, 0, P4::None, 0);
22464 b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22466 b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22467 let two = b.alloc_reg();
22469 b.emit_op(Opcode::Integer, 2, two, 0, P4::None, 0);
22470 b.emit_op(Opcode::Multiply, two, out, out, P4::None, 0);
22471 b.emit_op(Opcode::Subtract, out, out, out, P4::None, 0);
22473 b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22474 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22475 b.resolve_label(end);
22476 },
22477 vec![],
22478 );
22479 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
22480 }
22481
22482 #[test]
22483 fn test_arith_float_result_falls_through_general_path() {
22484 let rows = run_program_with_bindings(
22487 |b| {
22488 let end = b.emit_label();
22489 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22490 let lhs = b.alloc_reg();
22491 let rhs = b.alloc_reg();
22492 let out = b.alloc_reg();
22493 b.emit_op(Opcode::Integer, 5, lhs, 0, P4::None, 0);
22494 b.emit_op(Opcode::Real, 0, rhs, 0, P4::Real(2.5), 0);
22495 b.emit_op(Opcode::Add, rhs, lhs, out, P4::None, 0);
22496 b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22497 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22498 b.resolve_label(end);
22499 },
22500 vec![],
22501 );
22502 assert_eq!(rows, vec![vec![SqliteValue::Float(7.5)]]);
22503 }
22504
22505 #[test]
22506 fn test_div_rem_integer_result_in_place_and_zero_null() {
22507 let rows = run_program_with_bindings(
22511 |b| {
22512 let end = b.emit_label();
22513 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22514 let dividend = b.alloc_reg();
22515 let divisor = b.alloc_reg();
22516 let zero = b.alloc_reg();
22517 let out = b.alloc_reg();
22518 b.emit_op(Opcode::Integer, 86, dividend, 0, P4::None, 0);
22519 b.emit_op(Opcode::Integer, 7, divisor, 0, P4::None, 0);
22520 b.emit_op(Opcode::Integer, 0, zero, 0, P4::None, 0);
22521 b.emit_op(Opcode::Divide, divisor, dividend, out, P4::None, 0);
22523 b.emit_op(Opcode::Divide, divisor, dividend, out, P4::None, 0);
22524 b.emit_op(Opcode::Remainder, divisor, dividend, out, P4::None, 0);
22526 b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22527 b.emit_op(Opcode::Divide, zero, dividend, out, P4::None, 0);
22529 b.emit_op(Opcode::ResultRow, out, 1, 0, P4::None, 0);
22530 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22531 b.resolve_label(end);
22532 },
22533 vec![],
22534 );
22535 assert_eq!(
22536 rows,
22537 vec![vec![SqliteValue::Integer(2)], vec![SqliteValue::Null]]
22538 );
22539 }
22540
22541 #[test]
22542 fn test_variable_unbound_parameter_defaults_to_null() {
22543 let rows = run_program_with_bindings(
22544 |b| {
22545 let end = b.emit_label();
22546 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22547 let r1 = b.alloc_reg();
22548 b.emit_op(Opcode::Variable, 3, r1, 0, P4::None, 0);
22549 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22550 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22551 b.resolve_label(end);
22552 },
22553 vec![SqliteValue::Integer(11)],
22554 );
22555 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
22556 }
22557
22558 #[test]
22560 fn test_select_integer_literal() {
22561 let rows = run_program(|b| {
22563 let end = b.emit_label();
22564 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22565 let r1 = b.alloc_reg();
22566 b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
22567 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
22568 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22569 b.resolve_label(end);
22570 });
22571 assert_eq!(rows.len(), 1);
22572 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
22573 }
22574
22575 #[test]
22577 fn test_select_arithmetic() {
22578 let rows = run_program(|b| {
22580 let end = b.emit_label();
22581 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22582
22583 let r1 = b.alloc_reg(); let r2 = b.alloc_reg(); let r3 = b.alloc_reg(); b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
22588 b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
22589 b.emit_op(Opcode::Add, r1, r2, r3, P4::None, 0);
22590 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22591 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22592 b.resolve_label(end);
22593 });
22594 assert_eq!(rows.len(), 1);
22595 assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
22596 }
22597
22598 #[test]
22600 fn test_select_expression_eval() {
22601 let rows = run_program(|b| {
22603 let end = b.emit_label();
22604 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22605
22606 let r1 = b.alloc_reg();
22607 let r2 = b.alloc_reg();
22608 let r3 = b.alloc_reg(); let r4 = b.alloc_reg();
22610 let r5 = b.alloc_reg();
22611 let r6 = b.alloc_reg(); b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
22615 b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
22616 b.emit_op(Opcode::Add, r1, r2, r3, P4::None, 0);
22617
22618 b.emit_op(Opcode::String8, 0, r4, 0, P4::Str("abc".to_owned()), 0);
22620 b.emit_op(Opcode::String8, 0, r5, 0, P4::Str("def".to_owned()), 0);
22621 b.emit_op(Opcode::Concat, r5, r4, r6, P4::None, 0);
22622
22623 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22624 b.emit_op(Opcode::ResultRow, r6, 1, 0, P4::None, 0);
22626
22627 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22628 b.resolve_label(end);
22629 });
22630 assert_eq!(rows.len(), 2);
22631 assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
22632 assert_eq!(rows[1], vec![SqliteValue::Text("abcdef".into())]);
22633 }
22634
22635 #[test]
22637 fn test_select_multi_column() {
22638 let rows = run_program(|b| {
22640 let end = b.emit_label();
22641 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22642
22643 let out_start = b.alloc_regs(2);
22644 let r_tmp1 = b.alloc_reg();
22645 let r_tmp2 = b.alloc_reg();
22646
22647 b.emit_op(Opcode::Integer, 1, r_tmp1, 0, P4::None, 0);
22649 b.emit_op(Opcode::Integer, 2, r_tmp2, 0, P4::None, 0);
22650 b.emit_op(Opcode::Add, r_tmp1, r_tmp2, out_start, P4::None, 0);
22651
22652 b.emit_op(Opcode::String8, 0, r_tmp1, 0, P4::Str("abc".to_owned()), 0);
22654 b.emit_op(Opcode::String8, 0, r_tmp2, 0, P4::Str("def".to_owned()), 0);
22655 b.emit_op(Opcode::Concat, r_tmp2, r_tmp1, out_start + 1, P4::None, 0);
22656
22657 b.emit_op(Opcode::ResultRow, out_start, 2, 0, P4::None, 0);
22658 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22659 b.resolve_label(end);
22660 });
22661 assert_eq!(rows.len(), 1);
22662 assert_eq!(
22663 rows[0],
22664 vec![SqliteValue::Integer(3), SqliteValue::Text("abcdef".into()),]
22665 );
22666 }
22667
22668 #[test]
22670 fn test_vdbe_null_handling() {
22671 let rows = run_program(|b| {
22673 let end = b.emit_label();
22674 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22675
22676 let r_null = b.alloc_reg();
22677 let r_one = b.alloc_reg();
22678 let r_result = b.alloc_reg();
22679 let r_is_null = b.alloc_reg();
22680
22681 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
22683 b.emit_op(Opcode::Integer, 1, r_one, 0, P4::None, 0);
22685 b.emit_op(Opcode::Add, r_null, r_one, r_result, P4::None, 0);
22687 b.emit_op(Opcode::Integer, 0, r_is_null, 0, P4::None, 0);
22689 let skip = b.emit_label();
22690 b.emit_jump_to_label(Opcode::NotNull, r_result, 0, skip, P4::None, 0);
22691 b.emit_op(Opcode::Integer, 1, r_is_null, 0, P4::None, 0);
22692 b.resolve_label(skip);
22693
22694 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
22695 b.emit_op(Opcode::ResultRow, r_is_null, 1, 0, P4::None, 0);
22696 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22697 b.resolve_label(end);
22698 });
22699 assert_eq!(rows.len(), 2);
22700 assert_eq!(rows[0], vec![SqliteValue::Null]); assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); }
22703
22704 #[test]
22706 fn test_vdbe_comparison_affinity() {
22707 let rows = run_program(|b| {
22709 let end = b.emit_label();
22710 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22711
22712 let r_5 = b.alloc_reg();
22713 let r_3 = b.alloc_reg();
22714 let r_out = b.alloc_reg();
22715
22716 b.emit_op(Opcode::Integer, 5, r_5, 0, P4::None, 0);
22717 b.emit_op(Opcode::Integer, 3, r_3, 0, P4::None, 0);
22718
22719 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
22721 let gt_taken = b.emit_label();
22722 b.emit_jump_to_label(Opcode::Gt, r_3, r_5, gt_taken, P4::None, 0);
22723 let done1 = b.emit_label();
22725 b.emit_jump_to_label(Opcode::Goto, 0, 0, done1, P4::None, 0);
22726 b.resolve_label(gt_taken);
22728 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
22729 b.resolve_label(done1);
22730
22731 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
22732
22733 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
22735 let gt_taken2 = b.emit_label();
22736 b.emit_jump_to_label(Opcode::Gt, r_5, r_3, gt_taken2, P4::None, 0);
22738 let done2 = b.emit_label();
22739 b.emit_jump_to_label(Opcode::Goto, 0, 0, done2, P4::None, 0);
22740 b.resolve_label(gt_taken2);
22741 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
22742 b.resolve_label(done2);
22743
22744 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
22745 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22746 b.resolve_label(end);
22747 });
22748 assert_eq!(rows.len(), 2);
22749 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); }
22752
22753 #[test]
22754 fn test_comparison_affinity_precedes_same_storage_fast_paths() {
22755 let rows = run_program(|b| {
22756 let end = b.emit_label();
22757 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22758
22759 let r_text_non_numeric = b.alloc_reg();
22760 let r_text_two = b.alloc_reg();
22761 let r_text_ten = b.alloc_reg();
22762 let r_int_two = b.alloc_reg();
22763 let r_int_ten = b.alloc_reg();
22764 let r_float_two = b.alloc_reg();
22765 let r_float_ten = b.alloc_reg();
22766 let outputs = b.alloc_regs(4);
22767
22768 b.emit_op(
22769 Opcode::String8,
22770 0,
22771 r_text_non_numeric,
22772 0,
22773 P4::Str("0x".to_owned()),
22774 0,
22775 );
22776 b.emit_op(
22777 Opcode::String8,
22778 0,
22779 r_text_two,
22780 0,
22781 P4::Str("2".to_owned()),
22782 0,
22783 );
22784 b.emit_op(
22785 Opcode::String8,
22786 0,
22787 r_text_ten,
22788 0,
22789 P4::Str("10".to_owned()),
22790 0,
22791 );
22792 b.emit_op(Opcode::Integer, 2, r_int_two, 0, P4::None, 0);
22793 b.emit_op(Opcode::Integer, 10, r_int_ten, 0, P4::None, 0);
22794 b.emit_op(Opcode::Real, 0, r_float_two, 0, P4::Real(2.0), 0);
22795 b.emit_op(Opcode::Real, 0, r_float_ten, 0, P4::Real(10.0), 0);
22796
22797 b.emit_op(
22802 Opcode::Gt,
22803 r_text_ten,
22804 outputs,
22805 r_text_non_numeric,
22806 P4::None,
22807 SQLITE_AFF_NUMERIC | 0x20,
22808 );
22809 b.emit_op(
22811 Opcode::Lt,
22812 r_text_ten,
22813 outputs + 1,
22814 r_text_two,
22815 P4::None,
22816 SQLITE_AFF_NUMERIC | 0x20,
22817 );
22818 b.emit_op(
22821 Opcode::Gt,
22822 r_int_ten,
22823 outputs + 2,
22824 r_int_two,
22825 P4::None,
22826 SQLITE_AFF_TEXT | 0x20,
22827 );
22828 b.emit_op(
22829 Opcode::Gt,
22830 r_float_ten,
22831 outputs + 3,
22832 r_float_two,
22833 P4::None,
22834 SQLITE_AFF_TEXT | 0x20,
22835 );
22836
22837 b.emit_op(Opcode::ResultRow, outputs, 4, 0, P4::None, 0);
22838 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22839 b.resolve_label(end);
22840 });
22841
22842 assert_eq!(
22843 rows,
22844 vec![vec![
22845 SqliteValue::Integer(1),
22846 SqliteValue::Integer(1),
22847 SqliteValue::Integer(1),
22848 SqliteValue::Integer(1),
22849 ]]
22850 );
22851 }
22852
22853 #[test]
22854 fn test_compare_opcode_uses_per_field_collation_list() {
22855 let mut b = ProgramBuilder::new();
22856 let left_key = b.alloc_regs(2);
22857 let right_key = b.alloc_regs(2);
22858
22859 b.emit_op(
22860 Opcode::String8,
22861 0,
22862 left_key,
22863 0,
22864 P4::Str("Apple".to_owned()),
22865 0,
22866 );
22867 b.emit_op(
22868 Opcode::String8,
22869 0,
22870 left_key + 1,
22871 0,
22872 P4::Str("beta".to_owned()),
22873 0,
22874 );
22875 b.emit_op(
22876 Opcode::String8,
22877 0,
22878 right_key,
22879 0,
22880 P4::Str("apple".to_owned()),
22881 0,
22882 );
22883 b.emit_op(
22884 Opcode::String8,
22885 0,
22886 right_key + 1,
22887 0,
22888 P4::Str("Beta".to_owned()),
22889 0,
22890 );
22891 b.emit_op(
22892 Opcode::Compare,
22893 left_key,
22894 right_key,
22895 2,
22896 P4::Str("NOCASE,BINARY".to_owned()),
22897 0,
22898 );
22899 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22900
22901 let prog = b.finish().expect("program should build");
22902 let mut engine = VdbeEngine::new(prog.register_count());
22903 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22904 assert_eq!(outcome, ExecOutcome::Done);
22905 assert_eq!(
22906 engine.last_compare_result,
22907 Some(std::cmp::Ordering::Greater)
22908 );
22909 }
22910
22911 #[test]
22912 fn test_compare_opcode_parses_trimmed_collation_list() {
22913 let mut b = ProgramBuilder::new();
22914 let left_key = b.alloc_regs(2);
22915 let right_key = b.alloc_regs(2);
22916
22917 b.emit_op(
22918 Opcode::String8,
22919 0,
22920 left_key,
22921 0,
22922 P4::Str("Alpha".to_owned()),
22923 0,
22924 );
22925 b.emit_op(
22926 Opcode::String8,
22927 0,
22928 left_key + 1,
22929 0,
22930 P4::Str("tail ".to_owned()),
22931 0,
22932 );
22933 b.emit_op(
22934 Opcode::String8,
22935 0,
22936 right_key,
22937 0,
22938 P4::Str("alpha".to_owned()),
22939 0,
22940 );
22941 b.emit_op(
22942 Opcode::String8,
22943 0,
22944 right_key + 1,
22945 0,
22946 P4::Str("tail".to_owned()),
22947 0,
22948 );
22949 b.emit_op(
22950 Opcode::Compare,
22951 left_key,
22952 right_key,
22953 2,
22954 P4::Str(" NOCASE | RTRIM ".to_owned()),
22955 0,
22956 );
22957 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22958
22959 let prog = b.finish().expect("program should build");
22960 let mut engine = VdbeEngine::new(prog.register_count());
22961 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
22962 assert_eq!(outcome, ExecOutcome::Done);
22963 assert_eq!(engine.last_compare_result, Some(std::cmp::Ordering::Equal));
22964 }
22965
22966 #[test]
22968 fn test_vdbe_division_by_zero() {
22969 let rows = run_program(|b| {
22971 let end = b.emit_label();
22972 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22973
22974 let r1 = b.alloc_reg();
22975 let r2 = b.alloc_reg();
22976 let r3 = b.alloc_reg();
22977
22978 b.emit_op(Opcode::Integer, 0, r1, 0, P4::None, 0);
22979 b.emit_op(Opcode::Integer, 10, r2, 0, P4::None, 0);
22980 b.emit_op(Opcode::Divide, r1, r2, r3, P4::None, 0);
22981 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
22982 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
22983 b.resolve_label(end);
22984 });
22985 assert_eq!(rows.len(), 1);
22986 assert_eq!(rows[0], vec![SqliteValue::Null]); }
22988
22989 #[test]
22990 fn test_vdbe_nan_arithmetic_normalized_to_null() {
22991 let rows = run_program(|b| {
22994 let end = b.emit_label();
22995 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
22996
22997 let r_inf = b.alloc_reg();
22998 let r_zero = b.alloc_reg();
22999 let r_sub = b.alloc_reg();
23000 let r_mul = b.alloc_reg();
23001
23002 b.emit_op(Opcode::Real, 0, r_inf, 0, P4::Real(f64::INFINITY), 0);
23003 b.emit_op(Opcode::Real, 0, r_zero, 0, P4::Real(0.0), 0);
23004 b.emit_op(Opcode::Subtract, r_inf, r_inf, r_sub, P4::None, 0); b.emit_op(Opcode::Multiply, r_inf, r_zero, r_mul, P4::None, 0); b.emit_op(Opcode::ResultRow, r_sub, 2, 0, P4::None, 0);
23007 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23008 b.resolve_label(end);
23009 });
23010
23011 assert_eq!(rows.len(), 1);
23012 assert_eq!(rows[0], vec![SqliteValue::Null, SqliteValue::Null]);
23013 }
23014
23015 #[test]
23017 fn test_vdbe_string_concat_null() {
23018 let rows = run_program(|b| {
23020 let end = b.emit_label();
23021 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23022
23023 let r1 = b.alloc_reg();
23024 let r2 = b.alloc_reg();
23025 let r3 = b.alloc_reg();
23026
23027 b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("abc".to_owned()), 0);
23028 b.emit_op(Opcode::Null, 0, r2, 0, P4::None, 0);
23029 b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
23030 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23031 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23032 b.resolve_label(end);
23033 });
23034 assert_eq!(rows.len(), 1);
23035 assert_eq!(rows[0], vec![SqliteValue::Null]);
23036 }
23037
23038 #[test]
23040 fn test_vdbe_boolean_logic() {
23041 let rows = run_program(|b| {
23043 let end = b.emit_label();
23044 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23045
23046 let r_true = b.alloc_reg();
23047 let r_false = b.alloc_reg();
23048 let r_and = b.alloc_reg();
23049 let r_or = b.alloc_reg();
23050 let r_not = b.alloc_reg();
23051
23052 b.emit_op(Opcode::Integer, 1, r_true, 0, P4::None, 0);
23053 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
23054 b.emit_op(Opcode::And, r_true, r_false, r_and, P4::None, 0);
23055 b.emit_op(Opcode::Or, r_true, r_false, r_or, P4::None, 0);
23056 b.emit_op(Opcode::Not, r_true, r_not, 0, P4::None, 0);
23057
23058 b.emit_op(Opcode::ResultRow, r_and, 1, 0, P4::None, 0);
23059 b.emit_op(Opcode::ResultRow, r_or, 1, 0, P4::None, 0);
23060 b.emit_op(Opcode::ResultRow, r_not, 1, 0, P4::None, 0);
23061 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23062 b.resolve_label(end);
23063 });
23064 assert_eq!(rows.len(), 3);
23065 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); }
23069
23070 #[test]
23072 fn test_vdbe_three_valued_logic() {
23073 let rows = run_program(|b| {
23076 let end = b.emit_label();
23077 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23078
23079 let r_null = b.alloc_reg();
23080 let r_true = b.alloc_reg();
23081 let r_false = b.alloc_reg();
23082 let r1 = b.alloc_reg();
23083 let r2 = b.alloc_reg();
23084 let r3 = b.alloc_reg();
23085 let r4 = b.alloc_reg();
23086
23087 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
23088 b.emit_op(Opcode::Integer, 1, r_true, 0, P4::None, 0);
23089 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
23090
23091 b.emit_op(Opcode::And, r_null, r_false, r1, P4::None, 0); b.emit_op(Opcode::And, r_null, r_true, r2, P4::None, 0); b.emit_op(Opcode::Or, r_null, r_true, r3, P4::None, 0); b.emit_op(Opcode::Or, r_null, r_false, r4, P4::None, 0); b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
23097 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
23098 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23099 b.emit_op(Opcode::ResultRow, r4, 1, 0, P4::None, 0);
23100 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23101 b.resolve_label(end);
23102 });
23103 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); assert_eq!(rows[1], vec![SqliteValue::Null]); assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); assert_eq!(rows[3], vec![SqliteValue::Null]); }
23108
23109 #[test]
23111 fn test_vdbe_gosub_return() {
23112 let rows = run_program(|b| {
23114 let end = b.emit_label();
23115 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23116
23117 let r_return = b.alloc_reg(); let r_val = b.alloc_reg(); let sub_label = b.emit_label();
23122 b.emit_jump_to_label(Opcode::Gosub, r_return, 0, sub_label, P4::None, 0);
23123 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23124 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23125
23126 b.resolve_label(sub_label);
23128 b.emit_op(Opcode::Integer, 99, r_val, 0, P4::None, 0);
23129 b.emit_op(Opcode::Return, r_return, 0, 0, P4::None, 0);
23130
23131 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23132 b.resolve_label(end);
23133 });
23134 assert_eq!(rows.len(), 1);
23135 assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
23136 }
23137
23138 #[test]
23140 fn test_vdbe_is_null_comparison() {
23141 let rows = run_program(|b| {
23143 let end = b.emit_label();
23144 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23145
23146 let r_null = b.alloc_reg();
23147 let r_out = b.alloc_reg();
23148
23149 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
23150 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
23151
23152 let is_null_label = b.emit_label();
23154 b.emit_jump_to_label(Opcode::Eq, r_null, 0, is_null_label, P4::None, 0x80);
23156 let done = b.emit_label();
23157 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
23158 b.resolve_label(is_null_label);
23159 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23160 b.resolve_label(done);
23161
23162 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23163 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23164 b.resolve_label(end);
23165 });
23166 assert_eq!(rows.len(), 1);
23167 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); }
23169
23170 #[test]
23172 fn test_vdbe_coroutine() {
23173 let rows = run_program(|b| {
23175 let end = b.emit_label();
23176 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23177
23178 let r_co = b.alloc_reg(); let r_val = b.alloc_reg(); let init_addr = b.emit_op(Opcode::InitCoroutine, r_co, 0, 0, P4::None, 0);
23182
23183 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
23184 let consumer_start = b.current_addr() as i32;
23185 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23186 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23187 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23188 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23189 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23190 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
23191 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23192
23193 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
23194 let producer_start = b.current_addr() as i32;
23195 b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
23196 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23197 b.emit_op(Opcode::Integer, 20, r_val, 0, P4::None, 0);
23198 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23199 b.emit_op(Opcode::Integer, 30, r_val, 0, P4::None, 0);
23200 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
23201 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23202
23203 if let Some(init_op) = b.op_at_mut(init_addr) {
23204 init_op.p2 = consumer_start;
23205 init_op.p3 = producer_start;
23206 }
23207
23208 b.resolve_label(end);
23209 });
23210 assert_eq!(rows.len(), 3);
23211 assert_eq!(rows[0], vec![SqliteValue::Integer(10)]);
23212 assert_eq!(rows[1], vec![SqliteValue::Integer(20)]);
23213 assert_eq!(rows[2], vec![SqliteValue::Integer(30)]);
23214 }
23215
23216 #[test]
23218 fn test_vdbe_halt_with_error() {
23219 let mut b = ProgramBuilder::new();
23220 b.emit_op(
23221 Opcode::Halt,
23222 1,
23223 0,
23224 0,
23225 P4::Str("constraint failed".to_owned()),
23226 0,
23227 );
23228 let prog = b.finish().unwrap();
23229 let mut engine = VdbeEngine::new(prog.register_count());
23230 let outcome = run_async(engine.execute(&prog)).unwrap();
23231 assert_eq!(
23232 outcome,
23233 ExecOutcome::Error {
23234 code: 1,
23235 message: "constraint failed".to_owned(),
23236 }
23237 );
23238 }
23239
23240 #[test]
23241 fn test_vdbe_unique_halt_returns_constraint_error_without_mutation() {
23242 let mut b = ProgramBuilder::new();
23243 b.emit_op(
23244 Opcode::Halt,
23245 ErrorCode::Constraint as i32,
23246 0,
23247 0,
23248 P4::Str("t.email".to_owned()),
23249 OPFLAG_HALT_UNIQUE,
23250 );
23251 let program = b.finish().expect("program should finish");
23252 let mut engine = VdbeEngine::new(program.register_count());
23253 let error = run_async(engine.execute(&program)).expect_err("UNIQUE halt must fail");
23254 assert!(
23255 matches!(error, FrankenError::UniqueViolation { ref columns } if columns == "t.email"),
23256 "unexpected UNIQUE halt error: {error:?}"
23257 );
23258 assert_eq!(engine.changes(), 0);
23259 }
23260
23261 #[test]
23263 fn test_vdbe_disassemble_and_exec() {
23264 let mut b = ProgramBuilder::new();
23266 let end = b.emit_label();
23267 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23268 let r1 = b.alloc_reg();
23269 let r2 = b.alloc_reg();
23270 let r3 = b.alloc_reg();
23271 b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
23272 b.emit_op(Opcode::Integer, 20, r2, 0, P4::None, 0);
23273 b.emit_op(Opcode::Multiply, r1, r2, r3, P4::None, 0);
23274 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
23275 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23276 b.resolve_label(end);
23277
23278 let prog = b.finish().unwrap();
23279 let asm = prog.disassemble();
23280 assert!(asm.contains("Init"));
23281 assert!(asm.contains("Integer"));
23282 assert!(asm.contains("Multiply"));
23283 assert!(asm.contains("ResultRow"));
23284 assert!(asm.contains("Halt"));
23285
23286 let mut engine = VdbeEngine::new(prog.register_count());
23287 let outcome = run_async(engine.execute(&prog)).unwrap();
23288 assert_eq!(outcome, ExecOutcome::Done);
23289 assert_eq!(engine.results().len(), 1);
23290 assert_eq!(
23291 engine.results()[0].to_vec(),
23292 vec![SqliteValue::Integer(200)]
23293 );
23294 }
23295
23296 #[test]
23297 fn test_sorter_opcodes_sort_and_emit_rows() {
23298 let rows = run_program(|b| {
23299 let end = b.emit_label();
23300 let loop_start = b.emit_label();
23301 let empty = b.emit_label();
23302 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23303
23304 let r_value = b.alloc_reg();
23305 let r_record = b.alloc_reg();
23306 let r_sorted = b.alloc_reg();
23307
23308 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23309
23310 for value in [30, 10, 20] {
23311 b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
23312 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23313 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23314 }
23315
23316 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23317 b.resolve_label(loop_start);
23318 b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
23319 b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
23320 b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23321 b.resolve_label(empty);
23322 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23323 b.resolve_label(end);
23324 });
23325
23326 let decoded: Vec<i64> = rows
23327 .into_iter()
23328 .map(|row| decode_record(&row[0]).unwrap()[0].to_integer())
23329 .collect();
23330 assert_eq!(decoded, vec![10, 20, 30]);
23331 }
23332
23333 #[test]
23334 fn test_sorter_insert_consumes_make_record_sideband_without_materializing() {
23335 let _guard = VDBE_OBSERVABILITY_LOCK
23336 .lock()
23337 .unwrap_or_else(|e| e.into_inner());
23338 reset_vdbe_test_sideband_materialization_count();
23339
23340 let before = vdbe_test_sideband_materialization_count_snapshot();
23341 let rows = run_program(|b| {
23342 let end = b.emit_label();
23343 let loop_start = b.emit_label();
23344 let empty = b.emit_label();
23345 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23346
23347 let r_value = b.alloc_reg();
23348 let r_record = b.alloc_reg();
23349 let r_sorted = b.alloc_reg();
23350
23351 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23352
23353 for value in [3, 1, 2] {
23354 b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
23355 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23356 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23357 }
23358
23359 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23360 b.resolve_label(loop_start);
23361 b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
23362 b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
23363 b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23364 b.resolve_label(empty);
23365 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23366 b.resolve_label(end);
23367 });
23368 let after = vdbe_test_sideband_materialization_count_snapshot();
23369
23370 assert_eq!(
23371 after - before,
23372 0,
23373 "SorterInsert should move MakeRecord sideband bytes directly into the sorter"
23374 );
23375 let decoded: Vec<i64> = rows
23376 .into_iter()
23377 .map(|row| decode_record(&row[0]).unwrap()[0].to_integer())
23378 .collect();
23379 assert_eq!(decoded, vec![1, 2, 3]);
23380 }
23381
23382 #[test]
23383 fn test_sorter_compare_jumps_on_key_difference() {
23384 let rows = run_program(|b| {
23385 let end = b.emit_label();
23386 let diff = b.emit_label();
23387 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23388
23389 let r_value = b.alloc_reg();
23390 let r_record = b.alloc_reg();
23391 let r_probe = b.alloc_reg();
23392 let r_probe_record = b.alloc_reg();
23393 let r_out = b.alloc_reg();
23394
23395 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23396 b.emit_op(Opcode::Integer, 10, r_value, 0, P4::None, 0);
23397 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23398 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23399 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, diff, P4::None, 0);
23400
23401 b.emit_op(Opcode::Integer, 20, r_probe, 0, P4::None, 0);
23402 b.emit_op(Opcode::MakeRecord, r_probe, 1, r_probe_record, P4::None, 0);
23403 b.emit_jump_to_label(Opcode::SorterCompare, 0, r_probe_record, diff, P4::None, 0);
23404
23405 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23406 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23407 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23408
23409 b.resolve_label(diff);
23410 b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23411 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23412 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23413 b.resolve_label(end);
23414 });
23415
23416 assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
23417 }
23418
23419 #[test]
23420 fn test_sorter_compare_falls_through_on_equal_make_record_sideband_key() {
23421 let rows = run_program(|b| {
23422 let end = b.emit_label();
23423 let diff = b.emit_label();
23424 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23425
23426 let r_value = b.alloc_reg();
23427 let r_record = b.alloc_reg();
23428 let r_probe = b.alloc_reg();
23429 let r_probe_record = b.alloc_reg();
23430 let r_out = b.alloc_reg();
23431
23432 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23433 b.emit_op(Opcode::Integer, 10, r_value, 0, P4::None, 0);
23434 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23435 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23436 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, diff, P4::None, 0);
23437
23438 b.emit_op(Opcode::Integer, 10, r_probe, 0, P4::None, 0);
23439 b.emit_op(Opcode::MakeRecord, r_probe, 1, r_probe_record, P4::None, 0);
23440 b.emit_jump_to_label(Opcode::SorterCompare, 0, r_probe_record, diff, P4::None, 0);
23441
23442 b.emit_op(Opcode::Copy, r_probe_record, r_out, 0, P4::None, 0);
23443 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23444 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23445
23446 b.resolve_label(diff);
23447 b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23448 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23449 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23450 b.resolve_label(end);
23451 });
23452
23453 assert_eq!(rows.len(), 1);
23454 assert_eq!(
23455 decode_record(&rows[0][0]).expect("copied probe should remain a valid record"),
23456 vec![SqliteValue::Integer(10)]
23457 );
23458 }
23459
23460 #[test]
23461 fn test_sorter_compare_unpositioned_sorter_jumps_without_decoding_probe() {
23462 let rows = run_program(|b| {
23463 let end = b.emit_label();
23464 let unpositioned = b.emit_label();
23465 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23466
23467 let r_uninitialized_probe = b.alloc_reg();
23468 let r_out = b.alloc_reg();
23469 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23470 b.emit_jump_to_label(
23471 Opcode::SorterCompare,
23472 0,
23473 r_uninitialized_probe,
23474 unpositioned,
23475 P4::None,
23476 0,
23477 );
23478 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23479 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23480 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23481
23482 b.resolve_label(unpositioned);
23483 b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23484 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23485 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23486 b.resolve_label(end);
23487 });
23488
23489 assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
23490 }
23491
23492 #[test]
23493 fn test_sorter_compare_top_n_preflight_uses_runtime_bound() {
23494 let _guard = VDBE_OBSERVABILITY_LOCK
23495 .lock()
23496 .unwrap_or_else(|error| error.into_inner());
23497 reset_vdbe_test_sideband_materialization_count();
23498 let materializations_before = vdbe_test_sideband_materialization_count_snapshot();
23499 let rows = run_program(|b| {
23500 let end = b.emit_label();
23501 let rejected = b.emit_label();
23502 let after_rejected_probe = b.emit_label();
23503 let unexpectedly_rejected = b.emit_label();
23504 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23505
23506 let r_bound = b.alloc_reg();
23507 let r_key = b.alloc_reg();
23508 let r_record = b.alloc_reg();
23509 let r_out = b.alloc_reg();
23510
23511 b.emit_op(Opcode::Integer, 1, r_bound, 0, P4::None, 0);
23512 b.emit_op(
23513 Opcode::SorterOpen,
23514 0,
23515 1,
23516 r_bound,
23517 P4::Str("+".to_owned()),
23518 SORTER_OPEN_TOP_N_REGISTER,
23519 );
23520 b.emit_op(Opcode::Integer, 10, r_key, 0, P4::None, 0);
23521 b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23522 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23523
23524 b.emit_op(Opcode::Integer, 20, r_key, 0, P4::None, 0);
23526 b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23527 b.emit_jump_to_label(
23528 Opcode::SorterCompare,
23529 0,
23530 r_record,
23531 rejected,
23532 P4::None,
23533 SORTER_COMPARE_TOP_N_PREFLIGHT,
23534 );
23535 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23536 b.emit_jump_to_label(Opcode::Goto, 0, 0, after_rejected_probe, P4::None, 0);
23537 b.resolve_label(rejected);
23538 b.emit_op(Opcode::Integer, 2, r_out, 0, P4::None, 0);
23539 b.resolve_label(after_rejected_probe);
23540 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23541
23542 b.emit_op(Opcode::Integer, 5, r_key, 0, P4::None, 0);
23544 b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23545 b.emit_jump_to_label(
23546 Opcode::SorterCompare,
23547 0,
23548 r_record,
23549 unexpectedly_rejected,
23550 P4::None,
23551 SORTER_COMPARE_TOP_N_PREFLIGHT,
23552 );
23553 b.emit_op(Opcode::Integer, 3, r_out, 0, P4::None, 0);
23554 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23555 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23556
23557 b.resolve_label(unexpectedly_rejected);
23558 b.emit_op(Opcode::Integer, 4, r_out, 0, P4::None, 0);
23559 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23560 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23561 b.resolve_label(end);
23562 });
23563 let materializations_after = vdbe_test_sideband_materialization_count_snapshot();
23564
23565 assert_eq!(
23566 rows,
23567 vec![vec![SqliteValue::Integer(2)], vec![SqliteValue::Integer(3)],]
23568 );
23569 assert_eq!(
23570 materializations_after - materializations_before,
23571 0,
23572 "top-N preflight should consume MakeRecord sideband bytes without allocating an Arc-backed register blob"
23573 );
23574 }
23575
23576 #[test]
23577 fn test_sorter_open_zero_runtime_bound_retains_no_rows() {
23578 let rows = run_program(|b| {
23579 let end = b.emit_label();
23580 let empty = b.emit_label();
23581 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23582
23583 let r_bound = b.alloc_reg();
23584 let r_key = b.alloc_reg();
23585 let r_record = b.alloc_reg();
23586 let r_out = b.alloc_reg();
23587
23588 b.emit_op(Opcode::Integer, 0, r_bound, 0, P4::None, 0);
23589 b.emit_op(
23590 Opcode::SorterOpen,
23591 0,
23592 1,
23593 r_bound,
23594 P4::Str("+".to_owned()),
23595 SORTER_OPEN_TOP_N_REGISTER,
23596 );
23597 b.emit_op(Opcode::Integer, 10, r_key, 0, P4::None, 0);
23598 b.emit_op(Opcode::MakeRecord, r_key, 1, r_record, P4::None, 0);
23599 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23600 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23601
23602 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23603 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23604 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23605
23606 b.resolve_label(empty);
23607 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23608 b.resolve_label(end);
23609 });
23610
23611 assert!(rows.is_empty());
23612 }
23613
23614 #[test]
23615 fn test_sorter_column_reads_decode_non_key_payload_columns() {
23616 let rows = run_program(|b| {
23617 let end = b.emit_label();
23618 let loop_start = b.emit_label();
23619 let empty = b.emit_label();
23620 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23621
23622 let r_key = b.alloc_reg();
23623 let r_payload = b.alloc_reg();
23624 let r_record = b.alloc_reg();
23625 let r_out = b.alloc_reg();
23626
23627 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23628
23629 for (key, payload) in [(30, 300), (10, 100), (20, 200)] {
23630 b.emit_op(Opcode::Integer, key, r_key, 0, P4::None, 0);
23631 b.emit_op(Opcode::Integer, payload, r_payload, 0, P4::None, 0);
23632 b.emit_op(Opcode::MakeRecord, r_key, 2, r_record, P4::None, 0);
23633 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23634 }
23635
23636 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23637 b.resolve_label(loop_start);
23638 b.emit_op(Opcode::Column, 0, 1, r_out, P4::None, 0);
23639 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23640 b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
23641 b.resolve_label(empty);
23642 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23643 b.resolve_label(end);
23644 });
23645
23646 assert_eq!(
23647 rows,
23648 vec![
23649 vec![SqliteValue::Integer(100)],
23650 vec![SqliteValue::Integer(200)],
23651 vec![SqliteValue::Integer(300)],
23652 ]
23653 );
23654 }
23655
23656 #[test]
23657 fn test_reset_sorter_clears_entries() {
23658 let rows = run_program(|b| {
23659 let end = b.emit_label();
23660 let empty = b.emit_label();
23661 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
23662
23663 let r_value = b.alloc_reg();
23664 let r_record = b.alloc_reg();
23665 let r_out = b.alloc_reg();
23666
23667 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
23668 b.emit_op(Opcode::Integer, 7, r_value, 0, P4::None, 0);
23669 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
23670 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
23671 b.emit_op(Opcode::ResetSorter, 0, 0, 0, P4::None, 0);
23672 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
23673
23674 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
23676 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
23677 b.resolve_label(empty);
23678 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23679 b.resolve_label(end);
23680 });
23681
23682 assert!(rows.is_empty());
23683 }
23684
23685 mod codegen_integration {
23688 use super::*;
23689 use crate::codegen::{
23690 CodegenContext, ColumnInfo, TableSchema, codegen_delete, codegen_insert,
23691 codegen_select, codegen_update,
23692 };
23693 use fsqlite_ast::{
23694 Assignment, AssignmentTarget, BinaryOp as AstBinaryOp, ColumnRef, DeleteStatement,
23695 Distinctness, Expr, FromClause, InsertSource, InsertStatement, Literal,
23696 PlaceholderType, QualifiedName, QualifiedTableRef, ResultColumn, SelectBody,
23697 SelectCore, SelectStatement, Span, TableOrSubquery, UpdateStatement,
23698 };
23699
23700 fn test_schema() -> Vec<TableSchema> {
23701 vec![TableSchema {
23702 name: "t".to_owned(),
23703 root_page: 2,
23704 columns: vec![
23705 ColumnInfo {
23706 name: "a".to_owned(),
23707 affinity: 'd',
23708 is_ipk: false,
23709 type_name: None,
23710 notnull: false,
23711 unique: false,
23712 default_value: None,
23713 strict_type: None,
23714 generated_expr: None,
23715 generated_stored: None,
23716 collation: None,
23717 conflict_action: None,
23718 },
23719 ColumnInfo {
23720 name: "b".to_owned(),
23721 affinity: 'C',
23722 is_ipk: false,
23723 type_name: None,
23724 notnull: false,
23725 unique: false,
23726 default_value: None,
23727 strict_type: None,
23728 generated_expr: None,
23729 generated_stored: None,
23730 collation: None,
23731 conflict_action: None,
23732 },
23733 ],
23734 indexes: vec![],
23735 strict: false,
23736 without_rowid: false,
23737 primary_key_constraints: Vec::new(),
23738 foreign_keys: Vec::new(),
23739 check_constraints: Vec::new(),
23740 }]
23741 }
23742
23743 fn from_table(name: &str) -> FromClause {
23744 FromClause {
23745 source: TableOrSubquery::Table {
23746 name: QualifiedName {
23747 schema: None,
23748 name: name.to_owned(),
23749 },
23750 alias: None,
23751 index_hint: None,
23752 time_travel: None,
23753 },
23754 joins: Vec::new(),
23755 }
23756 }
23757
23758 fn span() -> Span {
23759 Span { start: 0, end: 0 }
23760 }
23761
23762 #[test]
23764 fn test_codegen_insert_executes() {
23765 let schema = test_schema();
23766 let ctx = CodegenContext::default();
23767
23768 let stmt = InsertStatement {
23769 with: None,
23770 or_conflict: None,
23771 table: QualifiedName {
23772 schema: None,
23773 name: "t".to_owned(),
23774 },
23775 alias: None,
23776 columns: vec![],
23777 source: InsertSource::Values(vec![vec![
23778 Expr::Literal(Literal::Integer(42), span()),
23779 Expr::Literal(Literal::String("hello".to_owned()), span()),
23780 ]]),
23781 upsert: vec![],
23782 returning: vec![],
23783 };
23784
23785 let mut b = ProgramBuilder::new();
23786 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23787 let prog = b.finish().expect("program should build");
23788
23789 let mut engine = VdbeEngine::new(prog.register_count());
23790 engine.set_reject_mem_fallback(false);
23791 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23792 assert_eq!(outcome, ExecOutcome::Done);
23793 }
23794
23795 #[test]
23797 fn test_codegen_select_full_scan_executes() {
23798 let schema = test_schema();
23799 let ctx = CodegenContext::default();
23800
23801 let stmt = SelectStatement {
23802 with: None,
23803 body: SelectBody {
23804 select: SelectCore::Select {
23805 distinct: Distinctness::All,
23806 columns: vec![ResultColumn::Star],
23807 from: Some(from_table("t")),
23808 where_clause: None,
23809 group_by: vec![],
23810 having: None,
23811 windows: vec![],
23812 },
23813 compounds: vec![],
23814 },
23815 order_by: vec![],
23816 limit: None,
23817 };
23818
23819 let mut b = ProgramBuilder::new();
23820 codegen_select(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23821 let prog = b.finish().expect("program should build");
23822
23823 let mut engine = VdbeEngine::new(prog.register_count());
23825 engine.set_reject_mem_fallback(false);
23826 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23827 assert_eq!(outcome, ExecOutcome::Done);
23828 }
23829
23830 #[test]
23832 fn test_openread_uses_storage_cursor_backend_when_enabled() {
23833 let mut b = ProgramBuilder::new();
23834 b.emit_op(Opcode::OpenRead, 0, 2, 0, P4::None, 0);
23835 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
23836 let prog = b.finish().expect("program should build");
23837
23838 let mut db = MemDatabase::new();
23839 let root = db.create_table(1);
23840 assert_eq!(root, 2);
23841 if let Some(table) = db.get_table_mut(root) {
23842 table.insert(1, vec![SqliteValue::Integer(99)]);
23843 }
23844
23845 let mut engine = VdbeEngine::new(prog.register_count());
23846 engine.enable_storage_read_cursors(true);
23847 engine.set_database(db);
23848 engine.set_reject_mem_fallback(false);
23849 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23850 assert_eq!(outcome, ExecOutcome::Done);
23851 assert!(engine.storage_cursors.contains_key(&0));
23852 assert!(!engine.cursors.contains_key(&0));
23853 }
23854
23855 #[test]
23857 fn test_codegen_update_executes() {
23858 let schema = test_schema();
23859 let ctx = CodegenContext::default();
23860
23861 let stmt = UpdateStatement {
23862 with: None,
23863 or_conflict: None,
23864 table: QualifiedTableRef {
23865 name: QualifiedName {
23866 schema: None,
23867 name: "t".to_owned(),
23868 },
23869 alias: None,
23870 index_hint: None,
23871 time_travel: None,
23872 },
23873 assignments: vec![Assignment {
23874 target: AssignmentTarget::Column("b".to_owned()),
23875 value: Expr::Placeholder(PlaceholderType::Numbered(1), span()),
23876 }],
23877 from: None,
23878 where_clause: Some(Expr::BinaryOp {
23879 left: Box::new(Expr::Column(
23880 ColumnRef {
23881 table: None,
23882 column: "rowid".into(),
23883 },
23884 span(),
23885 )),
23886 op: AstBinaryOp::Eq,
23887 right: Box::new(Expr::Placeholder(PlaceholderType::Numbered(2), span())),
23888 span: span(),
23889 }),
23890 returning: vec![],
23891 order_by: vec![],
23892 limit: None,
23893 };
23894
23895 let mut b = ProgramBuilder::new();
23896 codegen_update(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23897 let prog = b.finish().expect("program should build");
23898
23899 let mut engine = VdbeEngine::new(prog.register_count());
23900 engine.set_reject_mem_fallback(false);
23901 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23902 assert_eq!(outcome, ExecOutcome::Done);
23903 }
23904
23905 #[test]
23907 fn test_codegen_delete_executes() {
23908 let schema = test_schema();
23909 let ctx = CodegenContext::default();
23910
23911 let stmt = DeleteStatement {
23912 with: None,
23913 table: QualifiedTableRef {
23914 name: QualifiedName {
23915 schema: None,
23916 name: "t".to_owned(),
23917 },
23918 alias: None,
23919 index_hint: None,
23920 time_travel: None,
23921 },
23922 where_clause: Some(Expr::BinaryOp {
23923 left: Box::new(Expr::Column(
23924 ColumnRef {
23925 table: None,
23926 column: "rowid".into(),
23927 },
23928 span(),
23929 )),
23930 op: AstBinaryOp::Eq,
23931 right: Box::new(Expr::Placeholder(PlaceholderType::Numbered(1), span())),
23932 span: span(),
23933 }),
23934 returning: vec![],
23935 order_by: vec![],
23936 limit: None,
23937 };
23938
23939 let mut b = ProgramBuilder::new();
23940 codegen_delete(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23941 let prog = b.finish().expect("program should build");
23942
23943 let mut engine = VdbeEngine::new(prog.register_count());
23944 engine.set_reject_mem_fallback(false);
23945 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23946 assert_eq!(outcome, ExecOutcome::Done);
23947 }
23948
23949 #[test]
23951 fn test_codegen_insert_returning_produces_result() {
23952 let schema = test_schema();
23953 let ctx = CodegenContext::default();
23954
23955 let stmt = InsertStatement {
23956 with: None,
23957 or_conflict: None,
23958 table: QualifiedName {
23959 schema: None,
23960 name: "t".to_owned(),
23961 },
23962 alias: None,
23963 columns: vec![],
23964 source: InsertSource::Values(vec![vec![
23965 Expr::Literal(Literal::Integer(7), span()),
23966 Expr::Literal(Literal::String("world".to_owned()), span()),
23967 ]]),
23968 upsert: vec![],
23969 returning: vec![ResultColumn::Star],
23970 };
23971
23972 let mut b = ProgramBuilder::new();
23973 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
23974 let prog = b.finish().expect("program should build");
23975
23976 let mut db = MemDatabase::new();
23979 let root = db.create_table(2);
23980 assert_eq!(root, 2);
23981
23982 let mut engine = VdbeEngine::new(prog.register_count());
23983 engine.set_database(db);
23984 engine.set_reject_mem_fallback(false);
23985 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
23986 assert_eq!(outcome, ExecOutcome::Done);
23987 assert_eq!(engine.results().len(), 1);
23989 }
23990
23991 #[test]
23993 fn test_codegen_insert_literal_values_disassemble() {
23994 let schema = test_schema();
23995 let ctx = CodegenContext::default();
23996
23997 let stmt = InsertStatement {
23998 with: None,
23999 or_conflict: None,
24000 table: QualifiedName {
24001 schema: None,
24002 name: "t".to_owned(),
24003 },
24004 alias: None,
24005 columns: vec![],
24006 source: InsertSource::Values(vec![vec![
24007 Expr::Literal(Literal::Integer(99), span()),
24008 Expr::Literal(Literal::String("test".to_owned()), span()),
24009 ]]),
24010 upsert: vec![],
24011 returning: vec![],
24012 };
24013
24014 let mut b = ProgramBuilder::new();
24015 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24016 let prog = b.finish().expect("program should build");
24017
24018 let asm = prog.disassemble();
24019 let insert = prog
24020 .ops()
24021 .iter()
24022 .find(|op| op.opcode == Opcode::Insert)
24023 .expect("program should contain Insert");
24024 assert!(asm.contains("Init"), "should have Init opcode");
24025 assert!(asm.contains("OpenWrite"), "should have OpenWrite opcode");
24026 assert!(asm.contains("NewRowid"), "should have NewRowid opcode");
24027 assert!(
24028 asm.contains("Integer"),
24029 "should have Integer opcode for literal 99"
24030 );
24031 assert!(
24032 asm.contains("String8"),
24033 "should have String8 opcode for literal 'test'"
24034 );
24035 assert!(
24036 !prog.ops().iter().any(|op| op.opcode == Opcode::Blob),
24037 "preformatted table records should avoid a standalone Blob opcode"
24038 );
24039 assert!(
24040 !asm.contains("MakeRecord"),
24041 "literal-only INSERT should not execute MakeRecord for the table record"
24042 );
24043 assert!(
24044 matches!(&insert.p4, P4::Blob(record) if !record.is_empty()),
24045 "Insert should carry the preformatted table record directly in P4"
24046 );
24047 assert!(asm.contains("Insert"), "should have Insert opcode");
24048 assert!(asm.contains("Halt"), "should have Halt opcode");
24049 }
24050
24051 #[test]
24052 fn test_codegen_insert_literal_values_execute_without_make_record_opcode() {
24053 let schema = test_schema();
24054 let ctx = CodegenContext::default();
24055 let stmt = InsertStatement {
24056 with: None,
24057 or_conflict: None,
24058 table: QualifiedName {
24059 schema: None,
24060 name: "t".to_owned(),
24061 },
24062 alias: None,
24063 columns: vec![],
24064 source: InsertSource::Values(vec![vec![
24065 Expr::Literal(Literal::Integer(99), span()),
24066 Expr::Literal(Literal::String("test".to_owned()), span()),
24067 ]]),
24068 upsert: vec![],
24069 returning: vec![ResultColumn::Star],
24070 };
24071
24072 let mut b = ProgramBuilder::new();
24073 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24074 let prog = b.finish().expect("program should build");
24075 assert!(
24076 !prog.ops().iter().any(|op| op.opcode == Opcode::MakeRecord),
24077 "preformatted INSERT should skip table-row MakeRecord execution"
24078 );
24079
24080 let mut db = MemDatabase::new();
24081 let _root = db.create_table(2);
24082
24083 let mut engine = VdbeEngine::new(prog.register_count());
24084 engine.enable_storage_cursors(true);
24085 engine.set_database(db);
24086 engine.set_reject_mem_fallback(false);
24087 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24088 assert_eq!(outcome, ExecOutcome::Done);
24089
24090 let rows: Vec<_> = engine
24091 .take_results()
24092 .into_iter()
24093 .map(|row| row.into_vec())
24094 .collect();
24095
24096 assert_eq!(
24097 rows,
24098 vec![vec![
24099 SqliteValue::Integer(99),
24100 SqliteValue::Text("test".into())
24101 ]]
24102 );
24103 }
24104
24105 #[test]
24106 fn test_codegen_insert_large_literal_blob_round_trips_via_preformatted_insert() {
24107 let schema = vec![TableSchema {
24108 name: "t".to_owned(),
24109 root_page: 2,
24110 columns: vec![
24111 ColumnInfo {
24112 name: "a".to_owned(),
24113 affinity: 'd',
24114 is_ipk: false,
24115 type_name: None,
24116 notnull: false,
24117 unique: false,
24118 default_value: None,
24119 strict_type: None,
24120 generated_expr: None,
24121 generated_stored: None,
24122 collation: None,
24123 conflict_action: None,
24124 },
24125 ColumnInfo {
24126 name: "b".to_owned(),
24127 affinity: 'A',
24128 is_ipk: false,
24129 type_name: None,
24130 notnull: false,
24131 unique: false,
24132 default_value: None,
24133 strict_type: None,
24134 generated_expr: None,
24135 generated_stored: None,
24136 collation: None,
24137 conflict_action: None,
24138 },
24139 ],
24140 indexes: vec![],
24141 strict: false,
24142 without_rowid: false,
24143 primary_key_constraints: Vec::new(),
24144 foreign_keys: Vec::new(),
24145 check_constraints: Vec::new(),
24146 }];
24147 let large_blob = vec![0xAB; 5_000];
24148 let stmt = InsertStatement {
24149 with: None,
24150 or_conflict: None,
24151 table: QualifiedName {
24152 schema: None,
24153 name: "t".to_owned(),
24154 },
24155 alias: None,
24156 columns: vec![],
24157 source: InsertSource::Values(vec![vec![
24158 Expr::Literal(Literal::Integer(7), span()),
24159 Expr::Literal(Literal::Blob(large_blob.clone()), span()),
24160 ]]),
24161 upsert: vec![],
24162 returning: vec![ResultColumn::Star],
24163 };
24164 let ctx = CodegenContext::default();
24165
24166 let mut b = ProgramBuilder::new();
24167 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24168 let prog = b.finish().expect("program should build");
24169
24170 let insert = prog
24171 .ops()
24172 .iter()
24173 .find(|op| op.opcode == Opcode::Insert)
24174 .expect("program should contain Insert");
24175 assert!(
24176 !prog
24177 .ops()
24178 .iter()
24179 .any(|op| op.opcode == Opcode::Blob && op.p2 == insert.p2),
24180 "preformatted literal rows should not materialize the table record into a Blob register"
24181 );
24182 assert!(
24183 matches!(&insert.p4, P4::Blob(record) if record.len() > 4_096),
24184 "Insert should carry the full preformatted record, including large overflow-bound payloads"
24185 );
24186
24187 let mut db = MemDatabase::new();
24188 let root = db.create_table(2);
24189 assert_eq!(root, 2);
24190
24191 let mut engine = VdbeEngine::new(prog.register_count());
24192 engine.enable_storage_cursors(true);
24193 engine.set_database(db);
24194 engine.set_reject_mem_fallback(false);
24195 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24196 assert_eq!(outcome, ExecOutcome::Done);
24197
24198 let rows: Vec<_> = engine
24199 .take_results()
24200 .into_iter()
24201 .map(|row| row.into_vec())
24202 .collect();
24203 assert_eq!(
24204 rows,
24205 vec![vec![
24206 SqliteValue::Integer(7),
24207 SqliteValue::Blob(large_blob.into()),
24208 ]],
24209 "large literal blob should round-trip through storage even when the record is preformatted on Insert"
24210 );
24211 }
24212
24213 #[test]
24214 fn test_codegen_insert_current_timestamp_uses_runtime_make_record() {
24215 let schema = test_schema();
24216 let ctx = CodegenContext::default();
24217 let stmt = InsertStatement {
24218 with: None,
24219 or_conflict: None,
24220 table: QualifiedName {
24221 schema: None,
24222 name: "t".to_owned(),
24223 },
24224 alias: None,
24225 columns: vec![],
24226 source: InsertSource::Values(vec![vec![
24227 Expr::Literal(Literal::CurrentTimestamp, span()),
24228 Expr::Literal(Literal::String("runtime".to_owned()), span()),
24229 ]]),
24230 upsert: vec![],
24231 returning: vec![],
24232 };
24233
24234 let mut b = ProgramBuilder::new();
24235 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24236 let prog = b.finish().expect("program should build");
24237 let insert = prog
24238 .ops()
24239 .iter()
24240 .find(|op| op.opcode == Opcode::Insert)
24241 .expect("program should contain Insert");
24242
24243 assert!(
24244 prog.ops().iter().any(|op| op.opcode == Opcode::MakeRecord),
24245 "volatile CURRENT_TIMESTAMP values must stay on the runtime record assembly path"
24246 );
24247 assert!(
24248 !matches!(&insert.p4, P4::Blob(_)),
24249 "volatile CURRENT_TIMESTAMP values must not be captured in a preformatted Insert payload"
24250 );
24251 }
24252
24253 #[test]
24255 fn test_codegen_insert_arithmetic_expr() {
24256 let schema = test_schema();
24257 let ctx = CodegenContext::default();
24258
24259 let stmt = InsertStatement {
24261 with: None,
24262 or_conflict: None,
24263 table: QualifiedName {
24264 schema: None,
24265 name: "t".to_owned(),
24266 },
24267 alias: None,
24268 columns: vec![],
24269 source: InsertSource::Values(vec![vec![
24270 Expr::BinaryOp {
24271 left: Box::new(Expr::Literal(Literal::Integer(2), span())),
24272 op: AstBinaryOp::Add,
24273 right: Box::new(Expr::Literal(Literal::Integer(3), span())),
24274 span: span(),
24275 },
24276 Expr::Literal(Literal::String("hi".to_owned()), span()),
24277 ]]),
24278 upsert: vec![],
24279 returning: vec![],
24280 };
24281
24282 let mut b = ProgramBuilder::new();
24283 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24284 let prog = b.finish().expect("program should build");
24285
24286 let asm = prog.disassemble();
24287 assert!(asm.contains("Add"), "should have Add opcode for 2+3");
24288 assert!(asm.contains("Integer"), "should have Integer opcodes");
24289
24290 let mut engine = VdbeEngine::new(prog.register_count());
24291 engine.set_reject_mem_fallback(false);
24292 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24293 assert_eq!(outcome, ExecOutcome::Done);
24294 }
24295
24296 #[test]
24298 fn test_codegen_insert_negation_expr() {
24299 use fsqlite_ast::UnaryOp as AstUnaryOp;
24300
24301 let schema = test_schema();
24302 let ctx = CodegenContext::default();
24303
24304 let stmt = InsertStatement {
24306 with: None,
24307 or_conflict: None,
24308 table: QualifiedName {
24309 schema: None,
24310 name: "t".to_owned(),
24311 },
24312 alias: None,
24313 columns: vec![],
24314 source: InsertSource::Values(vec![vec![
24315 Expr::UnaryOp {
24316 op: AstUnaryOp::Negate,
24317 expr: Box::new(Expr::Literal(Literal::Integer(42), span())),
24318 span: span(),
24319 },
24320 Expr::Literal(Literal::String("neg".to_owned()), span()),
24321 ]]),
24322 upsert: vec![],
24323 returning: vec![],
24324 };
24325
24326 let mut b = ProgramBuilder::new();
24327 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24328 let prog = b.finish().expect("program should build");
24329
24330 let asm = prog.disassemble();
24331 assert!(asm.contains("Multiply"), "negation emits Multiply by -1");
24332
24333 let mut engine = VdbeEngine::new(prog.register_count());
24334 engine.set_reject_mem_fallback(false);
24335 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24336 assert_eq!(outcome, ExecOutcome::Done);
24337 }
24338
24339 #[test]
24341 fn test_codegen_insert_case_expr() {
24342 let schema = test_schema();
24343 let ctx = CodegenContext::default();
24344
24345 let stmt = InsertStatement {
24347 with: None,
24348 or_conflict: None,
24349 table: QualifiedName {
24350 schema: None,
24351 name: "t".to_owned(),
24352 },
24353 alias: None,
24354 columns: vec![],
24355 source: InsertSource::Values(vec![vec![
24356 Expr::Case {
24357 operand: None,
24358 whens: vec![(
24359 Expr::Literal(Literal::True, span()),
24360 Expr::Literal(Literal::Integer(10), span()),
24361 )],
24362 else_expr: Some(Box::new(Expr::Literal(Literal::Integer(20), span()))),
24363 span: span(),
24364 },
24365 Expr::Literal(Literal::String("case".to_owned()), span()),
24366 ]]),
24367 upsert: vec![],
24368 returning: vec![],
24369 };
24370
24371 let mut b = ProgramBuilder::new();
24372 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24373 let prog = b.finish().expect("program should build");
24374
24375 let asm = prog.disassemble();
24376 assert!(asm.contains("IfNot"), "searched CASE emits IfNot");
24377 assert!(asm.contains("Goto"), "CASE branches with Goto");
24378
24379 let mut engine = VdbeEngine::new(prog.register_count());
24380 engine.set_reject_mem_fallback(false);
24381 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24382 assert_eq!(outcome, ExecOutcome::Done);
24383 }
24384
24385 #[test]
24387 fn test_codegen_insert_comparison_expr() {
24388 let schema = test_schema();
24389 let ctx = CodegenContext::default();
24390
24391 let stmt = InsertStatement {
24393 with: None,
24394 or_conflict: None,
24395 table: QualifiedName {
24396 schema: None,
24397 name: "t".to_owned(),
24398 },
24399 alias: None,
24400 columns: vec![],
24401 source: InsertSource::Values(vec![vec![
24402 Expr::BinaryOp {
24403 left: Box::new(Expr::Literal(Literal::Integer(3), span())),
24404 op: AstBinaryOp::Gt,
24405 right: Box::new(Expr::Literal(Literal::Integer(2), span())),
24406 span: span(),
24407 },
24408 Expr::Literal(Literal::String("cmp".to_owned()), span()),
24409 ]]),
24410 upsert: vec![],
24411 returning: vec![],
24412 };
24413
24414 let mut b = ProgramBuilder::new();
24415 codegen_insert(&mut b, &stmt, &schema, &ctx).expect("codegen should succeed");
24416 let prog = b.finish().expect("program should build");
24417
24418 let asm = prog.disassemble();
24419 assert!(asm.contains("Gt"), "comparison emits Gt opcode");
24420
24421 let mut engine = VdbeEngine::new(prog.register_count());
24422 engine.set_reject_mem_fallback(false);
24423 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
24424 assert_eq!(outcome, ExecOutcome::Done);
24425 }
24426 }
24427
24428 #[test]
24435 fn test_int64_large_value() {
24436 let rows = run_program(|b| {
24437 let end = b.emit_label();
24438 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24439 let r = b.alloc_reg();
24440 b.emit_op(Opcode::Int64, 0, r, 0, P4::Int64(i64::MAX), 0);
24441 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24442 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24443 b.resolve_label(end);
24444 });
24445 assert_eq!(rows[0], vec![SqliteValue::Integer(i64::MAX)]);
24446 }
24447
24448 #[test]
24449 fn test_int64_negative() {
24450 let rows = run_program(|b| {
24451 let end = b.emit_label();
24452 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24453 let r = b.alloc_reg();
24454 b.emit_op(Opcode::Int64, 0, r, 0, P4::Int64(-999_999_999_999), 0);
24455 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24456 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24457 b.resolve_label(end);
24458 });
24459 assert_eq!(rows[0], vec![SqliteValue::Integer(-999_999_999_999)]);
24460 }
24461
24462 #[test]
24463 fn test_real_constant() {
24464 let rows = run_program(|b| {
24465 let end = b.emit_label();
24466 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24467 let r = b.alloc_reg();
24468 b.emit_op(Opcode::Real, 0, r, 0, P4::Real(std::f64::consts::PI), 0);
24469 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24470 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24471 b.resolve_label(end);
24472 });
24473 assert_eq!(rows[0], vec![SqliteValue::Float(std::f64::consts::PI)]);
24474 }
24475
24476 #[test]
24477 fn test_real_negative_zero() {
24478 let rows = run_program(|b| {
24479 let end = b.emit_label();
24480 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24481 let r = b.alloc_reg();
24482 b.emit_op(Opcode::Real, 0, r, 0, P4::Real(0.0), 0);
24483 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24484 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24485 b.resolve_label(end);
24486 });
24487 assert_eq!(rows[0], vec![SqliteValue::Float(0.0)]);
24488 }
24489
24490 #[test]
24491 fn test_string_opcode() {
24492 let rows = run_program(|b| {
24493 let end = b.emit_label();
24494 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24495 let r = b.alloc_reg();
24496 b.emit_op(Opcode::String, 5, r, 0, P4::Str("hello".to_owned()), 0);
24497 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24498 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24499 b.resolve_label(end);
24500 });
24501 assert_eq!(rows[0], vec![SqliteValue::Text("hello".into())]);
24502 }
24503
24504 #[test]
24505 fn test_blob_constant() {
24506 let rows = run_program(|b| {
24507 let end = b.emit_label();
24508 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24509 let r = b.alloc_reg();
24510 b.emit_op(
24511 Opcode::Blob,
24512 0,
24513 r,
24514 0,
24515 P4::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
24516 0,
24517 );
24518 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24519 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24520 b.resolve_label(end);
24521 });
24522 assert_eq!(
24523 rows[0],
24524 vec![SqliteValue::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF].into())]
24525 );
24526 }
24527
24528 #[test]
24529 fn test_null_range() {
24530 let rows = run_program(|b| {
24532 let end = b.emit_label();
24533 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24534 let r1 = b.alloc_reg();
24535 let r2 = b.alloc_reg();
24536 let r3 = b.alloc_reg();
24537 b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
24539 b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
24540 b.emit_op(Opcode::Integer, 3, r3, 0, P4::None, 0);
24541 b.emit_op(Opcode::Null, 0, r1, r3, P4::None, 0);
24543 b.emit_op(Opcode::Null, 0, r1, r3, P4::None, 0);
24545 b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24546 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24547 b.resolve_label(end);
24548 });
24549 assert_eq!(
24550 rows[0],
24551 vec![SqliteValue::Null, SqliteValue::Null, SqliteValue::Null]
24552 );
24553 }
24554
24555 #[test]
24556 fn test_null_clears_already_null_sideband_and_subtype() {
24557 let rows = run_program(|b| {
24558 let end = b.emit_label();
24559 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24560 let r_value = b.alloc_reg();
24561 let r_record = b.alloc_reg();
24562 let r_subtype = b.alloc_reg();
24563 let r_observed_subtype = b.alloc_reg();
24564
24565 b.emit_op(Opcode::Integer, 42, r_value, 0, P4::None, 0);
24568 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
24569 b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
24570 b.emit_op(Opcode::SetSubtype, r_subtype, r_record, 0, P4::None, 0);
24571
24572 b.emit_op(Opcode::Null, 0, r_record, 0, P4::None, 0);
24573 b.emit_op(
24574 Opcode::GetSubtype,
24575 r_record,
24576 r_observed_subtype,
24577 0,
24578 P4::None,
24579 0,
24580 );
24581 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
24582 b.emit_op(Opcode::ResultRow, r_observed_subtype, 1, 0, P4::None, 0);
24583 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24584 b.resolve_label(end);
24585 });
24586
24587 assert_eq!(
24588 rows,
24589 vec![vec![SqliteValue::Null], vec![SqliteValue::Integer(0)],]
24590 );
24591 }
24592
24593 #[test]
24594 fn test_soft_null() {
24595 let rows = run_program(|b| {
24596 let end = b.emit_label();
24597 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24598 let r = b.alloc_reg();
24599 b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
24600 b.emit_op(Opcode::SoftNull, r, 0, 0, P4::None, 0);
24601 b.emit_op(Opcode::SoftNull, r, 0, 0, P4::None, 0);
24603 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24604 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24605 b.resolve_label(end);
24606 });
24607 assert_eq!(rows[0], vec![SqliteValue::Null]);
24608 }
24609
24610 #[test]
24611 fn test_soft_null_clears_already_null_sideband_and_subtype() {
24612 let rows = run_program(|b| {
24613 let end = b.emit_label();
24614 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24615 let r_value = b.alloc_reg();
24616 let r_record = b.alloc_reg();
24617 let r_subtype = b.alloc_reg();
24618 let r_observed_subtype = b.alloc_reg();
24619
24620 b.emit_op(Opcode::Integer, 42, r_value, 0, P4::None, 0);
24623 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
24624 b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
24625 b.emit_op(Opcode::SetSubtype, r_subtype, r_record, 0, P4::None, 0);
24626
24627 b.emit_op(Opcode::SoftNull, r_record, 0, 0, P4::None, 0);
24628 b.emit_op(
24629 Opcode::GetSubtype,
24630 r_record,
24631 r_observed_subtype,
24632 0,
24633 P4::None,
24634 0,
24635 );
24636 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
24637 b.emit_op(Opcode::ResultRow, r_observed_subtype, 1, 0, P4::None, 0);
24638 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24639 b.resolve_label(end);
24640 });
24641
24642 assert_eq!(
24643 rows,
24644 vec![vec![SqliteValue::Null], vec![SqliteValue::Integer(0)],]
24645 );
24646 }
24647
24648 #[test]
24649 fn test_move_registers() {
24650 let rows = run_program(|b| {
24652 let end = b.emit_label();
24653 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24654 let src = b.alloc_reg();
24655 let dst = b.alloc_reg();
24656 b.emit_op(Opcode::Integer, 77, src, 0, P4::None, 0);
24657 b.emit_op(Opcode::Move, src, dst, 1, P4::None, 0);
24659 b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24661 b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24662 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24663 b.resolve_label(end);
24664 });
24665 assert_eq!(rows[0], vec![SqliteValue::Integer(77)]);
24666 assert_eq!(rows[1], vec![SqliteValue::Null]);
24667 }
24668
24669 #[test]
24670 fn test_move_register_range_preserves_forward_overlap() {
24671 let rows = run_program(|b| {
24672 let end = b.emit_label();
24673 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24674 let r1 = b.alloc_reg();
24675 let r2 = b.alloc_reg();
24676 let r3 = b.alloc_reg();
24677 b.emit_op(Opcode::Integer, 11, r1, 0, P4::None, 0);
24678 b.emit_op(Opcode::Integer, 22, r2, 0, P4::None, 0);
24679 b.emit_op(Opcode::Integer, 33, r3, 0, P4::None, 0);
24680 b.emit_op(Opcode::Move, r1, r2, 2, P4::None, 0);
24681 b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24682 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24683 b.resolve_label(end);
24684 });
24685 assert_eq!(
24686 rows[0],
24687 vec![
24688 SqliteValue::Null,
24689 SqliteValue::Integer(11),
24690 SqliteValue::Integer(22),
24691 ]
24692 );
24693 }
24694
24695 #[test]
24696 fn test_move_register_range_preserves_backward_overlap() {
24697 let rows = run_program(|b| {
24698 let end = b.emit_label();
24699 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24700 let r1 = b.alloc_reg();
24701 let r2 = b.alloc_reg();
24702 let r3 = b.alloc_reg();
24703 b.emit_op(Opcode::Integer, 11, r1, 0, P4::None, 0);
24704 b.emit_op(Opcode::Integer, 22, r2, 0, P4::None, 0);
24705 b.emit_op(Opcode::Integer, 33, r3, 0, P4::None, 0);
24706 b.emit_op(Opcode::Move, r2, r1, 2, P4::None, 0);
24707 b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
24708 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24709 b.resolve_label(end);
24710 });
24711 assert_eq!(
24712 rows[0],
24713 vec![
24714 SqliteValue::Integer(22),
24715 SqliteValue::Integer(33),
24716 SqliteValue::Null,
24717 ]
24718 );
24719 }
24720
24721 #[test]
24722 fn test_copy_register() {
24723 let rows = run_program(|b| {
24724 let end = b.emit_label();
24725 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24726 let src = b.alloc_reg();
24727 let dst = b.alloc_reg();
24728 b.emit_op(Opcode::String8, 0, src, 0, P4::Str("copy_me".to_owned()), 0);
24729 b.emit_op(Opcode::Copy, src, dst, 0, P4::None, 0);
24730 b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24732 b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24733 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24734 b.resolve_label(end);
24735 });
24736 assert_eq!(rows[0], vec![SqliteValue::Text("copy_me".into())]);
24737 assert_eq!(rows[1], vec![SqliteValue::Text("copy_me".into())]);
24738 }
24739
24740 #[test]
24741 fn test_copy_integer_updates_payload_and_preserves_write_bookkeeping() {
24742 let mut engine = VdbeEngine::new(4);
24743 engine.set_reg(1, SqliteValue::Integer(41));
24744 engine.set_reg(2, SqliteValue::Integer(7));
24745 engine.set_register_subtype(2, 74);
24746
24747 engine.copy_single_reg(1, 2);
24748 assert_eq!(engine.get_reg(2), &SqliteValue::Integer(41));
24749 assert!(engine.register_subtype(2).is_none());
24750
24751 engine.set_register_subtype(1, 74);
24752 engine.copy_single_reg(1, 1);
24753 assert_eq!(engine.get_reg(1), &SqliteValue::Integer(41));
24754 assert!(engine.register_subtype(1).is_none());
24755
24756 let growing_dst = i32::try_from(engine.registers.len()).expect("small register file");
24757 engine.copy_single_reg(1, growing_dst);
24758 assert_eq!(engine.get_reg(growing_dst), &SqliteValue::Integer(41));
24759 }
24760
24761 #[test]
24762 fn test_scopy_register_via_hot_path() {
24763 let rows = run_program(|b| {
24768 let end = b.emit_label();
24769 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24770 let src = b.alloc_reg();
24771 let dst = b.alloc_reg();
24772 b.emit_op(
24773 Opcode::String8,
24774 0,
24775 src,
24776 0,
24777 P4::Str("shallow_me".to_owned()),
24778 0,
24779 );
24780 b.emit_op(Opcode::SCopy, src, dst, 0, P4::None, 0);
24781 b.emit_op(Opcode::ResultRow, src, 1, 0, P4::None, 0);
24782 b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24783 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24784 b.resolve_label(end);
24785 });
24786 assert_eq!(rows[0], vec![SqliteValue::Text("shallow_me".into())]);
24787 assert_eq!(rows[1], vec![SqliteValue::Text("shallow_me".into())]);
24788 }
24789
24790 #[test]
24791 fn test_string8_via_hot_path() {
24792 let rows = run_program(|b| {
24793 let end = b.emit_label();
24794 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24795 let r1 = b.alloc_reg();
24796 let r2 = b.alloc_reg();
24797 b.emit_op(
24798 Opcode::String8,
24799 0,
24800 r1,
24801 0,
24802 P4::Str("hello_hot".to_owned()),
24803 0,
24804 );
24805 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str(String::new()), 0);
24806 b.emit_op(Opcode::ResultRow, r1, 1, 0, P4::None, 0);
24807 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
24808 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24809 b.resolve_label(end);
24810 });
24811 assert_eq!(rows[0], vec![SqliteValue::Text("hello_hot".into())]);
24812 assert_eq!(rows[1], vec![SqliteValue::Text("".into())]);
24813 }
24814
24815 #[test]
24816 fn test_string8_hot_path_reuses_existing_text_buffer() {
24817 let rows = run_program(|b| {
24818 let end = b.emit_label();
24819 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24820 let r = b.alloc_reg();
24821 b.emit_op(
24822 Opcode::String8,
24823 0,
24824 r,
24825 0,
24826 P4::Str("first_value".to_owned()),
24827 0,
24828 );
24829 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24830 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("second".to_owned()), 0);
24831 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
24832 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24833 b.resolve_label(end);
24834 });
24835 assert_eq!(rows[0], vec![SqliteValue::Text("first_value".into())]);
24836 assert_eq!(rows[1], vec![SqliteValue::Text("second".into())]);
24837 }
24838
24839 #[test]
24840 fn test_intcopy_coerces() {
24841 let rows = run_program(|b| {
24843 let end = b.emit_label();
24844 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24845 let src = b.alloc_reg();
24846 let dst = b.alloc_reg();
24847 b.emit_op(Opcode::Real, 0, src, 0, P4::Real(3.7), 0);
24848 b.emit_op(Opcode::IntCopy, src, dst, 0, P4::None, 0);
24849 b.emit_op(Opcode::ResultRow, dst, 1, 0, P4::None, 0);
24850 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24851 b.resolve_label(end);
24852 });
24853 assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
24854 }
24855
24856 #[test]
24859 fn test_subtract_integers() {
24860 let rows = run_program(|b| {
24861 let end = b.emit_label();
24862 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24863 let r1 = b.alloc_reg();
24864 let r2 = b.alloc_reg();
24865 let r3 = b.alloc_reg();
24866 b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
24867 b.emit_op(Opcode::Integer, 3, r2, 0, P4::None, 0);
24868 b.emit_op(Opcode::Subtract, r2, r1, r3, P4::None, 0);
24870 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
24871 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24872 b.resolve_label(end);
24873 });
24874 assert_eq!(rows[0], vec![SqliteValue::Integer(7)]);
24875 }
24876
24877 #[test]
24878 fn test_multiply_large() {
24879 let rows = run_program(|b| {
24880 let end = b.emit_label();
24881 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24882 let r1 = b.alloc_reg();
24883 let r2 = b.alloc_reg();
24884 let r3 = b.alloc_reg();
24885 b.emit_op(Opcode::Integer, 100, r1, 0, P4::None, 0);
24886 b.emit_op(Opcode::Integer, 200, r2, 0, P4::None, 0);
24887 b.emit_op(Opcode::Multiply, r1, r2, r3, P4::None, 0);
24888 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
24889 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24890 b.resolve_label(end);
24891 });
24892 assert_eq!(rows[0], vec![SqliteValue::Integer(20_000)]);
24893 }
24894
24895 #[test]
24896 fn test_integer_division_truncates() {
24897 let rows = run_program(|b| {
24899 let end = b.emit_label();
24900 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24901 let r_divisor = b.alloc_reg();
24902 let r_dividend = b.alloc_reg();
24903 let r_result = b.alloc_reg();
24904 b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24905 b.emit_op(Opcode::Integer, 7, r_dividend, 0, P4::None, 0);
24906 b.emit_op(Opcode::Divide, r_divisor, r_dividend, r_result, P4::None, 0);
24908 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24909 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24910 b.resolve_label(end);
24911 });
24912 assert_eq!(rows[0], vec![SqliteValue::Integer(3)]);
24913 }
24914
24915 #[test]
24916 fn test_remainder_integers() {
24917 let rows = run_program(|b| {
24919 let end = b.emit_label();
24920 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24921 let r_divisor = b.alloc_reg();
24922 let r_dividend = b.alloc_reg();
24923 let r_result = b.alloc_reg();
24924 b.emit_op(Opcode::Integer, 3, r_divisor, 0, P4::None, 0);
24925 b.emit_op(Opcode::Integer, 7, r_dividend, 0, P4::None, 0);
24926 b.emit_op(
24927 Opcode::Remainder,
24928 r_divisor,
24929 r_dividend,
24930 r_result,
24931 P4::None,
24932 0,
24933 );
24934 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24935 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24936 b.resolve_label(end);
24937 });
24938 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
24939 }
24940
24941 #[test]
24942 fn test_remainder_by_zero() {
24943 let rows = run_program(|b| {
24944 let end = b.emit_label();
24945 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24946 let r_zero = b.alloc_reg();
24947 let r_val = b.alloc_reg();
24948 let r_result = b.alloc_reg();
24949 b.emit_op(Opcode::Integer, 0, r_zero, 0, P4::None, 0);
24950 b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
24951 b.emit_op(Opcode::Remainder, r_zero, r_val, r_result, P4::None, 0);
24952 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24953 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24954 b.resolve_label(end);
24955 });
24956 assert_eq!(rows[0], vec![SqliteValue::Null]);
24957 }
24958
24959 #[test]
24960 fn test_divide_text_prefix_uses_integer_path() {
24961 let rows = run_program(|b| {
24962 let end = b.emit_label();
24963 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24964 let r_divisor = b.alloc_reg();
24965 let r_dividend = b.alloc_reg();
24966 let r_result = b.alloc_reg();
24967 b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24968 b.emit_op(
24969 Opcode::String8,
24970 0,
24971 r_dividend,
24972 0,
24973 P4::Str("123abc".to_owned()),
24974 0,
24975 );
24976 b.emit_op(Opcode::Divide, r_divisor, r_dividend, r_result, P4::None, 0);
24977 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
24978 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
24979 b.resolve_label(end);
24980 });
24981 assert_eq!(rows[0], vec![SqliteValue::Integer(61)]);
24982 }
24983
24984 #[test]
24985 fn test_remainder_blob_prefix_uses_integer_path() {
24986 let rows = run_program(|b| {
24987 let end = b.emit_label();
24988 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
24989 let r_divisor = b.alloc_reg();
24990 let r_dividend = b.alloc_reg();
24991 let r_result = b.alloc_reg();
24992 b.emit_op(Opcode::Integer, 2, r_divisor, 0, P4::None, 0);
24993 b.emit_op(
24994 Opcode::Blob,
24995 4,
24996 r_dividend,
24997 0,
24998 P4::Blob(b"123a".to_vec()),
24999 0,
25000 );
25001 b.emit_op(
25002 Opcode::Remainder,
25003 r_divisor,
25004 r_dividend,
25005 r_result,
25006 P4::None,
25007 0,
25008 );
25009 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25010 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25011 b.resolve_label(end);
25012 });
25013 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25014 }
25015
25016 #[test]
25017 fn test_null_arithmetic_propagation() {
25018 let rows = run_program(|b| {
25020 let end = b.emit_label();
25021 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25022 let r_null = b.alloc_reg();
25023 let r_one = b.alloc_reg();
25024 let r_add = b.alloc_reg();
25025 let r_mul = b.alloc_reg();
25026 let r_sub = b.alloc_reg();
25027 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25028 b.emit_op(Opcode::Integer, 5, r_one, 0, P4::None, 0);
25029 b.emit_op(Opcode::Add, r_null, r_one, r_add, P4::None, 0);
25030 b.emit_op(Opcode::Multiply, r_null, r_one, r_mul, P4::None, 0);
25031 b.emit_op(Opcode::Subtract, r_null, r_one, r_sub, P4::None, 0);
25032 b.emit_op(Opcode::ResultRow, r_add, 1, 0, P4::None, 0);
25033 b.emit_op(Opcode::ResultRow, r_mul, 1, 0, P4::None, 0);
25034 b.emit_op(Opcode::ResultRow, r_sub, 1, 0, P4::None, 0);
25035 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25036 b.resolve_label(end);
25037 });
25038 assert_eq!(rows[0], vec![SqliteValue::Null]);
25039 assert_eq!(rows[1], vec![SqliteValue::Null]);
25040 assert_eq!(rows[2], vec![SqliteValue::Null]);
25041 }
25042
25043 #[test]
25044 fn test_add_imm() {
25045 let rows = run_program(|b| {
25047 let end = b.emit_label();
25048 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25049 let r = b.alloc_reg();
25050 b.emit_op(Opcode::Integer, 100, r, 0, P4::None, 0);
25051 b.emit_op(Opcode::AddImm, r, 50, 0, P4::None, 0);
25052 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25053 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25054 b.resolve_label(end);
25055 });
25056 assert_eq!(rows[0], vec![SqliteValue::Integer(150)]);
25057 }
25058
25059 #[test]
25060 fn test_add_imm_negative() {
25061 let rows = run_program(|b| {
25062 let end = b.emit_label();
25063 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25064 let r = b.alloc_reg();
25065 b.emit_op(Opcode::Integer, 100, r, 0, P4::None, 0);
25066 b.emit_op(Opcode::AddImm, r, -30, 0, P4::None, 0);
25067 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25068 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25069 b.resolve_label(end);
25070 });
25071 assert_eq!(rows[0], vec![SqliteValue::Integer(70)]);
25072 }
25073
25074 #[test]
25077 fn test_bit_and() {
25078 let rows = run_program(|b| {
25080 let end = b.emit_label();
25081 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25082 let r1 = b.alloc_reg();
25083 let r2 = b.alloc_reg();
25084 let r3 = b.alloc_reg();
25085 b.emit_op(Opcode::Integer, 0xFF, r1, 0, P4::None, 0);
25086 b.emit_op(Opcode::Integer, 0x0F, r2, 0, P4::None, 0);
25087 b.emit_op(Opcode::BitAnd, r1, r2, r3, P4::None, 0);
25088 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25089 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25090 b.resolve_label(end);
25091 });
25092 assert_eq!(rows[0], vec![SqliteValue::Integer(0x0F)]);
25093 }
25094
25095 #[test]
25096 fn test_bit_or() {
25097 let rows = run_program(|b| {
25099 let end = b.emit_label();
25100 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25101 let r1 = b.alloc_reg();
25102 let r2 = b.alloc_reg();
25103 let r3 = b.alloc_reg();
25104 b.emit_op(Opcode::Integer, 0xF0, r1, 0, P4::None, 0);
25105 b.emit_op(Opcode::Integer, 0x0F, r2, 0, P4::None, 0);
25106 b.emit_op(Opcode::BitOr, r1, r2, r3, P4::None, 0);
25107 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25108 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25109 b.resolve_label(end);
25110 });
25111 assert_eq!(rows[0], vec![SqliteValue::Integer(0xFF)]);
25112 }
25113
25114 #[test]
25115 fn test_shift_left() {
25116 let rows = run_program(|b| {
25118 let end = b.emit_label();
25119 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25120 let r_amount = b.alloc_reg();
25121 let r_val = b.alloc_reg();
25122 let r_result = b.alloc_reg();
25123 b.emit_op(Opcode::Integer, 8, r_amount, 0, P4::None, 0);
25124 b.emit_op(Opcode::Integer, 1, r_val, 0, P4::None, 0);
25125 b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25126 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25127 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25128 b.resolve_label(end);
25129 });
25130 assert_eq!(rows[0], vec![SqliteValue::Integer(256)]);
25131 }
25132
25133 #[test]
25134 fn test_shift_right() {
25135 let rows = run_program(|b| {
25137 let end = b.emit_label();
25138 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25139 let r_amount = b.alloc_reg();
25140 let r_val = b.alloc_reg();
25141 let r_result = b.alloc_reg();
25142 b.emit_op(Opcode::Integer, 4, r_amount, 0, P4::None, 0);
25143 b.emit_op(Opcode::Int64, 0, r_val, 0, P4::Int64(256), 0);
25144 b.emit_op(Opcode::ShiftRight, r_amount, r_val, r_result, P4::None, 0);
25145 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25146 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25147 b.resolve_label(end);
25148 });
25149 assert_eq!(rows[0], vec![SqliteValue::Integer(16)]);
25150 }
25151
25152 #[test]
25153 fn test_shift_left_overflow_clamp() {
25154 let rows = run_program(|b| {
25156 let end = b.emit_label();
25157 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25158 let r_amount = b.alloc_reg();
25159 let r_val = b.alloc_reg();
25160 let r_result = b.alloc_reg();
25161 b.emit_op(Opcode::Int64, 0, r_amount, 0, P4::Int64(64), 0);
25162 b.emit_op(Opcode::Integer, 1, r_val, 0, P4::None, 0);
25163 b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25164 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25165 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25166 b.resolve_label(end);
25167 });
25168 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25169 }
25170
25171 #[test]
25172 fn test_shift_negative_reverses() {
25173 let rows = run_program(|b| {
25175 let end = b.emit_label();
25176 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25177 let r_amount = b.alloc_reg();
25178 let r_val = b.alloc_reg();
25179 let r_result = b.alloc_reg();
25180 b.emit_op(Opcode::Int64, 0, r_amount, 0, P4::Int64(-2), 0);
25181 b.emit_op(Opcode::Integer, 8, r_val, 0, P4::None, 0);
25182 b.emit_op(Opcode::ShiftLeft, r_amount, r_val, r_result, P4::None, 0);
25183 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
25184 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25185 b.resolve_label(end);
25186 });
25187 assert_eq!(rows[0], vec![SqliteValue::Integer(2)]);
25189 }
25190
25191 #[test]
25192 fn test_bit_not() {
25193 let rows = run_program(|b| {
25195 let end = b.emit_label();
25196 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25197 let r1 = b.alloc_reg();
25198 let r2 = b.alloc_reg();
25199 b.emit_op(Opcode::Integer, 0, r1, 0, P4::None, 0);
25200 b.emit_op(Opcode::BitNot, r1, r2, 0, P4::None, 0);
25201 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
25202 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25203 b.resolve_label(end);
25204 });
25205 assert_eq!(rows[0], vec![SqliteValue::Integer(-1)]);
25206 }
25207
25208 #[test]
25209 fn test_bitwise_null_propagation() {
25210 let rows = run_program(|b| {
25211 let end = b.emit_label();
25212 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25213 let r_null = b.alloc_reg();
25214 let r_val = b.alloc_reg();
25215 let r_and = b.alloc_reg();
25216 let r_or = b.alloc_reg();
25217 let r_not = b.alloc_reg();
25218 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25219 b.emit_op(Opcode::Integer, 0xFF, r_val, 0, P4::None, 0);
25220 b.emit_op(Opcode::BitAnd, r_null, r_val, r_and, P4::None, 0);
25221 b.emit_op(Opcode::BitOr, r_null, r_val, r_or, P4::None, 0);
25222 b.emit_op(Opcode::BitNot, r_null, r_not, 0, P4::None, 0);
25223 b.emit_op(Opcode::ResultRow, r_and, 1, 0, P4::None, 0);
25224 b.emit_op(Opcode::ResultRow, r_or, 1, 0, P4::None, 0);
25225 b.emit_op(Opcode::ResultRow, r_not, 1, 0, P4::None, 0);
25226 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25227 b.resolve_label(end);
25228 });
25229 assert_eq!(rows[0], vec![SqliteValue::Null]);
25230 assert_eq!(rows[1], vec![SqliteValue::Null]);
25231 assert_eq!(rows[2], vec![SqliteValue::Null]);
25232 }
25233
25234 #[test]
25237 fn test_concat_two_strings() {
25238 let rows = run_program(|b| {
25239 let end = b.emit_label();
25240 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25241 let r1 = b.alloc_reg();
25242 let r2 = b.alloc_reg();
25243 let r3 = b.alloc_reg();
25244 b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("hello ".to_owned()), 0);
25245 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("world".to_owned()), 0);
25246 b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
25248 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25249 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25250 b.resolve_label(end);
25251 });
25252 assert_eq!(rows[0], vec![SqliteValue::Text("hello world".into())]);
25253 }
25254
25255 #[test]
25256 fn test_concat_empty_string() {
25257 let rows = run_program(|b| {
25258 let end = b.emit_label();
25259 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25260 let r1 = b.alloc_reg();
25261 let r2 = b.alloc_reg();
25262 let r3 = b.alloc_reg();
25263 b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("test".to_owned()), 0);
25264 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str(String::new()), 0);
25265 b.emit_op(Opcode::Concat, r2, r1, r3, P4::None, 0);
25266 b.emit_op(Opcode::ResultRow, r3, 1, 0, P4::None, 0);
25267 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25268 b.resolve_label(end);
25269 });
25270 assert_eq!(rows[0], vec![SqliteValue::Text("test".into())]);
25271 }
25272
25273 #[test]
25276 fn test_eq_jump_taken() {
25277 let rows = run_program(|b| {
25278 let end = b.emit_label();
25279 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25280 let r1 = b.alloc_reg();
25281 let r2 = b.alloc_reg();
25282 let r_out = b.alloc_reg();
25283 b.emit_op(Opcode::Integer, 42, r1, 0, P4::None, 0);
25284 b.emit_op(Opcode::Integer, 42, r2, 0, P4::None, 0);
25285 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25286 let taken = b.emit_label();
25287 b.emit_jump_to_label(Opcode::Eq, r1, r2, taken, P4::None, 0);
25289 let done = b.emit_label();
25290 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25291 b.resolve_label(taken);
25292 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25293 b.resolve_label(done);
25294 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25295 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25296 b.resolve_label(end);
25297 });
25298 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25299 }
25300
25301 #[test]
25302 fn test_ne_jump_taken() {
25303 let rows = run_program(|b| {
25304 let end = b.emit_label();
25305 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25306 let r1 = b.alloc_reg();
25307 let r2 = b.alloc_reg();
25308 let r_out = b.alloc_reg();
25309 b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
25310 b.emit_op(Opcode::Integer, 20, r2, 0, P4::None, 0);
25311 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25312 let taken = b.emit_label();
25313 b.emit_jump_to_label(Opcode::Ne, r1, r2, taken, P4::None, 0);
25314 let done = b.emit_label();
25315 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25316 b.resolve_label(taken);
25317 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25318 b.resolve_label(done);
25319 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25320 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25321 b.resolve_label(end);
25322 });
25323 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25324 }
25325
25326 #[test]
25327 fn test_lt_jump_taken() {
25328 let rows = run_program(|b| {
25329 let end = b.emit_label();
25330 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25331 let r_big = b.alloc_reg();
25332 let r_small = b.alloc_reg();
25333 let r_out = b.alloc_reg();
25334 b.emit_op(Opcode::Integer, 100, r_big, 0, P4::None, 0);
25335 b.emit_op(Opcode::Integer, 5, r_small, 0, P4::None, 0);
25336 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25337 let taken = b.emit_label();
25339 b.emit_jump_to_label(Opcode::Lt, r_big, r_small, taken, P4::None, 0);
25340 let done = b.emit_label();
25341 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25342 b.resolve_label(taken);
25343 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25344 b.resolve_label(done);
25345 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25346 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25347 b.resolve_label(end);
25348 });
25349 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25350 }
25351
25352 #[test]
25353 fn test_le_with_equal_values() {
25354 let rows = run_program(|b| {
25355 let end = b.emit_label();
25356 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25357 let r1 = b.alloc_reg();
25358 let r2 = b.alloc_reg();
25359 let r_out = b.alloc_reg();
25360 b.emit_op(Opcode::Integer, 7, r1, 0, P4::None, 0);
25361 b.emit_op(Opcode::Integer, 7, r2, 0, P4::None, 0);
25362 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25363 let taken = b.emit_label();
25364 b.emit_jump_to_label(Opcode::Le, r1, r2, taken, P4::None, 0);
25365 let done = b.emit_label();
25366 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25367 b.resolve_label(taken);
25368 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25369 b.resolve_label(done);
25370 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25371 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25372 b.resolve_label(end);
25373 });
25374 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25375 }
25376
25377 #[test]
25378 fn test_ge_with_greater_value() {
25379 let rows = run_program(|b| {
25380 let end = b.emit_label();
25381 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25382 let r_big = b.alloc_reg();
25383 let r_small = b.alloc_reg();
25384 let r_out = b.alloc_reg();
25385 b.emit_op(Opcode::Integer, 5, r_small, 0, P4::None, 0);
25386 b.emit_op(Opcode::Integer, 100, r_big, 0, P4::None, 0);
25387 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25388 let taken = b.emit_label();
25389 b.emit_jump_to_label(Opcode::Ge, r_small, r_big, taken, P4::None, 0);
25391 let done = b.emit_label();
25392 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25393 b.resolve_label(taken);
25394 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25395 b.resolve_label(done);
25396 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25397 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25398 b.resolve_label(end);
25399 });
25400 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25401 }
25402
25403 #[test]
25404 fn test_comparison_null_no_jump() {
25405 let rows = run_program(|b| {
25407 let end = b.emit_label();
25408 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25409 let r_null = b.alloc_reg();
25410 let r_five = b.alloc_reg();
25411 let r_out = b.alloc_reg();
25412 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25413 b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25414 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25415 let taken = b.emit_label();
25416 b.emit_jump_to_label(Opcode::Eq, r_five, r_null, taken, P4::None, 0);
25417 let done = b.emit_label();
25418 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25419 b.resolve_label(taken);
25420 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25421 b.resolve_label(done);
25422 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25423 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25424 b.resolve_label(end);
25425 });
25426 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25428 }
25429
25430 #[test]
25431 fn test_ne_nulleq_one_null() {
25432 let rows = run_program(|b| {
25434 let end = b.emit_label();
25435 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25436 let r_null = b.alloc_reg();
25437 let r_five = b.alloc_reg();
25438 let r_out = b.alloc_reg();
25439 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25440 b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25441 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25442 let taken = b.emit_label();
25443 b.emit_jump_to_label(Opcode::Ne, r_five, r_null, taken, P4::None, 0x80);
25444 let done = b.emit_label();
25445 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25446 b.resolve_label(taken);
25447 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25448 b.resolve_label(done);
25449 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25450 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25451 b.resolve_label(end);
25452 });
25453 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25454 }
25455
25456 #[test]
25457 fn test_comparison_jumpifnull_flag() {
25458 let rows = run_program(|b| {
25460 let end = b.emit_label();
25461 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25462 let r_null = b.alloc_reg();
25463 let r_five = b.alloc_reg();
25464 let r_out = b.alloc_reg();
25465 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25466 b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25467 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25468 let taken = b.emit_label();
25469 b.emit_jump_to_label(Opcode::Eq, r_five, r_null, taken, P4::None, 0x10);
25471 let done = b.emit_label();
25472 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25473 b.resolve_label(taken);
25474 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25475 b.resolve_label(done);
25476 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25477 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25478 b.resolve_label(end);
25479 });
25480 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25482 }
25483
25484 #[test]
25485 fn test_comparison_storep2_non_null() {
25486 let rows = run_program(|b| {
25488 let end = b.emit_label();
25489 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25490 let r_a = b.alloc_reg();
25491 let r_b = b.alloc_reg();
25492 let o_eq = b.alloc_reg();
25493 let o_ne = b.alloc_reg();
25494 let o_lt = b.alloc_reg();
25495 b.emit_op(Opcode::Integer, 5, r_a, 0, P4::None, 0);
25496 b.emit_op(Opcode::Integer, 10, r_b, 0, P4::None, 0);
25497 b.emit_op(Opcode::Eq, r_b, o_eq, r_a, P4::None, 0x20);
25499 b.emit_op(Opcode::Ne, r_b, o_ne, r_a, P4::None, 0x20);
25501 b.emit_op(Opcode::Lt, r_b, o_lt, r_a, P4::None, 0x20);
25503 b.emit_op(Opcode::ResultRow, o_eq, 3, 0, P4::None, 0);
25504 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25505 b.resolve_label(end);
25506 });
25507 assert_eq!(
25508 rows[0],
25509 vec![
25510 SqliteValue::Integer(0), SqliteValue::Integer(1), SqliteValue::Integer(1), ]
25514 );
25515 }
25516
25517 #[test]
25518 fn test_comparison_storep2_null_gives_null() {
25519 let rows = run_program(|b| {
25521 let end = b.emit_label();
25522 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25523 let r_null = b.alloc_reg();
25524 let r_five = b.alloc_reg();
25525 let o1 = b.alloc_reg();
25526 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25527 b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
25528 b.emit_op(Opcode::Eq, r_five, o1, r_null, P4::None, 0x20);
25530 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
25531 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25532 b.resolve_label(end);
25533 });
25534 assert_eq!(rows[0], vec![SqliteValue::Null]);
25535 }
25536
25537 #[test]
25540 fn test_not_null_is_null() {
25541 let rows = run_program(|b| {
25542 let end = b.emit_label();
25543 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25544 let r_null = b.alloc_reg();
25545 let r_out = b.alloc_reg();
25546 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
25547 b.emit_op(Opcode::Not, r_null, r_out, 0, P4::None, 0);
25548 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25549 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25550 b.resolve_label(end);
25551 });
25552 assert_eq!(rows[0], vec![SqliteValue::Null]);
25553 }
25554
25555 #[test]
25556 fn test_not_zero_is_one() {
25557 let rows = run_program(|b| {
25558 let end = b.emit_label();
25559 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25560 let r_zero = b.alloc_reg();
25561 let r_out = b.alloc_reg();
25562 b.emit_op(Opcode::Integer, 0, r_zero, 0, P4::None, 0);
25563 b.emit_op(Opcode::Not, r_zero, r_out, 0, P4::None, 0);
25564 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25565 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25566 b.resolve_label(end);
25567 });
25568 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25569 }
25570
25571 #[test]
25572 fn test_not_nonzero_is_zero() {
25573 let rows = run_program(|b| {
25574 let end = b.emit_label();
25575 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25576 let r_val = b.alloc_reg();
25577 let r_out = b.alloc_reg();
25578 b.emit_op(Opcode::Integer, 42, r_val, 0, P4::None, 0);
25579 b.emit_op(Opcode::Not, r_val, r_out, 0, P4::None, 0);
25580 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25581 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25582 b.resolve_label(end);
25583 });
25584 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25585 }
25586
25587 #[test]
25590 fn test_if_true_jumps() {
25591 let rows = run_program(|b| {
25592 let end = b.emit_label();
25593 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25594 let r_cond = b.alloc_reg();
25595 let r_out = b.alloc_reg();
25596 b.emit_op(Opcode::Integer, 1, r_cond, 0, P4::None, 0);
25597 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25598 let taken = b.emit_label();
25599 b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25600 let done = b.emit_label();
25601 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25602 b.resolve_label(taken);
25603 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25604 b.resolve_label(done);
25605 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25606 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25607 b.resolve_label(end);
25608 });
25609 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25610 }
25611
25612 #[test]
25613 fn test_if_false_no_jump() {
25614 let rows = run_program(|b| {
25615 let end = b.emit_label();
25616 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25617 let r_cond = b.alloc_reg();
25618 let r_out = b.alloc_reg();
25619 b.emit_op(Opcode::Integer, 0, r_cond, 0, P4::None, 0);
25620 b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
25621 let taken = b.emit_label();
25622 b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25623 let done = b.emit_label();
25624 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25625 b.resolve_label(taken);
25626 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25627 b.resolve_label(done);
25628 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25629 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25630 b.resolve_label(end);
25631 });
25632 assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
25634 }
25635
25636 #[test]
25637 fn test_if_null_no_jump() {
25638 let rows = run_program(|b| {
25639 let end = b.emit_label();
25640 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25641 let r_cond = b.alloc_reg();
25642 let r_out = b.alloc_reg();
25643 b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25644 b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
25645 let taken = b.emit_label();
25646 b.emit_jump_to_label(Opcode::If, r_cond, 0, taken, P4::None, 0);
25647 let done = b.emit_label();
25648 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25649 b.resolve_label(taken);
25650 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25651 b.resolve_label(done);
25652 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25653 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25654 b.resolve_label(end);
25655 });
25656 assert_eq!(rows[0], vec![SqliteValue::Integer(99)]);
25658 }
25659
25660 #[test]
25661 fn test_ifnot_false_jumps() {
25662 let rows = run_program(|b| {
25663 let end = b.emit_label();
25664 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25665 let r_cond = b.alloc_reg();
25666 let r_out = b.alloc_reg();
25667 b.emit_op(Opcode::Integer, 0, r_cond, 0, P4::None, 0);
25668 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25669 let taken = b.emit_label();
25670 b.emit_jump_to_label(Opcode::IfNot, r_cond, 0, taken, P4::None, 0);
25671 let done = b.emit_label();
25672 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25673 b.resolve_label(taken);
25674 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25675 b.resolve_label(done);
25676 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25677 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25678 b.resolve_label(end);
25679 });
25680 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25681 }
25682
25683 #[test]
25684 fn test_ifnot_null_p3_zero_no_jump() {
25685 let rows = run_program(|b| {
25687 let end = b.emit_label();
25688 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25689 let r_cond = b.alloc_reg();
25690 let r_out = b.alloc_reg();
25691 b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25692 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25693 let taken = b.emit_label();
25694 b.emit_jump_to_label(Opcode::IfNot, r_cond, 0, taken, P4::None, 0);
25695 let done = b.emit_label();
25696 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25697 b.resolve_label(taken);
25698 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25699 b.resolve_label(done);
25700 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25701 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25702 b.resolve_label(end);
25703 });
25704 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
25706 }
25707
25708 #[test]
25709 fn test_ifnot_null_p3_one_jumps() {
25710 let rows = run_program(|b| {
25712 let end = b.emit_label();
25713 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25714 let r_cond = b.alloc_reg();
25715 let r_out = b.alloc_reg();
25716 b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25717 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25718 let taken = b.emit_label();
25719 b.emit_jump_to_label(Opcode::IfNot, r_cond, 1, taken, P4::None, 0);
25720 let done = b.emit_label();
25721 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25722 b.resolve_label(taken);
25723 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25724 b.resolve_label(done);
25725 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25726 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25727 b.resolve_label(end);
25728 });
25729 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25731 }
25732
25733 #[test]
25734 fn test_if_null_p3_one_jumps() {
25735 let rows = run_program(|b| {
25737 let end = b.emit_label();
25738 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25739 let r_cond = b.alloc_reg();
25740 let r_out = b.alloc_reg();
25741 b.emit_op(Opcode::Null, 0, r_cond, 0, P4::None, 0);
25742 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25743 let taken = b.emit_label();
25744 b.emit_jump_to_label(Opcode::If, r_cond, 1, taken, P4::None, 0);
25745 let done = b.emit_label();
25746 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25747 b.resolve_label(taken);
25748 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25749 b.resolve_label(done);
25750 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25751 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25752 b.resolve_label(end);
25753 });
25754 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25756 }
25757
25758 #[test]
25759 fn test_once_fires_only_once() {
25760 let rows = run_program(|b| {
25763 let end = b.emit_label();
25764 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25765 let r_counter = b.alloc_reg();
25766 b.emit_op(Opcode::Integer, 0, r_counter, 0, P4::None, 0);
25767
25768 let loop_start = b.emit_label();
25769 b.resolve_label(loop_start);
25770 let skip_init = b.emit_label();
25773 b.emit_jump_to_label(Opcode::Once, 0, 0, skip_init, P4::None, 0);
25774 b.emit_op(Opcode::AddImm, r_counter, 1, 0, P4::None, 0);
25776 b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
25777 b.resolve_label(skip_init);
25779 b.emit_op(Opcode::ResultRow, r_counter, 1, 0, P4::None, 0);
25780 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25781 b.resolve_label(end);
25782 });
25783 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
25786 }
25787
25788 #[test]
25789 fn test_open_autoindex_without_memdb_supports_idxinsert_and_found() {
25790 let rows = run_program(|b| {
25791 let end = b.emit_label();
25792 let found = b.emit_label();
25793 let done = b.emit_label();
25794 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25795
25796 let cursor_id = 1;
25797 let r_insert_val = b.alloc_reg();
25798 let r_insert_key = b.alloc_reg();
25799 let r_probe_val = b.alloc_reg();
25800 let r_probe_key = b.alloc_reg();
25801 let r_out = b.alloc_reg();
25802
25803 b.emit_op(Opcode::OpenAutoindex, cursor_id, 1, 0, P4::None, 0);
25804 b.emit_op(Opcode::Integer, 7, r_insert_val, 0, P4::None, 0);
25805 b.emit_op(
25806 Opcode::MakeRecord,
25807 r_insert_val,
25808 1,
25809 r_insert_key,
25810 P4::None,
25811 0,
25812 );
25813 b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25814
25815 b.emit_op(Opcode::Integer, 7, r_probe_val, 0, P4::None, 0);
25816 b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
25817 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25818 b.emit_jump_to_label(Opcode::Found, cursor_id, r_probe_key, found, P4::None, 0);
25819 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25820 b.resolve_label(found);
25821 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25822 b.resolve_label(done);
25823
25824 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25825 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25826 b.resolve_label(end);
25827 });
25828 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
25829 }
25830
25831 #[test]
25832 fn test_open_autoindex_honors_nocase_collation_for_found() {
25833 let rows = run_program(|b| {
25834 let end = b.emit_label();
25835 let found = b.emit_label();
25836 let done = b.emit_label();
25837 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25838
25839 let cursor_id = 1;
25840 let r_insert_val = b.alloc_reg();
25841 let r_insert_key = b.alloc_reg();
25842 let r_probe_val = b.alloc_reg();
25843 let r_probe_key = b.alloc_reg();
25844 let r_out = b.alloc_reg();
25845
25846 b.emit_op(
25847 Opcode::OpenAutoindex,
25848 cursor_id,
25849 1,
25850 0,
25851 P4::Collation("NOCASE".to_owned()),
25852 0,
25853 );
25854 b.emit_op(
25855 Opcode::String8,
25856 0,
25857 r_insert_val,
25858 0,
25859 P4::Str("alpha".to_owned()),
25860 0,
25861 );
25862 b.emit_op(
25863 Opcode::MakeRecord,
25864 r_insert_val,
25865 1,
25866 r_insert_key,
25867 P4::None,
25868 0,
25869 );
25870 b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25871
25872 b.emit_op(
25873 Opcode::String8,
25874 0,
25875 r_probe_val,
25876 0,
25877 P4::Str("ALPHA".to_owned()),
25878 0,
25879 );
25880 b.emit_op(Opcode::MakeRecord, r_probe_val, 1, r_probe_key, P4::None, 0);
25881 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
25882 b.emit_jump_to_label(Opcode::Found, cursor_id, r_probe_key, found, P4::None, 0);
25883 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
25884 b.resolve_label(found);
25885 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
25886 b.resolve_label(done);
25887
25888 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25889 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25890 b.resolve_label(end);
25891 });
25892 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
25893 }
25894
25895 #[test]
25896 fn test_column_reads_from_key_only_autoindex_without_trailing_rowid() {
25897 let rows = run_program(|b| {
25898 let end = b.emit_label();
25899 let done = b.emit_label();
25900 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25901
25902 let cursor_id = 1;
25903 let r_insert_val = b.alloc_reg();
25904 let r_insert_key = b.alloc_reg();
25905 let r_out = b.alloc_reg();
25906
25907 b.emit_op(Opcode::OpenAutoindex, cursor_id, 1, 0, P4::None, 0);
25908 b.emit_op(
25909 Opcode::String8,
25910 0,
25911 r_insert_val,
25912 0,
25913 P4::Str("alpha".to_owned()),
25914 0,
25915 );
25916 b.emit_op(
25917 Opcode::MakeRecord,
25918 r_insert_val,
25919 1,
25920 r_insert_key,
25921 P4::None,
25922 0,
25923 );
25924 b.emit_op(Opcode::IdxInsert, cursor_id, r_insert_key, 0, P4::None, 0);
25925 b.emit_jump_to_label(Opcode::Rewind, cursor_id, 0, done, P4::None, 0);
25926 b.emit_op(Opcode::Column, cursor_id, 0, r_out, P4::None, 0);
25927 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
25928 b.resolve_label(done);
25929 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25930 b.resolve_label(end);
25931 });
25932
25933 assert_eq!(rows, vec![vec![SqliteValue::Text("alpha".into())]]);
25934 }
25935
25936 #[test]
25939 fn test_cast_integer_to_text() {
25940 let rows = run_program(|b| {
25941 let end = b.emit_label();
25942 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25943 let r = b.alloc_reg();
25944 b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
25945 b.emit_op(Opcode::Cast, r, 66, 0, P4::None, 0);
25947 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25948 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25949 b.resolve_label(end);
25950 });
25951 assert_eq!(rows[0], vec![SqliteValue::Text("42".into())]);
25952 }
25953
25954 #[test]
25955 fn test_cast_text_to_integer() {
25956 let rows = run_program(|b| {
25957 let end = b.emit_label();
25958 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25959 let r = b.alloc_reg();
25960 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("123".to_owned()), 0);
25961 b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0);
25963 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25964 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25965 b.resolve_label(end);
25966 });
25967 assert_eq!(rows[0], vec![SqliteValue::Integer(123)]);
25968 }
25969
25970 #[test]
25971 fn test_cast_to_real() {
25972 let rows = run_program(|b| {
25973 let end = b.emit_label();
25974 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25975 let r = b.alloc_reg();
25976 b.emit_op(Opcode::Integer, 5, r, 0, P4::None, 0);
25977 b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0);
25979 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25980 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25981 b.resolve_label(end);
25982 });
25983 assert_eq!(rows[0], vec![SqliteValue::Float(5.0)]);
25984 }
25985
25986 #[test]
25987 fn test_cast_to_blob() {
25988 let rows = run_program(|b| {
25989 let end = b.emit_label();
25990 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
25991 let r = b.alloc_reg();
25992 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("hi".to_owned()), 0);
25993 b.emit_op(Opcode::Cast, r, 65, 0, P4::None, 0);
25995 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
25996 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
25997 b.resolve_label(end);
25998 });
25999 assert_eq!(rows[0], vec![SqliteValue::Blob(b"hi".to_vec().into())]);
26000 }
26001
26002 #[test]
26003 fn test_cast_text_sci_notation_to_integer() {
26004 let rows = run_program(|b| {
26007 let end = b.emit_label();
26008 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26009 let r = b.alloc_reg();
26010 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("1.5e2abc".to_owned()), 0);
26011 b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0); b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26013 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26014 b.resolve_label(end);
26015 });
26016 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26017 }
26018
26019 #[test]
26020 fn test_cast_text_huge_exponent_to_integer_ignores_exponent() {
26021 let rows = run_program(|b| {
26022 let end = b.emit_label();
26023 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26024 let r = b.alloc_reg();
26025 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("1e999".to_owned()), 0);
26026 b.emit_op(Opcode::Cast, r, 68, 0, P4::None, 0); b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26028 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26029 b.resolve_label(end);
26030 });
26031 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26032 }
26033
26034 #[test]
26035 fn test_cast_text_prefix_to_real() {
26036 let rows = run_program(|b| {
26038 let end = b.emit_label();
26039 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26040 let r = b.alloc_reg();
26041 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("3.14abc".to_owned()), 0);
26042 b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0); b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26044 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26045 b.resolve_label(end);
26046 });
26047 assert_eq!(rows[0], vec![SqliteValue::Float(3.14)]);
26048 }
26049
26050 #[test]
26051 fn test_cast_sci_notation_to_real() {
26052 let rows = run_program(|b| {
26054 let end = b.emit_label();
26055 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26056 let r = b.alloc_reg();
26057 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("2.5e3xyz".to_owned()), 0);
26058 b.emit_op(Opcode::Cast, r, 69, 0, P4::None, 0); b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26060 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26061 b.resolve_label(end);
26062 });
26063 assert_eq!(rows[0], vec![SqliteValue::Float(2500.0)]);
26064 }
26065
26066 #[test]
26067 fn test_cast_text_prefix_to_numeric() {
26068 let rows = run_program(|b| {
26069 let end = b.emit_label();
26070 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26071 let r1 = b.alloc_reg();
26072 let r2 = b.alloc_reg();
26073 let r3 = b.alloc_reg();
26074 b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("123abc".to_owned()), 0);
26075 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("1.5e2abc".to_owned()), 0);
26076 b.emit_op(Opcode::String8, 0, r3, 0, P4::Str("abc".to_owned()), 0);
26077 b.emit_op(Opcode::Cast, r1, 67, 0, P4::None, 0); b.emit_op(Opcode::Cast, r2, 67, 0, P4::None, 0);
26079 b.emit_op(Opcode::Cast, r3, 67, 0, P4::None, 0);
26080 b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
26081 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26082 b.resolve_label(end);
26083 });
26084 assert_eq!(
26085 rows[0],
26086 vec![
26087 SqliteValue::Integer(123),
26088 SqliteValue::Integer(150),
26089 SqliteValue::Integer(0),
26090 ]
26091 );
26092 }
26093
26094 #[test]
26095 fn test_must_be_int_accepts_integer() {
26096 let rows = run_program(|b| {
26097 let end = b.emit_label();
26098 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26099 let r = b.alloc_reg();
26100 b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26101 b.emit_op(Opcode::MustBeInt, r, 0, 0, P4::None, 0);
26103 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26104 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26105 b.resolve_label(end);
26106 });
26107 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26108 }
26109
26110 #[test]
26111 fn test_must_be_int_jumps_on_non_int() {
26112 let rows = run_program(|b| {
26114 let end = b.emit_label();
26115 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26116 let r = b.alloc_reg();
26117 let r_out = b.alloc_reg();
26118 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("not_int".to_owned()), 0);
26119 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
26120 let fallback = b.emit_label();
26121 b.emit_jump_to_label(Opcode::MustBeInt, r, 0, fallback, P4::None, 0);
26122 let done = b.emit_label();
26123 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
26124 b.resolve_label(fallback);
26125 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
26126 b.resolve_label(done);
26127 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
26128 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26129 b.resolve_label(end);
26130 });
26131 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26133 }
26134
26135 #[test]
26136 fn test_real_affinity_converts_int() {
26137 let rows = run_program(|b| {
26138 let end = b.emit_label();
26139 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26140 let r = b.alloc_reg();
26141 b.emit_op(Opcode::Integer, 7, r, 0, P4::None, 0);
26142 b.emit_op(Opcode::RealAffinity, r, 0, 0, P4::None, 0);
26143 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26144 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26145 b.resolve_label(end);
26146 });
26147 assert_eq!(rows[0], vec![SqliteValue::Float(7.0)]);
26148 }
26149
26150 #[test]
26151 fn test_real_affinity_no_op_on_float() {
26152 let rows = run_program(|b| {
26154 let end = b.emit_label();
26155 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26156 let r = b.alloc_reg();
26157 b.emit_op(Opcode::Real, 0, r, 0, P4::Real(std::f64::consts::PI), 0);
26158 b.emit_op(Opcode::RealAffinity, r, 0, 0, P4::None, 0);
26159 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26160 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26161 b.resolve_label(end);
26162 });
26163 assert_eq!(rows[0], vec![SqliteValue::Float(std::f64::consts::PI)]);
26164 }
26165
26166 #[test]
26169 fn test_halt_if_null_triggers() {
26170 let mut b = ProgramBuilder::new();
26171 let end = b.emit_label();
26172 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26173 let r = b.alloc_reg();
26174 b.emit_op(Opcode::Null, 0, r, 0, P4::None, 0);
26175 b.emit_op(
26176 Opcode::HaltIfNull,
26177 19,
26178 0,
26179 r,
26180 P4::Str("NOT NULL constraint failed".to_owned()),
26181 0,
26182 );
26183 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26184 b.resolve_label(end);
26185 let prog = b.finish().unwrap();
26186 let mut engine = VdbeEngine::new(prog.register_count());
26187 let outcome = run_async(engine.execute(&prog)).unwrap();
26188 assert_eq!(
26189 outcome,
26190 ExecOutcome::Error {
26191 code: 19,
26192 message: "NOT NULL constraint failed".to_owned(),
26193 }
26194 );
26195 }
26196
26197 #[test]
26198 fn test_halt_if_null_passes_non_null() {
26199 let rows = run_program(|b| {
26200 let end = b.emit_label();
26201 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26202 let r = b.alloc_reg();
26203 b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26204 b.emit_op(
26205 Opcode::HaltIfNull,
26206 19,
26207 0,
26208 r,
26209 P4::Str("should not fire".to_owned()),
26210 0,
26211 );
26212 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26213 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26214 b.resolve_label(end);
26215 });
26216 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26217 }
26218
26219 #[test]
26220 fn test_typecheck_reports_sqlite_constraint_datatype_marker() {
26221 let mut b = ProgramBuilder::new();
26222 let end = b.emit_label();
26223 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26224 let r = b.alloc_reg();
26225 b.emit_op(Opcode::String8, 0, r, 0, P4::Str("bad".to_owned()), 0);
26226 b.emit_op(
26228 Opcode::TypeCheck,
26229 r,
26230 1,
26231 0,
26232 P4::Str("I\ttbl\tcol_a".to_owned()),
26233 0,
26234 );
26235 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26236 b.resolve_label(end);
26237
26238 let prog = b.finish().expect("program should build");
26239 let mut engine = VdbeEngine::new(prog.register_count());
26240 let err = run_async(engine.execute(&prog))
26241 .expect_err("typecheck should fail for TEXT into INTEGER STRICT slot");
26242 let err_text = err.to_string();
26243 assert!(
26244 err_text.contains("cannot store"),
26245 "error should mention 'cannot store': {err_text}"
26246 );
26247 assert!(
26248 err_text.contains("tbl.col_a"),
26249 "error should mention column: {err_text}"
26250 );
26251 assert_eq!(err.error_code(), ErrorCode::Constraint);
26252 assert_eq!(err.extended_error_code(), 3091); }
26254
26255 #[test]
26258 fn test_zero_or_null_opcode() {
26259 let rows = run_program(|b| {
26260 let end = b.emit_label();
26261 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26262 let non_null_lhs = b.alloc_reg();
26263 let non_null_rhs = b.alloc_reg();
26264 let null_lhs = b.alloc_reg();
26265 let null_rhs = b.alloc_reg();
26266 let o1 = b.alloc_reg();
26267 let o2 = b.alloc_reg();
26268 let o3 = b.alloc_reg();
26269 b.emit_op(Opcode::Integer, 7, non_null_lhs, 0, P4::None, 0);
26270 b.emit_op(Opcode::Integer, 11, non_null_rhs, 0, P4::None, 0);
26271 b.emit_op(Opcode::Null, 0, null_lhs, 0, P4::None, 0);
26272 b.emit_op(Opcode::Null, 0, null_rhs, 0, P4::None, 0);
26273 b.emit_op(
26274 Opcode::ZeroOrNull,
26275 non_null_lhs,
26276 o1,
26277 non_null_rhs,
26278 P4::None,
26279 0,
26280 );
26281 b.emit_op(Opcode::ZeroOrNull, null_lhs, o2, non_null_rhs, P4::None, 0);
26282 b.emit_op(Opcode::ZeroOrNull, non_null_lhs, o3, null_rhs, P4::None, 0);
26283 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26284 b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26285 b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26286 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26287 b.resolve_label(end);
26288 });
26289 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]);
26290 assert_eq!(rows[1], vec![SqliteValue::Null]);
26291 assert_eq!(rows[2], vec![SqliteValue::Null]);
26292 }
26293
26294 #[test]
26295 fn test_is_true_opcode() {
26296 let rows = run_program(|b| {
26297 let end = b.emit_label();
26298 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26299 let r_true = b.alloc_reg();
26300 let r_false = b.alloc_reg();
26301 let r_null = b.alloc_reg();
26302 let o1 = b.alloc_reg();
26303 let o2 = b.alloc_reg();
26304 let o3 = b.alloc_reg();
26305 b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26306 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26307 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26308 b.emit_op(Opcode::IsTrue, r_true, o1, 0, P4::None, 0);
26309 b.emit_op(Opcode::IsTrue, r_false, o2, 0, P4::None, 0);
26310 b.emit_op(Opcode::IsTrue, r_null, o3, 0, P4::None, 0);
26311 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26312 b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26313 b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26314 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26315 b.resolve_label(end);
26316 });
26317 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); }
26321
26322 #[test]
26323 fn test_is_true_is_false_semantics() {
26324 let rows = run_program(|b| {
26326 let end = b.emit_label();
26327 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26328 let r_true = b.alloc_reg();
26329 let r_false = b.alloc_reg();
26330 let r_null = b.alloc_reg();
26331 let o1 = b.alloc_reg();
26332 let o2 = b.alloc_reg();
26333 let o3 = b.alloc_reg();
26334 b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26335 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26336 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26337 b.emit_op(Opcode::IsTrue, r_true, o1, 1, P4::Int(1), 0);
26338 b.emit_op(Opcode::IsTrue, r_false, o2, 1, P4::Int(1), 0);
26339 b.emit_op(Opcode::IsTrue, r_null, o3, 1, P4::Int(1), 0);
26340 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26341 b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26342 b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26343 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26344 b.resolve_label(end);
26345 });
26346 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); assert_eq!(rows[2], vec![SqliteValue::Integer(0)]); }
26350
26351 #[test]
26352 fn test_is_true_is_not_true_semantics() {
26353 let rows = run_program(|b| {
26355 let end = b.emit_label();
26356 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26357 let r_true = b.alloc_reg();
26358 let r_false = b.alloc_reg();
26359 let r_null = b.alloc_reg();
26360 let o1 = b.alloc_reg();
26361 let o2 = b.alloc_reg();
26362 let o3 = b.alloc_reg();
26363 b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26364 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26365 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26366 b.emit_op(Opcode::IsTrue, r_true, o1, 0, P4::Int(1), 0);
26367 b.emit_op(Opcode::IsTrue, r_false, o2, 0, P4::Int(1), 0);
26368 b.emit_op(Opcode::IsTrue, r_null, o3, 0, P4::Int(1), 0);
26369 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26370 b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26371 b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26372 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26373 b.resolve_label(end);
26374 });
26375 assert_eq!(rows[0], vec![SqliteValue::Integer(0)]); assert_eq!(rows[1], vec![SqliteValue::Integer(1)]); assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); }
26379
26380 #[test]
26381 fn test_is_true_is_not_false_semantics() {
26382 let rows = run_program(|b| {
26384 let end = b.emit_label();
26385 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26386 let r_true = b.alloc_reg();
26387 let r_false = b.alloc_reg();
26388 let r_null = b.alloc_reg();
26389 let o1 = b.alloc_reg();
26390 let o2 = b.alloc_reg();
26391 let o3 = b.alloc_reg();
26392 b.emit_op(Opcode::Integer, 42, r_true, 0, P4::None, 0);
26393 b.emit_op(Opcode::Integer, 0, r_false, 0, P4::None, 0);
26394 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
26395 b.emit_op(Opcode::IsTrue, r_true, o1, 1, P4::Int(0), 0);
26396 b.emit_op(Opcode::IsTrue, r_false, o2, 1, P4::Int(0), 0);
26397 b.emit_op(Opcode::IsTrue, r_null, o3, 1, P4::Int(0), 0);
26398 b.emit_op(Opcode::ResultRow, o1, 1, 0, P4::None, 0);
26399 b.emit_op(Opcode::ResultRow, o2, 1, 0, P4::None, 0);
26400 b.emit_op(Opcode::ResultRow, o3, 1, 0, P4::None, 0);
26401 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26402 b.resolve_label(end);
26403 });
26404 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]); assert_eq!(rows[1], vec![SqliteValue::Integer(0)]); assert_eq!(rows[2], vec![SqliteValue::Integer(1)]); }
26408
26409 #[test]
26410 fn test_noop_does_nothing() {
26411 let rows = run_program(|b| {
26412 let end = b.emit_label();
26413 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26414 let r = b.alloc_reg();
26415 b.emit_op(Opcode::Integer, 42, r, 0, P4::None, 0);
26416 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26417 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26418 b.emit_op(Opcode::Noop, 0, 0, 0, P4::None, 0);
26419 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26420 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26421 b.resolve_label(end);
26422 });
26423 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
26424 }
26425
26426 #[test]
26427 fn test_result_row_three_columns() {
26428 let rows = run_program(|b| {
26429 let end = b.emit_label();
26430 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26431 let r1 = b.alloc_reg();
26432 let r2 = b.alloc_reg();
26433 let r3 = b.alloc_reg();
26434 b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
26435 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("two".to_owned()), 0);
26436 b.emit_op(Opcode::Real, 0, r3, 0, P4::Real(3.0), 0);
26437 b.emit_op(Opcode::ResultRow, r1, 3, 0, P4::None, 0);
26438 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26439 b.resolve_label(end);
26440 });
26441 assert_eq!(
26442 rows[0],
26443 vec![
26444 SqliteValue::Integer(1),
26445 SqliteValue::Text("two".into()),
26446 SqliteValue::Float(3.0),
26447 ]
26448 );
26449 }
26450
26451 #[test]
26452 fn test_multiple_result_rows() {
26453 let rows = run_program(|b| {
26454 let end = b.emit_label();
26455 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26456 let r = b.alloc_reg();
26457 b.emit_op(Opcode::Integer, 1, r, 0, P4::None, 0);
26458 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26459 b.emit_op(Opcode::Integer, 2, r, 0, P4::None, 0);
26460 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26461 b.emit_op(Opcode::Integer, 3, r, 0, P4::None, 0);
26462 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
26463 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26464 b.resolve_label(end);
26465 });
26466 assert_eq!(rows.len(), 3);
26467 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
26468 assert_eq!(rows[1], vec![SqliteValue::Integer(2)]);
26469 assert_eq!(rows[2], vec![SqliteValue::Integer(3)]);
26470 }
26471
26472 #[test]
26473 fn test_gosub_nested() {
26474 let rows = run_program(|b| {
26476 let end = b.emit_label();
26477 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26478 let r_ret1 = b.alloc_reg();
26479 let r_ret2 = b.alloc_reg();
26480 let r_val = b.alloc_reg();
26481
26482 let sub1 = b.emit_label();
26484 b.emit_jump_to_label(Opcode::Gosub, r_ret1, 0, sub1, P4::None, 0);
26485 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26486 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26487
26488 b.resolve_label(sub1);
26490 b.emit_op(Opcode::Integer, 10, r_val, 0, P4::None, 0);
26491 let sub2 = b.emit_label();
26492 b.emit_jump_to_label(Opcode::Gosub, r_ret2, 0, sub2, P4::None, 0);
26493 b.emit_op(Opcode::AddImm, r_val, 1, 0, P4::None, 0);
26494 b.emit_op(Opcode::Return, r_ret1, 0, 0, P4::None, 0);
26495
26496 b.resolve_label(sub2);
26498 let r_five = b.alloc_reg();
26499 b.emit_op(Opcode::Integer, 5, r_five, 0, P4::None, 0);
26500 b.emit_op(Opcode::Multiply, r_five, r_val, r_val, P4::None, 0);
26501 b.emit_op(Opcode::Return, r_ret2, 0, 0, P4::None, 0);
26502
26503 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26504 b.resolve_label(end);
26505 });
26506 assert_eq!(rows[0], vec![SqliteValue::Integer(51)]);
26508 }
26509
26510 #[test]
26511 fn test_coroutine_yield_resume() {
26512 let rows = run_program(|b| {
26514 let end = b.emit_label();
26515 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26516
26517 let r_co = b.alloc_reg();
26518 let r_val = b.alloc_reg();
26519
26520 let init_addr = b.emit_op(Opcode::InitCoroutine, r_co, 0, 0, P4::None, 0);
26522 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
26523 let consumer_start = b.current_addr() as i32;
26524 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26525 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26526 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26527 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26528 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26529 b.emit_op(Opcode::ResultRow, r_val, 1, 0, P4::None, 0);
26530 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26531
26532 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
26533 let producer_start = b.current_addr() as i32;
26534 b.emit_op(Opcode::Integer, 100, r_val, 0, P4::None, 0);
26535 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26536 b.emit_op(Opcode::Integer, 200, r_val, 0, P4::None, 0);
26537 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26538 b.emit_op(Opcode::Integer, 300, r_val, 0, P4::None, 0);
26539 b.emit_op(Opcode::Yield, r_co, 0, 0, P4::None, 0);
26540 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26541
26542 if let Some(init_op) = b.op_at_mut(init_addr) {
26543 init_op.p2 = consumer_start;
26544 init_op.p3 = producer_start;
26545 }
26546
26547 b.resolve_label(end);
26548 });
26549 assert_eq!(rows.len(), 3);
26550 assert_eq!(rows[0], vec![SqliteValue::Integer(100)]);
26551 assert_eq!(rows[1], vec![SqliteValue::Integer(200)]);
26552 assert_eq!(rows[2], vec![SqliteValue::Integer(300)]);
26553 }
26554
26555 #[test]
26556 fn test_make_record_encodes_values() {
26557 let rows = run_program(|b| {
26559 let end = b.emit_label();
26560 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26561 let r1 = b.alloc_reg();
26562 let r2 = b.alloc_reg();
26563 let r_rec = b.alloc_reg();
26564 b.emit_op(Opcode::Integer, 1, r1, 0, P4::None, 0);
26565 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("a".to_owned()), 0);
26566 b.emit_op(Opcode::MakeRecord, r1, 2, r_rec, P4::None, 0);
26567 b.emit_op(Opcode::ResultRow, r_rec, 1, 0, P4::None, 0);
26568 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26569 b.resolve_label(end);
26570 });
26571 let produced_blob = rows.first().and_then(|row| row.first());
26572 assert!(
26573 matches!(produced_blob, Some(SqliteValue::Blob(_))),
26574 "MakeRecord should produce a blob"
26575 );
26576 let decoded = decode_record(&rows[0][0]).unwrap();
26577 assert_eq!(
26578 decoded,
26579 vec![SqliteValue::Integer(1), SqliteValue::Text("a".into())]
26580 );
26581 }
26582
26583 #[test]
26584 fn test_make_record_negative_source_register_stays_null() {
26585 let rows = run_program(|b| {
26586 let end = b.emit_label();
26587 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26588
26589 let r_record = b.alloc_reg();
26590 b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
26591 b.emit_op(Opcode::MakeRecord, -1, 1, r_record, P4::None, 0);
26592 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26593 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26594 b.resolve_label(end);
26595 });
26596
26597 let decoded = decode_record(&rows[0][0]).expect("record should decode");
26598 assert_eq!(decoded, vec![SqliteValue::Null]);
26599 }
26600
26601 #[test]
26602 fn test_make_record_precomputed_header_matches_generic_path() {
26603 let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26604 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26605 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26606 fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
26607 ]);
26608
26609 let generic_rows = run_program(|b| {
26610 let end = b.emit_label();
26611 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26612 let r_rowid = b.alloc_reg();
26613 let r_int = b.alloc_reg();
26614 let r_real = b.alloc_reg();
26615 let r_record = b.alloc_reg();
26616 b.emit_op(Opcode::Integer, 77, r_rowid, 0, P4::None, 0);
26617 b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
26618 b.emit_op(Opcode::Real, 0, r_real, 0, P4::Real(2.5), 0);
26619 b.emit_op(
26620 Opcode::MakeRecord,
26621 r_rowid,
26622 3,
26623 r_record,
26624 P4::Affinity("XDE".to_owned()),
26625 0,
26626 );
26627 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26628 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26629 b.resolve_label(end);
26630 });
26631
26632 let precomputed_rows = run_program(|b| {
26633 let end = b.emit_label();
26634 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26635 let r_rowid = b.alloc_reg();
26636 let r_int = b.alloc_reg();
26637 let r_real = b.alloc_reg();
26638 let r_record = b.alloc_reg();
26639 b.emit_op(Opcode::Integer, 77, r_rowid, 0, P4::None, 0);
26640 b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
26641 b.emit_op(Opcode::Real, 0, r_real, 0, P4::Real(2.5), 0);
26642 b.emit_op(
26643 Opcode::MakeRecord,
26644 r_rowid,
26645 3,
26646 r_record,
26647 P4::PrecomputedHeader(precomputed.clone()),
26648 0,
26649 );
26650 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26651 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26652 b.resolve_label(end);
26653 });
26654
26655 assert_eq!(precomputed_rows, generic_rows);
26656 let decoded = decode_record(&precomputed_rows[0][0]).expect("record should decode");
26657 assert_eq!(
26658 decoded,
26659 vec![
26660 SqliteValue::Null,
26661 SqliteValue::Integer(5),
26662 SqliteValue::Float(2.5),
26663 ]
26664 );
26665 }
26666
26667 #[test]
26668 fn test_make_record_precomputed_header_falls_back_on_column_count_mismatch() {
26669 let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26670 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26671 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26672 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26673 ]);
26674
26675 let rows = run_program(|b| {
26676 let end = b.emit_label();
26677 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26678 let r_a = b.alloc_reg();
26679 let r_b = b.alloc_reg();
26680 let r_c = b.alloc_reg();
26681 let r_record = b.alloc_reg();
26682 b.emit_op(Opcode::Integer, 11, r_a, 0, P4::None, 0);
26683 b.emit_op(Opcode::Integer, 22, r_b, 0, P4::None, 0);
26684 b.emit_op(Opcode::Integer, 33, r_c, 0, P4::None, 0);
26685 b.emit_op(
26686 Opcode::MakeRecord,
26687 r_a,
26688 2,
26689 r_record,
26690 P4::PrecomputedHeader(precomputed),
26691 0,
26692 );
26693 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26694 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26695 b.resolve_label(end);
26696 });
26697
26698 let decoded = decode_record(&rows[0][0]).expect("record should decode");
26699 assert_eq!(
26700 decoded,
26701 vec![SqliteValue::Integer(11), SqliteValue::Integer(22)],
26702 "stale precomputed headers must not widen the record shape",
26703 );
26704 }
26705
26706 #[test]
26707 fn test_make_record_affinity_shape_follows_p2_column_count() {
26708 let _guard = VDBE_OBSERVABILITY_LOCK
26709 .lock()
26710 .unwrap_or_else(|e| e.into_inner());
26711
26712 let rows = run_program(|b| {
26713 let end = b.emit_label();
26714 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26715 let r_a = b.alloc_reg();
26716 let r_b = b.alloc_reg();
26717 let r_c = b.alloc_reg();
26718 let r_record_short_affinity = b.alloc_reg();
26719 let r_record_long_affinity = b.alloc_reg();
26720 b.emit_op(Opcode::Integer, 11, r_a, 0, P4::None, 0);
26721 b.emit_op(Opcode::Integer, 22, r_b, 0, P4::None, 0);
26722 b.emit_op(Opcode::Integer, 33, r_c, 0, P4::None, 0);
26723 b.emit_op(
26724 Opcode::MakeRecord,
26725 r_a,
26726 2,
26727 r_record_short_affinity,
26728 P4::Affinity("X".to_owned()),
26729 0,
26730 );
26731 b.emit_op(
26732 Opcode::MakeRecord,
26733 r_a,
26734 1,
26735 r_record_long_affinity,
26736 P4::Affinity("XDE".to_owned()),
26737 0,
26738 );
26739 b.emit_op(
26740 Opcode::ResultRow,
26741 r_record_short_affinity,
26742 2,
26743 0,
26744 P4::None,
26745 0,
26746 );
26747 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26748 b.resolve_label(end);
26749 });
26750
26751 let short_affinity =
26752 decode_record(&rows[0][0]).expect("short-affinity record should decode");
26753 assert_eq!(
26754 short_affinity,
26755 vec![SqliteValue::Null, SqliteValue::Integer(22)],
26756 "a short affinity string must not shrink the P2 register range",
26757 );
26758 let long_affinity = decode_record(&rows[0][1]).expect("long-affinity record should decode");
26759 assert_eq!(
26760 long_affinity,
26761 vec![SqliteValue::Null],
26762 "a long affinity string must not widen the P2 register range",
26763 );
26764 }
26765
26766 #[test]
26767 fn test_compiled_simple_insert_applies_make_record_null_placeholders() {
26768 let mut affinity_values = vec![
26769 SqliteValue::Integer(77),
26770 SqliteValue::Text("kept".into()),
26771 SqliteValue::Integer(9),
26772 ];
26773 VdbeEngine::apply_make_record_null_placeholders(
26774 &mut affinity_values,
26775 &P4::Affinity("XBD".to_owned()),
26776 );
26777 assert_eq!(
26778 affinity_values,
26779 vec![
26780 SqliteValue::Null,
26781 SqliteValue::Text("kept".into()),
26782 SqliteValue::Integer(9),
26783 ],
26784 "compiled INSERT must preserve MakeRecord X-affinity IPK placeholders",
26785 );
26786
26787 let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26788 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26789 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26790 ]);
26791 let mut precomputed_values = vec![SqliteValue::Integer(88), SqliteValue::Integer(5)];
26792 VdbeEngine::apply_make_record_null_placeholders(
26793 &mut precomputed_values,
26794 &P4::PrecomputedHeader(precomputed),
26795 );
26796 assert_eq!(
26797 precomputed_values,
26798 vec![SqliteValue::Null, SqliteValue::Integer(5)],
26799 "compiled INSERT must preserve MakeRecord precomputed-header IPK placeholders",
26800 );
26801 }
26802
26803 #[test]
26804 fn test_compiled_record_builder_precomputed_header_matches_generic_and_logs_microbench() {
26805 let header = fsqlite_types::record::PrecomputedRecordHeader::new(&[
26806 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
26807 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
26808 fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
26809 ]);
26810 let builder = CompiledRecordBuilder::PrecomputedHeader(header);
26811 let mut values = vec![
26812 SqliteValue::Null,
26813 SqliteValue::Integer(200),
26814 SqliteValue::Float(2.5),
26815 ];
26816 let expected = serialize_record(&values);
26817
26818 let plan = build_compiled_record_write_plan(values.as_slice(), &builder);
26819 assert_eq!(plan.exact_size(), expected.len());
26820 let mut fused = vec![0; plan.exact_size()];
26821 plan.write_into_slice(&mut fused)
26822 .expect("precomputed record plan should write");
26823 assert_eq!(fused, expected);
26824
26825 let mut fallback_buf = Vec::new();
26826 serialize_compiled_record_into_vec(values.as_slice(), &builder, &mut fallback_buf);
26827 assert_eq!(fallback_buf, expected);
26828
26829 let iterations = 20_000usize;
26830 let mut generic_dst = Vec::new();
26831 let generic_start = Instant::now();
26832 for i in 0..iterations {
26833 values[1] = SqliteValue::Integer(200 + i64::try_from(i % 100).unwrap_or(0));
26834 let plan = fsqlite_types::record::plan_record_iter_serialization(values.iter());
26835 generic_dst.resize(plan.exact_size(), 0);
26836 plan.write_into_slice(generic_dst.as_mut_slice())
26837 .expect("generic plan should write");
26838 std::hint::black_box(&generic_dst);
26839 }
26840 let generic_elapsed = generic_start.elapsed();
26841
26842 let mut fused_dst = Vec::new();
26843 let fused_start = Instant::now();
26844 for i in 0..iterations {
26845 values[1] = SqliteValue::Integer(200 + i64::try_from(i % 100).unwrap_or(0));
26846 let plan = build_compiled_record_write_plan(values.as_slice(), &builder);
26847 fused_dst.resize(plan.exact_size(), 0);
26848 plan.write_into_slice(fused_dst.as_mut_slice())
26849 .expect("precomputed plan should write");
26850 std::hint::black_box(&fused_dst);
26851 }
26852 let fused_elapsed = fused_start.elapsed();
26853
26854 assert_eq!(fused_dst, generic_dst);
26855 eprintln!(
26856 "bd-q7qj6 record assembly microbench: iterations={iterations}, generic_plan_ns={}, fused_precomputed_ns={}, record_bytes={}",
26857 generic_elapsed.as_nanos(),
26858 fused_elapsed.as_nanos(),
26859 fused_dst.len()
26860 );
26861 }
26862
26863 #[test]
26864 fn test_make_record_sideband_is_invalidated_by_register_overwrite() {
26865 let rows = run_program(|b| {
26866 let end = b.emit_label();
26867 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
26868 let r_value = b.alloc_reg();
26869 let r_record = b.alloc_reg();
26870 b.emit_op(Opcode::Integer, 1, r_value, 0, P4::None, 0);
26871 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
26872 b.emit_op(Opcode::Integer, 42, r_record, 0, P4::None, 0);
26873 b.emit_op(Opcode::ResultRow, r_record, 1, 0, P4::None, 0);
26874 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
26875 b.resolve_label(end);
26876 });
26877
26878 assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
26879 }
26880
26881 #[test]
26882 fn test_write_text_to_reg_reuses_unique_heap_buffer() {
26883 let mut engine = VdbeEngine::new(2);
26884
26885 engine.write_text_to_reg(
26886 1,
26887 "this string is definitely longer than twenty three bytes",
26888 );
26889 let first_reg = engine.get_reg(1);
26890 assert!(
26891 matches!(first_reg, SqliteValue::Text(_)),
26892 "register should contain text"
26893 );
26894 let SqliteValue::Text(first) = first_reg else {
26895 return;
26896 };
26897 let original_ptr = first.as_str().as_ptr();
26898
26899 engine.write_text_to_reg(1, "another long string that still fits the same allocation");
26900
26901 let updated_reg = engine.get_reg(1);
26902 assert!(
26903 matches!(updated_reg, SqliteValue::Text(_)),
26904 "register should still contain text"
26905 );
26906 let SqliteValue::Text(updated) = updated_reg else {
26907 return;
26908 };
26909 assert_eq!(updated.as_str().as_ptr(), original_ptr);
26910 assert_eq!(
26911 updated.as_str(),
26912 "another long string that still fits the same allocation"
26913 );
26914 }
26915
26916 #[test]
26917 fn test_write_blob_to_reg_reuses_unique_buffer() {
26918 let mut engine = VdbeEngine::new(2);
26919
26920 engine.write_blob_to_reg(1, &[1_u8, 2, 3, 4]);
26921 let first_reg = engine.get_reg(1);
26922 assert!(
26923 matches!(first_reg, SqliteValue::Blob(_)),
26924 "register should contain blob"
26925 );
26926 let SqliteValue::Blob(first) = first_reg else {
26927 return;
26928 };
26929 let original_ptr = Arc::as_ptr(first);
26930
26931 engine.write_blob_to_reg(1, &[9_u8, 8, 7, 6]);
26932
26933 let updated_reg = engine.get_reg(1);
26934 assert!(
26935 matches!(updated_reg, SqliteValue::Blob(_)),
26936 "register should still contain blob"
26937 );
26938 let SqliteValue::Blob(updated) = updated_reg else {
26939 return;
26940 };
26941 assert_eq!(Arc::as_ptr(updated), original_ptr);
26942 assert_eq!(updated.as_ref(), &[9_u8, 8, 7, 6]);
26943 }
26944
26945 #[test]
26946 fn test_set_reg_returns_reusable_values_to_pool() {
26947 use fsqlite_types::value::{pool_clear, pool_len};
26948
26949 pool_clear();
26950 let mut engine = VdbeEngine::new(2);
26951
26952 engine.set_reg(
26953 1,
26954 SqliteValue::Text("this string is definitely longer than twenty three bytes".into()),
26955 );
26956 assert_eq!(pool_len(), 0);
26957
26958 engine.set_reg(1, SqliteValue::Integer(7));
26959 assert_eq!(pool_len(), 1);
26960
26961 pool_clear();
26962 }
26963
26964 #[test]
26965 fn test_set_reg_skips_shared_values_without_reusable_backing_storage() {
26966 use fsqlite_types::value::{pool_clear, pool_len};
26967
26968 pool_clear();
26969 let mut engine = VdbeEngine::new(2);
26970
26971 let shared_text =
26972 Arc::<str>::from("this string is definitely longer than twenty three bytes");
26973 engine.set_reg(
26974 1,
26975 SqliteValue::Text(SmallText::from_arc(Arc::clone(&shared_text))),
26976 );
26977 engine.set_reg(1, SqliteValue::Integer(7));
26978 assert_eq!(
26979 pool_len(),
26980 0,
26981 "shared text values should not fill the reusable slab on register overwrite",
26982 );
26983
26984 let shared_blob = Arc::<[u8]>::from([0xCA_u8, 0xFE, 0xBA, 0xBE].as_slice());
26985 engine.set_reg(1, SqliteValue::Blob(Arc::clone(&shared_blob)));
26986 engine.set_reg(1, SqliteValue::Integer(8));
26987 assert_eq!(
26988 pool_len(),
26989 0,
26990 "shared blob values should not fill the reusable slab on register overwrite",
26991 );
26992
26993 pool_clear();
26994 }
26995
26996 #[test]
26997 fn test_make_record_retains_statement_scratch_capacity() {
26998 let mut builder = ProgramBuilder::new();
26999 let end = builder.emit_label();
27000 builder.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27001 let r_value = builder.alloc_reg();
27002 let r_record = builder.alloc_reg();
27003 builder.emit_op(Opcode::String8, 0, r_value, 0, P4::Str("x".repeat(256)), 0);
27004 builder.emit_op(
27005 Opcode::MakeRecord,
27006 r_value,
27007 1,
27008 r_record,
27009 P4::Affinity("B".to_owned()),
27010 0,
27011 );
27012 builder.emit_op(
27013 Opcode::String8,
27014 0,
27015 r_value,
27016 0,
27017 P4::Str("tiny".to_owned()),
27018 0,
27019 );
27020 builder.emit_op(
27021 Opcode::MakeRecord,
27022 r_value,
27023 1,
27024 r_record,
27025 P4::Affinity("B".to_owned()),
27026 0,
27027 );
27028 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27029 builder.resolve_label(end);
27030
27031 let program = builder.finish().expect("program should build");
27032 let expected_capacity = estimate_make_record_buffer_capacity(&program, PageSize::DEFAULT);
27033 let mut engine = VdbeEngine::new(program.register_count());
27034
27035 let outcome = run_async(engine.execute(&program)).expect("program should execute");
27036 assert!(matches!(outcome, ExecOutcome::Done));
27037 assert!(
27038 engine.make_record_lookaside.capacity() >= expected_capacity,
27039 "MakeRecord scratch should retain its pre-sized capacity across the statement"
27040 );
27041 assert!(
27042 !engine.make_record_lookaside.is_empty(),
27043 "final MakeRecord should keep record bytes in the sideband until consumed"
27044 );
27045 let materialized = engine.clone_reg_materialized(r_record);
27046 assert!(
27047 engine.make_record_lookaside.is_empty(),
27048 "statement scratch should be length-reset after sideband materialization"
27049 );
27050 let decoded = decode_record(&materialized).expect("record should decode");
27051 assert_eq!(decoded, vec![SqliteValue::Text("tiny".into())]);
27052 }
27053
27054 #[test]
27055 fn test_function_negative_argument_register_stays_null() {
27056 let rows = run_program_with_functions(|b| {
27057 let end = b.emit_label();
27058 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27059
27060 let r_out = b.alloc_reg();
27061 b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27062 b.emit_op(
27063 Opcode::Function,
27064 0,
27065 -1,
27066 r_out,
27067 P4::FuncName("typeof".to_owned()),
27068 1,
27069 );
27070 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27071 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27072 b.resolve_label(end);
27073 });
27074
27075 assert_eq!(rows, vec![vec![SqliteValue::Text("null".into())]]);
27076 }
27077
27078 #[test]
27079 fn test_result_row_negative_start_register_stays_null() {
27080 let rows = run_program(|b| {
27081 let end = b.emit_label();
27082 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27083
27084 b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27085 b.emit_op(Opcode::ResultRow, -1, 1, 0, P4::None, 0);
27086 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27087 b.resolve_label(end);
27088 });
27089
27090 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
27091 }
27092
27093 #[test]
27094 fn test_move_negative_source_register_stays_null() {
27095 let rows = run_program(|b| {
27096 let end = b.emit_label();
27097 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27098
27099 let r_dest = b.alloc_reg();
27100 b.emit_op(Opcode::Integer, 777, 0, 0, P4::None, 0);
27101 b.emit_op(Opcode::Move, -1, r_dest, 1, P4::None, 0);
27102 b.emit_op(Opcode::ResultRow, r_dest, 1, 0, P4::None, 0);
27103 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27104 b.resolve_label(end);
27105 });
27106
27107 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
27108 }
27109
27110 #[test]
27111 fn test_complex_expression_chain() {
27112 let rows = run_program(|b| {
27114 let end = b.emit_label();
27115 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27116 let r10 = b.alloc_reg();
27117 let r20 = b.alloc_reg();
27118 let r3 = b.alloc_reg();
27119 let r5 = b.alloc_reg();
27120 let r2 = b.alloc_reg();
27121 let t1 = b.alloc_reg();
27122 let t2 = b.alloc_reg();
27123 let t3 = b.alloc_reg();
27124 b.emit_op(Opcode::Integer, 10, r10, 0, P4::None, 0);
27125 b.emit_op(Opcode::Integer, 20, r20, 0, P4::None, 0);
27126 b.emit_op(Opcode::Integer, 3, r3, 0, P4::None, 0);
27127 b.emit_op(Opcode::Integer, 5, r5, 0, P4::None, 0);
27128 b.emit_op(Opcode::Integer, 2, r2, 0, P4::None, 0);
27129 b.emit_op(Opcode::Add, r10, r20, t1, P4::None, 0); b.emit_op(Opcode::Multiply, r3, t1, t2, P4::None, 0); b.emit_op(Opcode::Subtract, r5, t2, t2, P4::None, 0); b.emit_op(Opcode::Divide, r2, t2, t3, P4::None, 0); b.emit_op(Opcode::ResultRow, t3, 1, 0, P4::None, 0);
27134 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27135 b.resolve_label(end);
27136 });
27137 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
27138 }
27139
27140 #[test]
27141 fn test_string_comparison() {
27142 let rows = run_program(|b| {
27144 let end = b.emit_label();
27145 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27146 let r1 = b.alloc_reg();
27147 let r2 = b.alloc_reg();
27148 let r_out = b.alloc_reg();
27149 b.emit_op(Opcode::String8, 0, r1, 0, P4::Str("abd".to_owned()), 0);
27150 b.emit_op(Opcode::String8, 0, r2, 0, P4::Str("abc".to_owned()), 0);
27151 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
27152 let taken = b.emit_label();
27153 b.emit_jump_to_label(Opcode::Lt, r1, r2, taken, P4::None, 0);
27155 let done = b.emit_label();
27156 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27157 b.resolve_label(taken);
27158 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
27159 b.resolve_label(done);
27160 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27161 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27162 b.resolve_label(end);
27163 });
27164 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
27165 }
27166
27167 #[test]
27168 fn test_mixed_type_comparison() {
27169 let rows = run_program(|b| {
27171 let end = b.emit_label();
27172 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27173 let r_int = b.alloc_reg();
27174 let r_float = b.alloc_reg();
27175 let r_out = b.alloc_reg();
27176 b.emit_op(Opcode::Integer, 5, r_int, 0, P4::None, 0);
27177 b.emit_op(Opcode::Real, 0, r_float, 0, P4::Real(5.0), 0);
27178 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
27179 let taken = b.emit_label();
27180 b.emit_jump_to_label(Opcode::Eq, r_int, r_float, taken, P4::None, 0);
27181 let done = b.emit_label();
27182 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27183 b.resolve_label(taken);
27184 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
27185 b.resolve_label(done);
27186 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
27187 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27188 b.resolve_label(end);
27189 });
27190 assert_eq!(rows[0], vec![SqliteValue::Integer(1)]);
27191 }
27192
27193 fn run_with_storage_cursors(
27197 db: MemDatabase,
27198 build: impl FnOnce(&mut ProgramBuilder),
27199 ) -> Vec<Vec<SqliteValue>> {
27200 let mut b = ProgramBuilder::new();
27201 build(&mut b);
27202 let prog = b.finish().expect("program should build");
27203 let mut engine = VdbeEngine::new(prog.register_count());
27204 engine.enable_storage_read_cursors(true);
27205 engine.set_database(db);
27206 engine.set_reject_mem_fallback(false);
27208 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27209 assert_eq!(outcome, ExecOutcome::Done);
27210 engine
27211 .take_results()
27212 .into_iter()
27213 .map(|v| v.into_vec())
27214 .collect()
27215 }
27216
27217 #[test]
27218 fn test_vdbe_openread_uses_btree_cursor_backend() {
27219 let mut db = MemDatabase::new();
27222 let root = db.create_table(2);
27223 let table = db.get_table_mut(root).unwrap();
27224 table.insert(
27225 1,
27226 vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
27227 );
27228 table.insert(
27229 2,
27230 vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
27231 );
27232
27233 let rows = run_with_storage_cursors(db, |b| {
27234 let end = b.emit_label();
27235 let done = b.emit_label();
27236 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27237 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(2), 0);
27238 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27239
27240 let body = b.current_addr();
27241 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27242 b.emit_op(Opcode::Column, 0, 1, 2, P4::None, 0);
27243 b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
27244
27245 let next_target =
27246 i32::try_from(body).expect("program counter should fit into i32 for tests");
27247 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27248
27249 b.resolve_label(done);
27250 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27251 b.resolve_label(end);
27252 });
27253
27254 assert_eq!(rows.len(), 2, "should return 2 rows via storage cursor");
27255 assert_eq!(rows[0][0], SqliteValue::Integer(10));
27256 assert_eq!(rows[0][1], SqliteValue::Text("a".into()));
27257 assert_eq!(rows[1][0], SqliteValue::Integer(20));
27258 assert_eq!(rows[1][1], SqliteValue::Text("b".into()));
27259 }
27260
27261 #[test]
27262 fn test_select_uses_storage_cursor_not_memdb_for_persisted_table() {
27263 let mut db = MemDatabase::new();
27266 let root = db.create_table(1);
27267 let table = db.get_table_mut(root).unwrap();
27268 table.insert(1, vec![SqliteValue::Integer(42)]);
27269
27270 let rows = run_with_storage_cursors(db, |b| {
27271 let end = b.emit_label();
27272 let done = b.emit_label();
27273 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27274 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
27276 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27277
27278 let body = b.current_addr();
27279 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27280 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27281 let next_target =
27282 i32::try_from(body).expect("program counter should fit into i32 for tests");
27283 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27284
27285 b.resolve_label(done);
27286 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27287 b.resolve_label(end);
27288 });
27289
27290 assert_eq!(rows.len(), 1);
27291 assert_eq!(rows[0], vec![SqliteValue::Integer(42)]);
27292 }
27293
27294 #[test]
27295 fn test_count_uses_exact_cardinality_for_storage_cursor() {
27296 let mut db = MemDatabase::new();
27297 let root = db.create_table(1);
27298 let table = db.get_table_mut(root).unwrap();
27299 table.insert(1, vec![SqliteValue::Integer(10)]);
27300 table.insert(2, vec![SqliteValue::Integer(20)]);
27301 table.insert(3, vec![SqliteValue::Integer(30)]);
27302
27303 let rows = run_with_storage_cursors(db, |b| {
27304 let end = b.emit_label();
27305 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27306 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
27307 b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27308 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27309 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27310 b.resolve_label(end);
27311 });
27312
27313 assert_eq!(rows, vec![vec![SqliteValue::Integer(3)]]);
27314 }
27315
27316 #[test]
27317 fn test_count_on_writable_storage_cursor_ignores_hydrated_memdb_fast_path() {
27318 let mut db = MemDatabase::new();
27319 let root = db.create_table(1);
27320 let table = db.get_table_mut(root).unwrap();
27321 table.insert(1, vec![SqliteValue::Integer(10)]);
27322 table.insert(2, vec![SqliteValue::Integer(20)]);
27323 table.insert(3, vec![SqliteValue::Integer(30)]);
27324
27325 let mut b = ProgramBuilder::new();
27326 let end = b.emit_label();
27327 let done = b.emit_label();
27328 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27329 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27330 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27331 b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27332 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27333 b.emit_op(Opcode::Count, 0, 2, 0, P4::None, 0);
27334 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27335 b.resolve_label(done);
27336 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27337 b.resolve_label(end);
27338
27339 let prog = b.finish().expect("program should build");
27340 let mut engine = VdbeEngine::new(prog.register_count());
27341 engine.enable_storage_cursors(true);
27342 engine.set_database(db);
27343 engine.set_memdb_rows_loaded(true);
27344 engine.set_reject_mem_fallback(false);
27345
27346 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27347 assert_eq!(outcome, ExecOutcome::Done);
27348 let rows: Vec<_> = engine
27349 .take_results()
27350 .into_iter()
27351 .map(|row| row.into_vec())
27352 .collect();
27353 assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27354 }
27355
27356 #[test]
27357 fn test_count_on_mem_cursor_uses_memdb_rows() {
27358 let mut db = MemDatabase::new();
27359 let root = db.create_table(1);
27360 let table = db.get_table_mut(root).unwrap();
27361 table.insert(1, vec![SqliteValue::Integer(10)]);
27362 table.insert(2, vec![SqliteValue::Integer(20)]);
27363
27364 let mut b = ProgramBuilder::new();
27365 let end = b.emit_label();
27366 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27367 b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27368 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27369 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27370 b.resolve_label(end);
27371
27372 let prog = b.finish().expect("program should build");
27373 let mut engine = VdbeEngine::new(prog.register_count());
27374 engine.set_database(db);
27375 engine.set_memdb_rows_loaded(true);
27376 engine.cursors.insert(0, MemCursor::new(root, false));
27377
27378 let rows = execute_program_with_engine(&mut engine, &prog);
27379
27380 assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27381 }
27382
27383 #[test]
27384 fn test_count_on_readonly_storage_cursor_ignores_stale_memdb_shortcut() {
27385 let mut db = MemDatabase::new();
27386 let root = db.create_table(1);
27387
27388 let mut b = ProgramBuilder::new();
27389 let end = b.emit_label();
27390 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27391 b.emit_op(Opcode::Count, 0, 1, 0, P4::None, 0);
27392 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27393 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27394 b.resolve_label(end);
27395
27396 let prog = b.finish().expect("program should build");
27397 let mut engine = VdbeEngine::new(prog.register_count());
27398 engine.enable_storage_cursors(true);
27399 engine.set_database(db);
27400 engine.set_memdb_rows_loaded(true);
27401 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
27402 engine.set_reject_mem_fallback(false);
27403
27404 assert!(run_async(engine.open_storage_cursor(0, root, true)));
27405 engine.cursor_root_pages.insert(0, root);
27406 {
27407 let sc = engine
27408 .storage_cursors
27409 .get_mut(&0)
27410 .expect("storage cursor should exist");
27411 run_async(sc.cursor.table_insert(
27412 &sc.cx,
27413 1,
27414 &encode_record(&[SqliteValue::Integer(10)]),
27415 ))
27416 .unwrap();
27417 run_async(sc.cursor.table_insert(
27418 &sc.cx,
27419 2,
27420 &encode_record(&[SqliteValue::Integer(20)]),
27421 ))
27422 .unwrap();
27423 sc.writable = false;
27424 }
27425
27426 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27427 assert_eq!(outcome, ExecOutcome::Done);
27428 let rows: Vec<_> = engine
27429 .take_results()
27430 .into_iter()
27431 .map(|row| row.into_vec())
27432 .collect();
27433 assert_eq!(rows, vec![vec![SqliteValue::Integer(2)]]);
27434
27435 let db = engine.take_database().expect("database should exist");
27436 let table = db.get_table(root).expect("table should exist");
27437 assert_eq!(table.rows.len(), 0, "attached MemDatabase stays stale here");
27438 }
27439
27440 fn run_write_with_storage_cursors(
27445 db: MemDatabase,
27446 build: impl FnOnce(&mut ProgramBuilder),
27447 ) -> (Vec<Vec<SqliteValue>>, MemDatabase) {
27448 let mut b = ProgramBuilder::new();
27449 build(&mut b);
27450 let prog = b.finish().expect("program should build");
27451 let mut engine = VdbeEngine::new(prog.register_count());
27452 engine.enable_storage_cursors(true);
27453 engine.set_database(db);
27454 engine.set_reject_mem_fallback(false);
27456 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27457 assert_eq!(outcome, ExecOutcome::Done);
27458 let results: Vec<_> = engine
27459 .take_results()
27460 .into_iter()
27461 .map(|v| v.into_vec())
27462 .collect();
27463 let db = engine.take_database().expect("database should exist");
27464 (results, db)
27465 }
27466
27467 #[test]
27468 fn test_openwrite_uses_storage_cursor_backend() {
27469 let mut db = MemDatabase::new();
27471 let root = db.create_table(1);
27472 let table = db.get_table_mut(root).unwrap();
27473 table.insert(1, vec![SqliteValue::Integer(100)]);
27474
27475 let mut b = ProgramBuilder::new();
27476 let end = b.emit_label();
27477 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27478 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27479 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27480 b.resolve_label(end);
27481
27482 let prog = b.finish().expect("program should build");
27483 let mut engine = VdbeEngine::new(prog.register_count());
27484 engine.enable_storage_cursors(true);
27485 engine.set_database(db);
27486 engine.set_reject_mem_fallback(false);
27487 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
27488 assert_eq!(outcome, ExecOutcome::Done);
27489 assert!(
27491 engine.storage_cursors.contains_key(&0),
27492 "OpenWrite should route through StorageCursor"
27493 );
27494 assert!(!engine.cursors.contains_key(&0));
27495 assert!(
27497 engine
27498 .storage_cursors
27499 .get(&0)
27500 .is_some_and(|cursor| cursor.writable)
27501 );
27502 }
27503
27504 #[test]
27505 fn test_insert_via_storage_cursor_write_path() {
27506 let mut db = MemDatabase::new();
27511 let root = db.create_table(2);
27512
27513 let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
27514 let end = b.emit_label();
27515 let done = b.emit_label();
27516 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27517
27518 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27520
27521 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
27523
27524 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
27526 b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("hello".to_owned()), 0);
27527 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
27528
27529 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
27531
27532 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27534
27535 let body = b.current_addr();
27536 b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27537 b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27538 b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27539 let next_target =
27540 i32::try_from(body).expect("program counter should fit into i32 for tests");
27541 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27542
27543 b.resolve_label(done);
27544 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27545 b.resolve_label(end);
27546 });
27547
27548 let table = final_db.get_table(root).expect("table should exist");
27550 assert_eq!(
27551 table.rows.len(),
27552 0,
27553 "MemDatabase must not be synced in write-through mode"
27554 );
27555
27556 assert_eq!(
27558 rows.len(),
27559 1,
27560 "should read back exactly one row from B-tree"
27561 );
27562 assert_eq!(rows[0][0], SqliteValue::Integer(42));
27563 assert_eq!(rows[0][1], SqliteValue::Text("hello".into()));
27564 }
27565
27566 #[test]
27567 fn test_fused_append_insert_honors_make_record_p4_metadata() {
27568 let mut db = MemDatabase::new();
27569 let root = db.create_table(2);
27570
27571 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27572 let end = b.emit_label();
27573 let done = b.emit_label();
27574 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27575
27576 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27577 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
27578 b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("fused".to_owned()), 0);
27579 b.emit_op(
27580 Opcode::FusedAppendInsert,
27581 0,
27582 2,
27583 2,
27584 P4::Affinity("XD".to_owned()),
27585 2,
27586 );
27587
27588 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27589 let body = b.current_addr();
27590 b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27591 b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27592 b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27593 let next_target =
27594 i32::try_from(body).expect("program counter should fit into i32 for tests");
27595 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27596
27597 b.resolve_label(done);
27598 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27599 b.resolve_label(end);
27600 });
27601
27602 assert_eq!(
27603 rows,
27604 vec![vec![SqliteValue::Null, SqliteValue::Text("fused".into())]],
27605 "FusedAppendInsert must preserve MakeRecord X-affinity placeholder semantics",
27606 );
27607 }
27608
27609 #[test]
27610 fn test_fused_append_insert_honors_precomputed_record_header() {
27611 let precomputed = fsqlite_types::record::PrecomputedRecordHeader::new(&[
27612 fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
27613 fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
27614 ]);
27615 let mut db = MemDatabase::new();
27616 let root = db.create_table(2);
27617
27618 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27619 let end = b.emit_label();
27620 let done = b.emit_label();
27621 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27622
27623 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(2), 0);
27624 b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
27625 b.emit_op(Opcode::Integer, 7, 3, 0, P4::None, 0);
27626 b.emit_op(
27627 Opcode::FusedAppendInsert,
27628 0,
27629 2,
27630 2,
27631 P4::PrecomputedHeader(precomputed),
27632 2,
27633 );
27634
27635 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27636 let body = b.current_addr();
27637 b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
27638 b.emit_op(Opcode::Column, 0, 1, 6, P4::None, 0);
27639 b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
27640 let next_target =
27641 i32::try_from(body).expect("program counter should fit into i32 for tests");
27642 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27643
27644 b.resolve_label(done);
27645 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27646 b.resolve_label(end);
27647 });
27648
27649 assert_eq!(
27650 rows,
27651 vec![vec![SqliteValue::Null, SqliteValue::Integer(7)]],
27652 "FusedAppendInsert must preserve MakeRecord precomputed-header placeholders",
27653 );
27654 }
27655
27656 #[test]
27657 fn test_delete_via_storage_cursor_write_path() {
27658 let mut db = MemDatabase::new();
27662 let root = db.create_table(1);
27663 let table = db.get_table_mut(root).unwrap();
27664 table.insert(1, vec![SqliteValue::Integer(10)]);
27665 table.insert(2, vec![SqliteValue::Integer(20)]);
27666 table.insert(3, vec![SqliteValue::Integer(30)]);
27667
27668 let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
27669 let end = b.emit_label();
27670 let done = b.emit_label();
27671 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27672
27673 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27675
27676 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27678 b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27679
27680 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27682
27683 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27685 let body = b.current_addr();
27686 b.emit_op(Opcode::Rowid, 0, 2, 0, P4::None, 0);
27687 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27688 let next_target =
27689 i32::try_from(body).expect("program counter should fit into i32 for tests");
27690 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
27691
27692 b.resolve_label(done);
27693 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27694 b.resolve_label(end);
27695 });
27696
27697 assert_eq!(
27698 rows,
27699 vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)],],
27700 "B-tree cursor should observe rowid=2 deleted"
27701 );
27702
27703 let table = final_db.get_table(root).expect("table should exist");
27705 assert_eq!(table.rows.len(), 3);
27706 let rowids: Vec<i64> = table.rows.iter().map(|r| r.rowid).collect();
27707 assert!(rowids.contains(&1));
27708 assert!(rowids.contains(&2));
27709 assert!(rowids.contains(&3));
27710 }
27711
27712 #[test]
27713 fn test_delete_invalidates_cached_storage_row_before_successor_column() {
27714 let mut db = MemDatabase::new();
27715 let root = db.create_table(1);
27716 let table = db.get_table_mut(root).unwrap();
27717 table.insert(1, vec![SqliteValue::Integer(10)]);
27718 table.insert(2, vec![SqliteValue::Integer(20)]);
27719 table.insert(3, vec![SqliteValue::Integer(30)]);
27720
27721 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27722 let end = b.emit_label();
27723 let done = b.emit_label();
27724 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27725 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27726 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27727
27728 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27730
27731 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27733
27734 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
27736 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27737 b.resolve_label(done);
27738 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27739 b.resolve_label(end);
27740 });
27741
27742 assert_eq!(rows, vec![vec![SqliteValue::Integer(20)]]);
27743 }
27744
27745 #[test]
27746 fn test_storage_cursor_delete_write_boundary_invalidates_decode_cache_once() {
27747 let _guard = VDBE_OBSERVABILITY_LOCK
27748 .lock()
27749 .unwrap_or_else(|e| e.into_inner());
27750 let prev_metrics_enabled = vdbe_metrics_enabled();
27751 reset_vdbe_metrics();
27752 set_vdbe_metrics_enabled(true);
27753
27754 let mut db = MemDatabase::new();
27755 let root = db.create_table(1);
27756 let table = db.get_table_mut(root).unwrap();
27757 table.insert(1, vec![SqliteValue::Integer(10)]);
27758 table.insert(2, vec![SqliteValue::Integer(20)]);
27759
27760 let before = vdbe_metrics_snapshot();
27761 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27762 let end = b.emit_label();
27763 let done = b.emit_label();
27764 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27765 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27766 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
27767
27768 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
27772 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27773 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
27774 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27775 b.resolve_label(done);
27776 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27777 b.resolve_label(end);
27778 });
27779 let after = vdbe_metrics_snapshot();
27780
27781 assert_eq!(rows, vec![vec![SqliteValue::Integer(20)]]);
27782 assert_eq!(
27783 after.decode_cache_invalidations_write_total
27784 - before.decode_cache_invalidations_write_total,
27785 1
27786 );
27787 assert_eq!(
27788 after.decode_cache_invalidations_position_total
27789 - before.decode_cache_invalidations_position_total,
27790 0
27791 );
27792 assert_eq!(
27793 after.decode_cache_hits_total - before.decode_cache_hits_total,
27794 0
27795 );
27796 assert_eq!(
27797 after.decode_cache_misses_total - before.decode_cache_misses_total,
27798 2
27799 );
27800
27801 set_vdbe_metrics_enabled(prev_metrics_enabled);
27802 }
27803
27804 #[test]
27805 fn test_delete_then_prev_then_next_advances_correctly() {
27806 let mut db = MemDatabase::new();
27810 let root = db.create_table(1);
27811 let table = db.get_table_mut(root).unwrap();
27812 table.insert(1, vec![SqliteValue::Integer(10)]);
27813 table.insert(2, vec![SqliteValue::Integer(20)]);
27814 table.insert(3, vec![SqliteValue::Integer(30)]);
27815
27816 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27817 let end = b.emit_label();
27818 let done = b.emit_label();
27819 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27820 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27821
27822 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27824 b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27825 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27826
27827 let prev_ok = b.emit_label();
27829 b.emit_jump_to_label(Opcode::Prev, 0, 0, prev_ok, P4::None, 0);
27830 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27831 b.resolve_label(prev_ok);
27832 b.emit_op(Opcode::Rowid, 0, 2, 0, P4::None, 0);
27833 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27834
27835 let next_ok = b.emit_label();
27837 b.emit_jump_to_label(Opcode::Next, 0, 0, next_ok, P4::None, 0);
27838 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27839 b.resolve_label(next_ok);
27840 b.emit_op(Opcode::Rowid, 0, 3, 0, P4::None, 0);
27841 b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27842
27843 b.resolve_label(done);
27844 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27845 b.resolve_label(end);
27846 });
27847
27848 assert_eq!(
27849 rows,
27850 vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)]],
27851 "Delete->Prev->Next should land on rowids 1 then 3 without repeating row 1"
27852 );
27853 }
27854
27855 #[test]
27856 fn test_delete_then_notexists_then_next_advances_from_probe_position() {
27857 let mut db = MemDatabase::new();
27861 let root = db.create_table(1);
27862 let table = db.get_table_mut(root).unwrap();
27863 table.insert(1, vec![SqliteValue::Integer(10)]);
27864 table.insert(2, vec![SqliteValue::Integer(20)]);
27865 table.insert(3, vec![SqliteValue::Integer(30)]);
27866
27867 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27868 let end = b.emit_label();
27869 let done = b.emit_label();
27870 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27871 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27872
27873 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27875 b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27876 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27877
27878 let probe_missing = b.emit_label();
27880 b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
27881 b.emit_jump_to_label(Opcode::NotExists, 0, 2, probe_missing, P4::None, 0);
27882
27883 b.emit_op(Opcode::Rowid, 0, 3, 0, P4::None, 0);
27885 b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27886
27887 let next_ok = b.emit_label();
27889 b.emit_jump_to_label(Opcode::Next, 0, 0, next_ok, P4::None, 0);
27890 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27891 b.resolve_label(next_ok);
27892 b.emit_op(Opcode::Rowid, 0, 4, 0, P4::None, 0);
27893 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
27894 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27895
27896 b.resolve_label(probe_missing);
27898 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27899
27900 b.resolve_label(done);
27901 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27902 b.resolve_label(end);
27903 });
27904
27905 assert_eq!(
27906 rows,
27907 vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(3)]],
27908 "Delete->NotExists->Next should advance from probe row 1 to row 3"
27909 );
27910 }
27911
27912 #[test]
27913 fn test_delete_then_newrowid_then_next_reports_no_successor() {
27914 let mut db = MemDatabase::new();
27918 let root = db.create_table(1);
27919 let table = db.get_table_mut(root).unwrap();
27920 table.insert(1, vec![SqliteValue::Integer(10)]);
27921 table.insert(2, vec![SqliteValue::Integer(20)]);
27922 table.insert(3, vec![SqliteValue::Integer(30)]);
27923
27924 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27925 let end = b.emit_label();
27926 let done = b.emit_label();
27927 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27928 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27929
27930 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
27932 b.emit_jump_to_label(Opcode::SeekRowid, 0, 1, done, P4::None, 0);
27933 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
27934
27935 b.emit_op(Opcode::NewRowid, 0, 2, 0, P4::None, 0);
27938
27939 let has_next = b.emit_label();
27941 b.emit_jump_to_label(Opcode::Next, 0, 0, has_next, P4::None, 0);
27942 b.emit_op(Opcode::Integer, 0, 3, 0, P4::None, 0);
27943 b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27944 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27945
27946 b.resolve_label(has_next);
27948 b.emit_op(Opcode::Integer, 1, 3, 0, P4::None, 0);
27949 b.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
27950 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
27951
27952 b.resolve_label(done);
27953 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27954 b.resolve_label(end);
27955 });
27956
27957 assert_eq!(
27958 rows,
27959 vec![vec![SqliteValue::Integer(0)]],
27960 "Delete->NewRowid->Next should report no successor at end-of-table"
27961 );
27962 }
27963
27964 #[test]
27965 fn test_newrowid_with_storage_cursor_allocates_correctly() {
27966 let mut db = MemDatabase::new();
27968 let root = db.create_table(1);
27969 let table = db.get_table_mut(root).unwrap();
27970 table.insert(5, vec![SqliteValue::Integer(50)]);
27971
27972 let (rows, _) = run_write_with_storage_cursors(db, |b| {
27973 let end = b.emit_label();
27974 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
27975
27976 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
27977
27978 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
27980 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
27981 b.emit_op(Opcode::NewRowid, 0, 2, 0, P4::None, 0);
27982 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
27983
27984 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
27985 b.resolve_label(end);
27986 });
27987
27988 assert_eq!(rows.len(), 2);
27990 assert_eq!(rows[0][0], SqliteValue::Integer(6));
27991 assert_eq!(rows[1][0], SqliteValue::Integer(7));
27992 }
27993
27994 #[test]
27995 fn test_newrowid_concurrent_flag_uses_snapshot_independent_path() {
27996 fn setup_db_with_stale_counter() -> (MemDatabase, i32) {
28000 let mut db = MemDatabase::new();
28001 let root = db.create_table(1);
28002 let table = db.get_table_mut(root).expect("table should exist");
28003 table.insert(10, vec![SqliteValue::Integer(10)]);
28004 table.insert(11, vec![SqliteValue::Integer(11)]);
28005 table.next_rowid = 1;
28007 (db, root)
28008 }
28009
28010 let (db_serialized, root) = setup_db_with_stale_counter();
28011 let (rows_serialized, _) = run_write_with_storage_cursors(db_serialized, |b| {
28012 let end = b.emit_label();
28013 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28014 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28015 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28018 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28019 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28020 b.resolve_label(end);
28021 });
28022
28023 let (db_concurrent, root) = setup_db_with_stale_counter();
28024 let (rows_concurrent, _) = run_write_with_storage_cursors(db_concurrent, |b| {
28025 let end = b.emit_label();
28026 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28027 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28028 b.emit_op(Opcode::NewRowid, 0, 1, 1, P4::None, 0);
28030 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28031 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28032 b.resolve_label(end);
28033 });
28034
28035 assert_eq!(rows_serialized, vec![vec![SqliteValue::Integer(12)]]);
28037 assert_eq!(rows_concurrent, vec![vec![SqliteValue::Integer(12)]]);
28038 }
28039
28040 #[test]
28041 fn test_memdb_concurrent_rowid_uses_visible_max_not_stale_counter() {
28042 let mut db = MemDatabase::new();
28043 let root = db.create_table(1);
28044 let table = db.get_table_mut(root).expect("table should exist");
28045 table.insert(10, vec![SqliteValue::Integer(10)]);
28046 table.insert(11, vec![SqliteValue::Integer(11)]);
28047 assert!(table.delete_by_rowid(11), "max rowid should be removed");
28048 table.next_rowid = 1;
28049
28050 let rowid = db.alloc_rowid_concurrent(root);
28051 assert_eq!(rowid, 11, "concurrent rowid should reuse max_visible+1");
28052 assert_eq!(
28053 db.get_table(root)
28054 .expect("table should exist")
28055 .next_rowid_hint(),
28056 12,
28057 "next_rowid should advance from the visible max"
28058 );
28059 }
28060
28061 #[test]
28062 fn test_memdb_delete_rowid_records_undo() {
28063 let mut db = MemDatabase::new();
28064 let root = db.create_table(2);
28065 db.get_table_mut(root)
28066 .expect("table should exist")
28067 .add_unique_column_group(vec![0]);
28068 db.upsert_row(
28069 root,
28070 1,
28071 vec![SqliteValue::Integer(10), SqliteValue::Integer(100)],
28072 );
28073
28074 db.begin_undo();
28075 let token = db.undo_version();
28076 assert!(db.delete_rowid(root, 1), "delete should find the row");
28077 let table = db.get_table(root).expect("table should exist");
28078 assert_eq!(table.row_values_by_rowid(1), None);
28079 assert!(
28080 table
28081 .find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)])
28082 .is_empty(),
28083 "delete should remove the unique-index entry"
28084 );
28085
28086 db.rollback_to(token);
28087 let table = db.get_table(root).expect("table should exist");
28088 let restored = vec![SqliteValue::Integer(10), SqliteValue::Integer(100)];
28089 assert_eq!(table.row_values_by_rowid(1), Some(restored.as_slice()));
28090 assert_eq!(
28091 table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28092 vec![1],
28093 "rollback should restore the unique-index entry"
28094 );
28095 }
28096
28097 #[test]
28098 fn test_memdb_insert_auto_row_records_undo_and_patches_rowid_alias() {
28099 let mut db = MemDatabase::new();
28100 let root = db.create_table(2);
28101 db.upsert_row(
28102 root,
28103 5,
28104 vec![SqliteValue::Integer(5), SqliteValue::Text("old".into())],
28105 );
28106
28107 db.begin_undo();
28108 let token = db.undo_version();
28109 let rowid = db
28110 .insert_auto_row(
28111 root,
28112 vec![SqliteValue::Null, SqliteValue::Text("new".into())],
28113 Some(0),
28114 )
28115 .expect("table should exist");
28116 assert_eq!(rowid, 6);
28117 let table = db.get_table(root).expect("table should exist");
28118 assert_eq!(table.next_rowid_hint(), 7);
28119 assert_eq!(
28120 table.row_values_by_rowid(6),
28121 Some([SqliteValue::Integer(6), SqliteValue::Text("new".into())].as_slice())
28122 );
28123
28124 db.rollback_to(token);
28125 let table = db.get_table(root).expect("table should exist");
28126 assert_eq!(table.next_rowid_hint(), 6);
28127 assert_eq!(table.row_values_by_rowid(6), None);
28128 assert_eq!(
28129 table.row_values_by_rowid(5),
28130 Some([SqliteValue::Integer(5), SqliteValue::Text("old".into())].as_slice())
28131 );
28132 }
28133
28134 #[test]
28135 fn test_memdb_replace_secondary_unique_rollback_restores_deleted_and_replaced_rows() {
28136 let mut db = MemDatabase::new();
28137 let root = db.create_table(2);
28138 db.get_table_mut(root)
28139 .expect("table should exist")
28140 .add_unique_column_group(vec![1]);
28141 db.upsert_row(
28142 root,
28143 1,
28144 vec![SqliteValue::Integer(1), SqliteValue::Text("alpha".into())],
28145 );
28146 db.upsert_row(
28147 root,
28148 2,
28149 vec![SqliteValue::Integer(2), SqliteValue::Text("beta".into())],
28150 );
28151
28152 db.begin_undo();
28153 let token = db.undo_version();
28154 assert!(
28155 db.delete_rowid(root, 1),
28156 "REPLACE should delete the secondary-unique conflict"
28157 );
28158 db.upsert_row(
28159 root,
28160 2,
28161 vec![SqliteValue::Integer(2), SqliteValue::Text("alpha".into())],
28162 );
28163 let table = db.get_table(root).expect("table should exist");
28164 assert_eq!(table.row_values_by_rowid(1), None);
28165 assert_eq!(
28166 table.row_values_by_rowid(2),
28167 Some([SqliteValue::Integer(2), SqliteValue::Text("alpha".into())].as_slice())
28168 );
28169 assert_eq!(
28170 table.find_unique_conflicts(&[
28171 SqliteValue::Integer(99),
28172 SqliteValue::Text("alpha".into())
28173 ]),
28174 vec![2]
28175 );
28176
28177 db.rollback_to(token);
28178 let table = db.get_table(root).expect("table should exist");
28179 assert_eq!(table.next_rowid_hint(), 3);
28180 assert_eq!(
28181 table.row_values_by_rowid(1),
28182 Some([SqliteValue::Integer(1), SqliteValue::Text("alpha".into())].as_slice())
28183 );
28184 assert_eq!(
28185 table.row_values_by_rowid(2),
28186 Some([SqliteValue::Integer(2), SqliteValue::Text("beta".into())].as_slice())
28187 );
28188 assert_eq!(
28189 table.find_unique_conflicts(&[
28190 SqliteValue::Integer(99),
28191 SqliteValue::Text("alpha".into())
28192 ]),
28193 vec![1]
28194 );
28195 assert_eq!(
28196 table.find_unique_conflicts(&[
28197 SqliteValue::Integer(99),
28198 SqliteValue::Text("beta".into())
28199 ]),
28200 vec![2]
28201 );
28202 }
28203
28204 #[test]
28205 fn test_memtable_rowid_lookup_uses_dense_offset_without_breaking_sparse_rows() {
28206 let mut table = MemTable::new(1);
28207 table.insert(10, vec![SqliteValue::Integer(10)]);
28208 table.insert(11, vec![SqliteValue::Integer(11)]);
28209 table.insert(12, vec![SqliteValue::Integer(12)]);
28210
28211 assert_eq!(table.find_by_rowid(11), Some(1));
28212 assert_eq!(
28213 table.row_values_by_rowid(11),
28214 Some(&[SqliteValue::Integer(11)][..])
28215 );
28216
28217 assert!(table.delete_by_rowid(11), "middle row should be removed");
28218 assert_eq!(table.find_by_rowid(11), None);
28219 assert_eq!(
28220 table.row_values_by_rowid(12),
28221 Some(&[SqliteValue::Integer(12)][..]),
28222 "sparse tables must still fall back to binary search"
28223 );
28224 }
28225
28226 #[test]
28227 fn test_memtable_rowid_range_bounds_use_dense_offset_with_sparse_fallback() {
28228 let mut table = MemTable::new(1);
28229 for rowid in 10..15 {
28230 table.insert(rowid, vec![SqliteValue::Integer(rowid)]);
28231 }
28232
28233 assert_eq!(table.count_rowid_range(11, 14), 3);
28234 assert_eq!(table.count_rowid_range(i64::MIN, 12), 2);
28235 assert_eq!(table.count_rowid_range(13, i64::MAX), 2);
28236 let dense_values = table
28237 .iter_rows_in_rowid_range(11, 14)
28238 .map(|(rowid, values)| (rowid, values.first().cloned()))
28239 .collect::<Vec<_>>();
28240 assert_eq!(
28241 dense_values,
28242 vec![
28243 (11, Some(SqliteValue::Integer(11))),
28244 (12, Some(SqliteValue::Integer(12))),
28245 (13, Some(SqliteValue::Integer(13))),
28246 ]
28247 );
28248
28249 assert!(table.delete_by_rowid(12), "middle row should be removed");
28250 assert_eq!(table.count_rowid_range(11, 14), 2);
28251 assert_eq!(table.count_rowid_range(i64::MIN, 12), 2);
28252 assert_eq!(table.count_rowid_range(13, i64::MAX), 2);
28253 let sparse_values = table
28254 .iter_rows_in_rowid_range(11, 14)
28255 .map(|(rowid, values)| (rowid, values.first().cloned()))
28256 .collect::<Vec<_>>();
28257 assert_eq!(
28258 sparse_values,
28259 vec![
28260 (11, Some(SqliteValue::Integer(11))),
28261 (13, Some(SqliteValue::Integer(13))),
28262 ]
28263 );
28264 }
28265
28266 #[test]
28267 fn test_memtable_unique_conflict_index_tracks_insert_delete_and_update() {
28268 let mut table = MemTable::new(2);
28269 table.add_unique_column_group(vec![0]);
28270 table.insert(1, vec![SqliteValue::Integer(10), SqliteValue::Integer(100)]);
28271 table.insert(2, vec![SqliteValue::Integer(20), SqliteValue::Integer(200)]);
28272
28273 assert_eq!(
28274 table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28275 vec![1],
28276 "lookup should find the conflicting rowid via the unique index"
28277 );
28278
28279 assert!(table.delete_by_rowid(1), "delete should succeed");
28280 assert!(
28281 table
28282 .find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)])
28283 .is_empty(),
28284 "deleted rows must be removed from the unique index"
28285 );
28286
28287 table.insert(2, vec![SqliteValue::Integer(10), SqliteValue::Integer(200)]);
28288 assert_eq!(
28289 table.find_unique_conflicts(&[SqliteValue::Integer(10), SqliteValue::Integer(999)]),
28290 vec![2],
28291 "updates must refresh the unique index entry"
28292 );
28293
28294 assert!(
28295 table
28296 .find_unique_conflicts(&[SqliteValue::Null, SqliteValue::Integer(999)])
28297 .is_empty(),
28298 "NULL keys must remain non-conflicting under SQLite UNIQUE semantics"
28299 );
28300 }
28301
28302 #[test]
28303 fn test_memtable_unique_conflict_index_honors_nocase_collation() {
28304 let mut table = MemTable::new(1);
28305 table.add_unique_column_group_with_collations(vec![0], vec![Some("NOCASE".to_owned())]);
28306 table.insert(1, vec![SqliteValue::Text("Alice".into())]);
28307
28308 assert_eq!(
28309 table.find_unique_conflicts(&[SqliteValue::Text("alice".into())]),
28310 vec![1],
28311 "builtin NOCASE unique indexes must normalize case-equivalent text"
28312 );
28313 }
28314
28315 #[test]
28316 fn test_engine_database_attachment_propagates_custom_collation_registry_for_memtable_uniques() {
28317 struct DashlessNoCaseCollation;
28318
28319 impl fsqlite_func::collation::CollationFunction for DashlessNoCaseCollation {
28320 fn name(&self) -> &str {
28321 "DASHLESS_NOCASE"
28322 }
28323
28324 fn compare(&self, left: &[u8], right: &[u8]) -> Ordering {
28325 let normalize = |bytes: &[u8]| {
28326 bytes
28327 .iter()
28328 .filter(|&&byte| byte != b'-' && byte != b'_' && byte != b' ')
28329 .map(u8::to_ascii_lowercase)
28330 .collect::<Vec<_>>()
28331 };
28332 normalize(left).cmp(&normalize(right))
28333 }
28334 }
28335
28336 let registry = Arc::new(Mutex::new(CollationRegistry::new()));
28337 registry
28338 .lock()
28339 .unwrap_or_else(|err| err.into_inner())
28340 .register(DashlessNoCaseCollation);
28341
28342 let mut db = MemDatabase::new();
28343 let root = db.create_table(1);
28344 {
28345 let table = db.get_table_mut(root).expect("table should exist");
28346 table.add_unique_column_group_with_collations(
28347 vec![0],
28348 vec![Some("DASHLESS_NOCASE".to_owned())],
28349 );
28350 table.insert(1, vec![SqliteValue::Text("Alpha-Beta".into())]);
28351 }
28352
28353 let mut engine = VdbeEngine::new(1);
28354 engine.set_database(db);
28355 engine.set_collation_registry(Arc::clone(®istry));
28356
28357 let db = engine
28358 .take_database()
28359 .expect("database should still be attached");
28360 let table = db.get_table(root).expect("table should exist");
28361 assert_eq!(
28362 table.find_unique_conflicts(&[SqliteValue::Text("alpha beta".into())]),
28363 vec![1],
28364 "custom-collation UNIQUE fallback must use the engine-provided registry"
28365 );
28366 }
28367
28368 #[test]
28371 fn test_insert_write_through_no_memdb_sync() {
28372 let mut db = MemDatabase::new();
28374 let root = db.create_table(1);
28375
28376 let (_, final_db) = run_write_with_storage_cursors(db, |b| {
28377 let end = b.emit_label();
28378 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28379 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28380 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28381 b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
28382 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28383 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28384 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28385 b.resolve_label(end);
28386 });
28387
28388 let table = final_db.get_table(root).expect("table should exist");
28389 assert_eq!(table.rows.len(), 0, "write-through must skip MemDatabase");
28390 }
28391
28392 #[test]
28393 fn test_insert_write_through_marks_root_page_dirty_for_lazy_memdb_sync() {
28394 let mut db = MemDatabase::new();
28395 let root = db.create_table(1);
28396
28397 let mut b = ProgramBuilder::new();
28398 let end = b.emit_label();
28399 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28400 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28401 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28402 b.emit_op(Opcode::Integer, 99, 2, 0, P4::None, 0);
28403 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28404 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28405 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28406 b.resolve_label(end);
28407
28408 let prog = b.finish().expect("program should build");
28409 let mut engine = VdbeEngine::new(prog.register_count());
28410 engine.enable_storage_cursors(true);
28411 engine.set_database(db);
28412 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28413 engine.set_reject_mem_fallback(false);
28414
28415 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28416 assert_eq!(outcome, ExecOutcome::Done);
28417 assert!(engine.has_dirty_root_pages());
28418 assert!(engine.dirty_root_pages().contains(&root));
28419 assert_eq!(engine.dirty_root_pages().len(), 1);
28420 assert!(
28421 !engine.storage_cursor_memdb_count_shortcuts_safe(),
28422 "storage-backed INSERT should leave MemDB fast paths disabled until reload"
28423 );
28424
28425 let db = engine.take_database().expect("database should exist");
28426 let table = db.get_table(root).expect("table should exist");
28427 assert_eq!(
28428 table.rows.len(),
28429 0,
28430 "write-through must keep MemDatabase stale"
28431 );
28432 }
28433
28434 #[test]
28435 fn test_lazy_dirty_flag_set_on_insert() {
28436 let mut db = MemDatabase::new();
28437 let root = db.create_table(1);
28438
28439 let mut b = ProgramBuilder::new();
28440 let end = b.emit_label();
28441 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28442 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28443 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28444 b.emit_op(Opcode::Integer, 77, 2, 0, P4::None, 0);
28445 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28446 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28447 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28448 b.resolve_label(end);
28449
28450 let prog = b.finish().expect("program should build");
28451 let mut engine = VdbeEngine::new(prog.register_count());
28452 engine.enable_storage_cursors(true);
28453 engine.set_database(db);
28454 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28455 engine.set_reject_mem_fallback(false);
28456
28457 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28458 assert_eq!(outcome, ExecOutcome::Done);
28459 assert!(engine.has_dirty_root_pages());
28460 assert!(engine.dirty_root_pages().contains(&root));
28461 assert_eq!(engine.dirty_root_pages().len(), 1);
28462 assert!(
28463 !engine.storage_cursor_memdb_count_shortcuts_safe(),
28464 "storage-backed INSERT must mark the MemDatabase mirror stale"
28465 );
28466
28467 let db = engine.take_database().expect("database should exist");
28468 let table = db.get_table(root).expect("table should exist");
28469 assert_eq!(
28470 table.rows.len(),
28471 0,
28472 "lazy dirty tracking must leave MemDatabase row mirroring deferred"
28473 );
28474 }
28475
28476 #[test]
28477 fn test_lazy_dirty_read_from_btree() {
28478 let mut db = MemDatabase::new();
28479 let root = db.create_table(1);
28480
28481 let mut b = ProgramBuilder::new();
28482 let end = b.emit_label();
28483 let done = b.emit_label();
28484 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28485 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28486 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28487 b.emit_op(Opcode::Integer, 41, 2, 0, P4::None, 0);
28488 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28489 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28490 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28491 let body = b.current_addr();
28492 b.emit_op(Opcode::Column, 0, 0, 4, P4::None, 0);
28493 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
28494 let next_target =
28495 i32::try_from(body).expect("program counter should fit into i32 for tests");
28496 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28497 b.resolve_label(done);
28498 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28499 b.resolve_label(end);
28500
28501 let prog = b.finish().expect("program should build");
28502 let mut engine = VdbeEngine::new(prog.register_count());
28503 engine.enable_storage_cursors(true);
28504 engine.set_database(db);
28505 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28506 engine.set_reject_mem_fallback(false);
28507
28508 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28509 assert_eq!(outcome, ExecOutcome::Done);
28510 let rows: Vec<_> = engine
28511 .take_results()
28512 .into_iter()
28513 .map(|row| row.into_vec())
28514 .collect();
28515 assert_eq!(
28516 rows,
28517 vec![vec![SqliteValue::Integer(41)]],
28518 "same-statement reads must come from the storage-backed B-tree when the MemDatabase mirror is stale"
28519 );
28520 assert!(engine.has_dirty_root_pages());
28521 assert!(engine.dirty_root_pages().contains(&root));
28522
28523 let db = engine.take_database().expect("database should exist");
28524 let table = db.get_table(root).expect("table should exist");
28525 assert_eq!(
28526 table.rows.len(),
28527 0,
28528 "B-tree fallback reads must not eagerly repopulate the stale MemDatabase mirror"
28529 );
28530 }
28531
28532 #[test]
28533 fn test_lazy_dirty_clean_table_scan_then_dirty_table_fallback() {
28534 let mut db = MemDatabase::new();
28535 let dirty_root = db.create_table(1);
28536 let clean_root = db.create_table(1);
28537 db.get_table_mut(clean_root)
28538 .expect("clean table should exist")
28539 .insert(1, vec![SqliteValue::Integer(7)]);
28540
28541 let mut b = ProgramBuilder::new();
28542 let end = b.emit_label();
28543 let clean_done = b.emit_label();
28544 let dirty_done = b.emit_label();
28545 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28546 b.emit_op(Opcode::OpenWrite, 0, dirty_root, 0, P4::Int(1), 0);
28547 b.emit_op(Opcode::OpenRead, 1, clean_root, 0, P4::Int(1), 0);
28548
28549 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28550 b.emit_op(Opcode::Integer, 41, 2, 0, P4::None, 0);
28551 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28552 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28553
28554 b.emit_jump_to_label(Opcode::Rewind, 1, 0, clean_done, P4::None, 0);
28555 let clean_body = b.current_addr();
28556 b.emit_op(Opcode::Column, 1, 0, 4, P4::None, 0);
28557 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
28558 let clean_next =
28559 i32::try_from(clean_body).expect("program counter should fit into i32 for tests");
28560 b.emit_op(Opcode::Next, 1, clean_next, 0, P4::None, 0);
28561 b.resolve_label(clean_done);
28562
28563 b.emit_jump_to_label(Opcode::Rewind, 0, 0, dirty_done, P4::None, 0);
28564 let dirty_body = b.current_addr();
28565 b.emit_op(Opcode::Column, 0, 0, 5, P4::None, 0);
28566 b.emit_op(Opcode::ResultRow, 5, 1, 0, P4::None, 0);
28567 let dirty_next =
28568 i32::try_from(dirty_body).expect("program counter should fit into i32 for tests");
28569 b.emit_op(Opcode::Next, 0, dirty_next, 0, P4::None, 0);
28570 b.resolve_label(dirty_done);
28571
28572 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28573 b.resolve_label(end);
28574
28575 let prog = b.finish().expect("program should build");
28576 let mut engine = VdbeEngine::new(prog.register_count());
28577 engine.enable_storage_cursors(true);
28578 engine.set_database(db);
28579 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28580 engine.set_reject_mem_fallback(false);
28581
28582 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28583 assert_eq!(outcome, ExecOutcome::Done);
28584 let rows: Vec<_> = engine
28585 .take_results()
28586 .into_iter()
28587 .map(|row| row.into_vec())
28588 .collect();
28589 assert_eq!(
28590 rows,
28591 vec![
28592 vec![SqliteValue::Integer(7)],
28593 vec![SqliteValue::Integer(41)],
28594 ],
28595 "same-statement reads should preserve clean-table visibility while still sourcing dirty-table rows from the storage-backed B-tree"
28596 );
28597 assert!(engine.has_dirty_root_pages());
28598 assert!(engine.dirty_root_pages().contains(&dirty_root));
28599 assert!(
28600 !engine.dirty_root_pages().contains(&clean_root),
28601 "reading a clean table must not mark it dirty"
28602 );
28603 assert_eq!(
28604 engine.dirty_root_pages().len(),
28605 1,
28606 "only the written table should remain dirty"
28607 );
28608
28609 let db = engine.take_database().expect("database should exist");
28610 assert_eq!(
28611 db.get_table(dirty_root)
28612 .expect("dirty table should exist")
28613 .rows
28614 .len(),
28615 0,
28616 "lazy dirty tracking must keep the written table stale in the MemDatabase mirror"
28617 );
28618 assert_eq!(
28619 db.get_table(clean_root)
28620 .expect("clean table should exist")
28621 .rows
28622 .len(),
28623 1,
28624 "clean-table reads must leave the untouched MemDatabase mirror intact"
28625 );
28626 }
28627
28628 #[test]
28629 fn test_lazy_dirty_multiple_tables() {
28630 let mut db = MemDatabase::new();
28631 let root_a = db.create_table(1);
28632 let root_b = db.create_table(1);
28633
28634 let mut b = ProgramBuilder::new();
28635 let end = b.emit_label();
28636 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28637 b.emit_op(Opcode::OpenWrite, 0, root_a, 0, P4::Int(1), 0);
28638 b.emit_op(Opcode::OpenWrite, 1, root_b, 0, P4::Int(1), 0);
28639
28640 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28641 b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28642 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28643 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28644
28645 b.emit_op(Opcode::NewRowid, 1, 4, 0, P4::None, 0);
28646 b.emit_op(Opcode::Integer, 20, 5, 0, P4::None, 0);
28647 b.emit_op(Opcode::MakeRecord, 5, 1, 6, P4::None, 0);
28648 b.emit_op(Opcode::Insert, 1, 6, 4, P4::None, 0);
28649
28650 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28651 b.resolve_label(end);
28652
28653 let prog = b.finish().expect("program should build");
28654 let mut engine = VdbeEngine::new(prog.register_count());
28655 engine.enable_storage_cursors(true);
28656 engine.set_database(db);
28657 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28658 engine.set_reject_mem_fallback(false);
28659
28660 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28661 assert_eq!(outcome, ExecOutcome::Done);
28662 assert!(engine.has_dirty_root_pages());
28663 assert!(engine.dirty_root_pages().contains(&root_a));
28664 assert!(engine.dirty_root_pages().contains(&root_b));
28665 assert_eq!(
28666 engine.dirty_root_pages().len(),
28667 2,
28668 "lazy dirty tracking must record every dirty table root separately"
28669 );
28670
28671 let db = engine.take_database().expect("database should exist");
28672 assert_eq!(
28673 db.get_table(root_a)
28674 .expect("table A should exist")
28675 .rows
28676 .len(),
28677 0,
28678 "table A should remain stale in MemDatabase until a later reload"
28679 );
28680 assert_eq!(
28681 db.get_table(root_b)
28682 .expect("table B should exist")
28683 .rows
28684 .len(),
28685 0,
28686 "table B should remain stale in MemDatabase until a later reload"
28687 );
28688 }
28689
28690 #[test]
28691 fn test_lazy_dirty_update_after_insert() {
28692 let mut db = MemDatabase::new();
28693 let root = db.create_table(1);
28694
28695 let mut b = ProgramBuilder::new();
28696 let end = b.emit_label();
28697 let done = b.emit_label();
28698 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28699 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28700
28701 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28702 b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28703 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28704 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28705
28706 b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
28707 b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
28708 b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 5);
28709
28710 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28711 let body = b.current_addr();
28712 b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
28713 b.emit_op(Opcode::ResultRow, 6, 1, 0, P4::None, 0);
28714 let next_target =
28715 i32::try_from(body).expect("program counter should fit into i32 for tests");
28716 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28717 b.resolve_label(done);
28718 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28719 b.resolve_label(end);
28720
28721 let prog = b.finish().expect("program should build");
28722 let mut engine = VdbeEngine::new(prog.register_count());
28723 engine.enable_storage_cursors(true);
28724 engine.set_database(db);
28725 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28726 engine.set_reject_mem_fallback(false);
28727
28728 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28729 assert_eq!(outcome, ExecOutcome::Done);
28730 let rows: Vec<_> = engine
28731 .take_results()
28732 .into_iter()
28733 .map(|row| row.into_vec())
28734 .collect();
28735 assert_eq!(
28736 rows,
28737 vec![vec![SqliteValue::Integer(99)]],
28738 "a later storage-backed REPLACE must be visible through the B-tree fallback even while the MemDatabase mirror remains stale"
28739 );
28740 assert!(engine.has_dirty_root_pages());
28741 assert!(engine.dirty_root_pages().contains(&root));
28742 assert_eq!(engine.dirty_root_pages().len(), 1);
28743
28744 let db = engine.take_database().expect("database should exist");
28745 let table = db.get_table(root).expect("table should exist");
28746 assert_eq!(
28747 table.rows.len(),
28748 0,
28749 "lazy dirty tracking must not eagerly maintain the MemDatabase row mirror across update-like writes"
28750 );
28751 }
28752
28753 #[test]
28754 fn test_lazy_dirty_delete_after_insert() {
28755 let mut db = MemDatabase::new();
28756 let root = db.create_table(1);
28757
28758 let mut b = ProgramBuilder::new();
28759 let end = b.emit_label();
28760 let after_delete = b.emit_label();
28761 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28762 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28763
28764 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28765 b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28766 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28767 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28768
28769 b.emit_op(Opcode::Integer, 1, 4, 0, P4::None, 0);
28770 b.emit_jump_to_label(Opcode::SeekRowid, 0, 4, after_delete, P4::None, 0);
28771 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
28772 b.resolve_label(after_delete);
28773
28774 b.emit_op(Opcode::Count, 0, 5, 0, P4::None, 0);
28775 b.emit_op(Opcode::ResultRow, 5, 1, 0, P4::None, 0);
28776 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28777 b.resolve_label(end);
28778
28779 let prog = b.finish().expect("program should build");
28780 let mut engine = VdbeEngine::new(prog.register_count());
28781 engine.enable_storage_cursors(true);
28782 engine.set_database(db);
28783 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28784 engine.set_reject_mem_fallback(false);
28785
28786 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
28787 assert_eq!(outcome, ExecOutcome::Done);
28788 let rows: Vec<_> = engine
28789 .take_results()
28790 .into_iter()
28791 .map(|row| row.into_vec())
28792 .collect();
28793 assert_eq!(
28794 rows,
28795 vec![vec![SqliteValue::Integer(0)]],
28796 "storage-backed COUNT must observe the delete even while MemDatabase mirroring is deferred"
28797 );
28798 assert!(engine.has_dirty_root_pages());
28799 assert!(engine.dirty_root_pages().contains(&root));
28800 assert_eq!(engine.dirty_root_pages().len(), 1);
28801
28802 let db = engine.take_database().expect("database should exist");
28803 let table = db.get_table(root).expect("table should exist");
28804 assert_eq!(
28805 table.rows.len(),
28806 0,
28807 "lazy dirty tracking must leave the MemDatabase empty until a later bulk reload"
28808 );
28809 }
28810
28811 #[test]
28812 fn test_insert_new_rowid_from_btree() {
28813 let mut db = MemDatabase::new();
28815 let root = db.create_table(1);
28816 let table = db.get_table_mut(root).unwrap();
28817 table.insert(1, vec![SqliteValue::Integer(10)]);
28820 table.insert(2, vec![SqliteValue::Integer(20)]);
28821 table.insert(3, vec![SqliteValue::Integer(30)]);
28822 table.next_rowid = 1;
28824
28825 let (rows, _) = run_write_with_storage_cursors(db, |b| {
28826 let end = b.emit_label();
28827 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28828 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28829 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28830 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
28831 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28832 b.resolve_label(end);
28833 });
28834
28835 assert_eq!(rows, vec![vec![SqliteValue::Integer(4)]]);
28837 }
28838
28839 #[test]
28840 fn test_insert_multiple_rows_write_through() {
28841 let mut db = MemDatabase::new();
28843 let root = db.create_table(1);
28844
28845 let (rows, final_db) = run_write_with_storage_cursors(db, |b| {
28846 let end = b.emit_label();
28847 let done = b.emit_label();
28848 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28849 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28850
28851 b.emit_op(Opcode::NewRowid, 0, 1, 0, P4::None, 0);
28853 b.emit_op(Opcode::Integer, 100, 2, 0, P4::None, 0);
28854 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28855 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28856
28857 b.emit_op(Opcode::NewRowid, 0, 4, 0, P4::None, 0);
28859 b.emit_op(Opcode::Integer, 200, 5, 0, P4::None, 0);
28860 b.emit_op(Opcode::MakeRecord, 5, 1, 6, P4::None, 0);
28861 b.emit_op(Opcode::Insert, 0, 6, 4, P4::None, 0);
28862
28863 b.emit_op(Opcode::NewRowid, 0, 7, 0, P4::None, 0);
28865 b.emit_op(Opcode::Integer, 300, 8, 0, P4::None, 0);
28866 b.emit_op(Opcode::MakeRecord, 8, 1, 9, P4::None, 0);
28867 b.emit_op(Opcode::Insert, 0, 9, 7, P4::None, 0);
28868
28869 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28871 let body = b.current_addr();
28872 b.emit_op(Opcode::Column, 0, 0, 10, P4::None, 0);
28873 b.emit_op(Opcode::ResultRow, 10, 1, 0, P4::None, 0);
28874 let next_target =
28875 i32::try_from(body).expect("program counter should fit into i32 for tests");
28876 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28877
28878 b.resolve_label(done);
28879 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28880 b.resolve_label(end);
28881 });
28882
28883 let table = final_db.get_table(root).expect("table should exist");
28885 assert_eq!(table.rows.len(), 0);
28886
28887 assert_eq!(rows.len(), 3);
28889 assert_eq!(rows[0][0], SqliteValue::Integer(100));
28890 assert_eq!(rows[1][0], SqliteValue::Integer(200));
28891 assert_eq!(rows[2][0], SqliteValue::Integer(300));
28892 }
28893
28894 #[test]
28895 fn test_insert_replace_upsert_via_btree() {
28896 let mut db = MemDatabase::new();
28898 let root = db.create_table(1);
28899
28900 let (rows, _) = run_write_with_storage_cursors(db, |b| {
28901 let end = b.emit_label();
28902 let done = b.emit_label();
28903 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
28904 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
28905
28906 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
28908 b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
28909 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
28910 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
28911
28912 b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
28914 b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
28915 b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 5);
28916
28917 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
28919 let body = b.current_addr();
28920 b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
28921 b.emit_op(Opcode::ResultRow, 6, 1, 0, P4::None, 0);
28922 let next_target =
28923 i32::try_from(body).expect("program counter should fit into i32 for tests");
28924 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
28925
28926 b.resolve_label(done);
28927 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
28928 b.resolve_label(end);
28929 });
28930
28931 assert_eq!(rows.len(), 1);
28933 assert_eq!(rows[0][0], SqliteValue::Integer(99));
28934 }
28935
28936 #[test]
28937 fn test_native_replace_row_deletes_conflict_victim_from_memdb_row_mirror() {
28938 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
28939
28940 let pager = MemoryMockMvccPager;
28941 let cx = Cx::new();
28942 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
28943
28944 let mut db = MemDatabase::new();
28945 let table_root = db.create_table(1);
28946 let index_root = 256;
28947 let table = db
28948 .get_table_mut(table_root)
28949 .expect("table mirror should exist");
28950 table.insert(1, vec![SqliteValue::Text("alice@example.com".into())]);
28951 table.insert(2, vec![SqliteValue::Text("bob@example.com".into())]);
28952 table.insert(3, vec![SqliteValue::Text("alice@example.com".into())]);
28953
28954 let mut engine = VdbeEngine::new(8);
28955 engine.set_database(db);
28956 engine.set_memdb_rows_loaded(true);
28957 engine.set_storage_cursor_memdb_count_shortcuts_safe(true);
28958 engine.set_transaction(txn);
28959 engine.table_index_meta = Arc::new(HashMap::from([(
28960 0,
28961 vec![IndexCursorMeta {
28962 cursor_id: 1,
28963 column_indices: vec![0],
28964 }]
28965 .into_boxed_slice(),
28966 )]));
28967
28968 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
28969 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
28970 engine.cursor_root_pages.insert(0, table_root);
28971 engine.cursor_root_pages.insert(1, index_root);
28972
28973 let row1 = encode_record(&[SqliteValue::Text("alice@example.com".into())]);
28974 let row2 = encode_record(&[SqliteValue::Text("bob@example.com".into())]);
28975 let row3 = encode_record(&[SqliteValue::Text("alice@example.com".into())]);
28976 let idx1 = encode_record(&[
28977 SqliteValue::Text("alice@example.com".into()),
28978 SqliteValue::Integer(1),
28979 ]);
28980 let idx2 = encode_record(&[
28981 SqliteValue::Text("bob@example.com".into()),
28982 SqliteValue::Integer(2),
28983 ]);
28984
28985 {
28986 let table_cursor = engine
28987 .storage_cursors
28988 .get_mut(&0)
28989 .expect("table storage cursor should exist");
28990 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
28991 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
28992 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 3, &row3)).unwrap();
28993 }
28994 {
28995 let index_cursor = engine
28996 .storage_cursors
28997 .get_mut(&1)
28998 .expect("index storage cursor should exist");
28999 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29000 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29001 }
29002
29003 run_async(engine.native_replace_row(0, 1)).unwrap();
29004
29005 let mirrored_table = engine
29006 .db
29007 .as_ref()
29008 .and_then(|db| db.get_table(table_root))
29009 .expect("mirrored table should still exist");
29010 let mirrored_rowids: Vec<i64> = mirrored_table.rows.iter().map(|row| row.rowid).collect();
29011 assert_eq!(
29012 mirrored_rowids,
29013 vec![1, 2, 3],
29014 "lazy dirty tracking should leave the MemDB mirror stale until the connection bulk-syncs it"
29015 );
29016 assert!(engine.has_dirty_root_pages());
29017 assert!(engine.dirty_root_pages().contains(&table_root));
29018 assert!(
29019 !engine.storage_cursor_memdb_count_shortcuts_safe(),
29020 "lazy delete mirroring must disable MemDB fast paths until reload"
29021 );
29022
29023 let index_cursor = engine
29024 .storage_cursors
29025 .get_mut(&1)
29026 .expect("index storage cursor should still exist");
29027 assert!(
29028 !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29029 .unwrap()
29030 .is_found(),
29031 "conflicting unique index entry should be removed from storage"
29032 );
29033 assert!(
29034 run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29035 .unwrap()
29036 .is_found(),
29037 "non-conflicting index entries must remain intact"
29038 );
29039 }
29040
29041 #[test]
29042 fn test_native_replace_row_scans_expression_index_for_victim_rowid() {
29043 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29044
29045 let pager = MemoryMockMvccPager;
29046 let cx = Cx::new();
29047 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29048 let mut db = MemDatabase::new();
29049 let table_root = db.create_table(2);
29050 let index_root = 256;
29051
29052 let mut engine = VdbeEngine::new(8);
29053 engine.set_database(db);
29054 engine.set_transaction(txn);
29055 engine.table_index_meta = Arc::new(HashMap::from([(
29056 0,
29057 vec![IndexCursorMeta {
29058 cursor_id: 1,
29059 column_indices: Vec::new(),
29060 }]
29061 .into_boxed_slice(),
29062 )]));
29063
29064 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29065 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29066 engine.cursor_root_pages.insert(0, table_root);
29067 engine.cursor_root_pages.insert(1, index_root);
29068
29069 let victim_row =
29070 encode_record(&[SqliteValue::Text("VICTIM".into()), SqliteValue::Integer(1)]);
29071 let keep_row = encode_record(&[SqliteValue::Text("KEEP".into()), SqliteValue::Integer(2)]);
29072 let victim_index =
29073 encode_record(&[SqliteValue::Text("victim".into()), SqliteValue::Integer(1)]);
29074 let keep_index =
29075 encode_record(&[SqliteValue::Text("keep".into()), SqliteValue::Integer(2)]);
29076
29077 {
29078 let table_cursor = engine.storage_cursors.get_mut(&0).unwrap();
29079 run_async(
29080 table_cursor
29081 .cursor
29082 .table_insert(&table_cursor.cx, 1, &victim_row),
29083 )
29084 .unwrap();
29085 run_async(
29086 table_cursor
29087 .cursor
29088 .table_insert(&table_cursor.cx, 2, &keep_row),
29089 )
29090 .unwrap();
29091 }
29092 {
29093 let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
29094 run_async(
29095 index_cursor
29096 .cursor
29097 .index_insert(&index_cursor.cx, &victim_index),
29098 )
29099 .unwrap();
29100 run_async(
29101 index_cursor
29102 .cursor
29103 .index_insert(&index_cursor.cx, &keep_index),
29104 )
29105 .unwrap();
29106 }
29107
29108 run_async(engine.native_replace_row(0, 1)).unwrap();
29109
29110 let index_cursor = engine.storage_cursors.get_mut(&1).unwrap();
29111 assert!(
29112 !run_async(
29113 index_cursor
29114 .cursor
29115 .index_move_to(&index_cursor.cx, &victim_index)
29116 )
29117 .unwrap()
29118 .is_found(),
29119 "expression-index cleanup must remove the victim's rowid-suffixed key"
29120 );
29121 assert!(
29122 run_async(
29123 index_cursor
29124 .cursor
29125 .index_move_to(&index_cursor.cx, &keep_index)
29126 )
29127 .unwrap()
29128 .is_found(),
29129 "rowid-suffix scanning must preserve other expression-index entries"
29130 );
29131 }
29132
29133 #[test]
29134 fn test_native_replace_row_deletes_index_entry_on_rowid_alias_column() {
29135 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29136
29137 let pager = MemoryMockMvccPager;
29138 let cx = Cx::new();
29139 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29140
29141 let mut db = MemDatabase::new();
29142 let table_root = db.create_table(2);
29143 let index_root = 256;
29144
29145 let mut engine = VdbeEngine::new(8);
29146 engine.set_database(db);
29147 engine.set_transaction(txn);
29148 engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29149 engine.table_index_meta = Arc::new(HashMap::from([(
29150 0,
29151 vec![IndexCursorMeta {
29152 cursor_id: 1,
29153 column_indices: vec![0],
29154 }]
29155 .into_boxed_slice(),
29156 )]));
29157
29158 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29159 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29160 engine.cursor_root_pages.insert(0, table_root);
29161 engine.cursor_root_pages.insert(1, index_root);
29162
29163 let row1 = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29164 let row2 = encode_record(&[SqliteValue::Null, SqliteValue::Text("keep".into())]);
29165 let idx1 = encode_record(&[SqliteValue::Integer(1), SqliteValue::Integer(1)]);
29166 let idx2 = encode_record(&[SqliteValue::Integer(2), SqliteValue::Integer(2)]);
29167
29168 {
29169 let table_cursor = engine
29170 .storage_cursors
29171 .get_mut(&0)
29172 .expect("table storage cursor should exist");
29173 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
29174 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
29175 }
29176 {
29177 let index_cursor = engine
29178 .storage_cursors
29179 .get_mut(&1)
29180 .expect("index storage cursor should exist");
29181 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29182 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29183 }
29184
29185 run_async(engine.native_replace_row(0, 1)).unwrap();
29186
29187 let index_cursor = engine
29188 .storage_cursors
29189 .get_mut(&1)
29190 .expect("index storage cursor should still exist");
29191 assert!(
29192 !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29193 .unwrap()
29194 .is_found(),
29195 "REPLACE cleanup must use the logical rowid alias, not the raw NULL payload slot"
29196 );
29197 assert!(
29198 run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29199 .unwrap()
29200 .is_found(),
29201 "non-conflicting rowid-alias index entries must remain intact"
29202 );
29203 }
29204
29205 #[test]
29206 fn test_native_replace_row_deletes_index_entry_after_multi_alter_short_rowid_alias_payload() {
29207 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29208
29209 let pager = MemoryMockMvccPager;
29210 let cx = Cx::new();
29211 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29212
29213 let mut db = MemDatabase::new();
29214 let table_root = db.create_table(4);
29215 let index_root = 256;
29216
29217 let mut engine = VdbeEngine::new(8);
29218 engine.set_database(db);
29219 engine.set_transaction(txn);
29220 engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29221 engine.set_table_column_count_by_root_page(HashMap::from([(table_root, 4)]));
29222 engine.table_index_meta = Arc::new(HashMap::from([(
29223 0,
29224 vec![IndexCursorMeta {
29225 cursor_id: 1,
29226 column_indices: vec![1],
29227 }]
29228 .into_boxed_slice(),
29229 )]));
29230
29231 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29232 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29233 engine.cursor_root_pages.insert(0, table_root);
29234 engine.cursor_root_pages.insert(1, index_root);
29235
29236 let row1 = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29237 let row2 = encode_record(&[SqliteValue::Null, SqliteValue::Text("keep".into())]);
29238 let idx1 = encode_record(&[SqliteValue::Text("old".into()), SqliteValue::Integer(1)]);
29239 let idx2 = encode_record(&[SqliteValue::Text("keep".into()), SqliteValue::Integer(2)]);
29240
29241 {
29242 let table_cursor = engine
29243 .storage_cursors
29244 .get_mut(&0)
29245 .expect("table storage cursor should exist");
29246 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 1, &row1)).unwrap();
29247 run_async(table_cursor.cursor.table_insert(&table_cursor.cx, 2, &row2)).unwrap();
29248 }
29249 {
29250 let index_cursor = engine
29251 .storage_cursors
29252 .get_mut(&1)
29253 .expect("index storage cursor should exist");
29254 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx1)).unwrap();
29255 run_async(index_cursor.cursor.index_insert(&index_cursor.cx, &idx2)).unwrap();
29256 }
29257
29258 run_async(engine.native_replace_row(0, 1)).unwrap();
29259
29260 let index_cursor = engine
29261 .storage_cursors
29262 .get_mut(&1)
29263 .expect("index storage cursor should still exist");
29264 assert!(
29265 !run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx1))
29266 .unwrap()
29267 .is_found(),
29268 "REPLACE cleanup must not shift columns left when an old row keeps the NULL IPK placeholder"
29269 );
29270 assert!(
29271 run_async(index_cursor.cursor.index_move_to(&index_cursor.cx, &idx2))
29272 .unwrap()
29273 .is_found(),
29274 "non-conflicting short rowid-alias payload index entries must remain intact"
29275 );
29276 }
29277
29278 #[test]
29279 fn test_update_conflict_restore_reinserts_index_entry_on_rowid_alias_column() {
29280 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29281
29282 let pager = MemoryMockMvccPager;
29283 let cx = Cx::new();
29284 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29285
29286 let mut db = MemDatabase::new();
29287 let table_root = db.create_table(2);
29288 let index_root = 256;
29289
29290 let mut engine = VdbeEngine::new(8);
29291 engine.set_database(db);
29292 engine.set_transaction(txn);
29293 engine.set_rowid_alias_column_by_root_page(HashMap::from([(table_root, 0)]));
29294 engine.table_index_meta = Arc::new(HashMap::from([(
29295 0,
29296 vec![IndexCursorMeta {
29297 cursor_id: 1,
29298 column_indices: vec![0],
29299 }]
29300 .into_boxed_slice(),
29301 )]));
29302
29303 assert!(run_async(engine.open_storage_cursor(0, table_root, true)));
29304 assert!(run_async(engine.open_storage_cursor(1, index_root, true)));
29305 engine.cursor_root_pages.insert(0, table_root);
29306 engine.cursor_root_pages.insert(1, index_root);
29307
29308 let restored_row = encode_record(&[SqliteValue::Null, SqliteValue::Text("old".into())]);
29309 let logical_idx = encode_record(&[SqliteValue::Integer(1), SqliteValue::Integer(1)]);
29310 let raw_payload_idx = encode_record(&[SqliteValue::Null, SqliteValue::Integer(1)]);
29311
29312 run_async(
29313 engine.restore_pending_update_after_conflict(PendingUpdateRestore::Storage {
29314 cursor_id: 0,
29315 rowid: 1,
29316 payload: restored_row,
29317 }),
29318 )
29319 .unwrap();
29320
29321 let table_cursor = engine
29322 .storage_cursors
29323 .get_mut(&0)
29324 .expect("table storage cursor should still exist");
29325 assert!(
29326 run_async(table_cursor.cursor.table_move_to(&table_cursor.cx, 1))
29327 .unwrap()
29328 .is_found(),
29329 "UPDATE conflict rollback should restore the original table row"
29330 );
29331
29332 let index_cursor = engine
29333 .storage_cursors
29334 .get_mut(&1)
29335 .expect("index storage cursor should still exist");
29336 assert!(
29337 run_async(
29338 index_cursor
29339 .cursor
29340 .index_move_to(&index_cursor.cx, &logical_idx)
29341 )
29342 .unwrap()
29343 .is_found(),
29344 "UPDATE conflict restore must use the logical rowid alias in index keys"
29345 );
29346 assert!(
29347 !run_async(
29348 index_cursor
29349 .cursor
29350 .index_move_to(&index_cursor.cx, &raw_payload_idx)
29351 )
29352 .unwrap()
29353 .is_found(),
29354 "UPDATE conflict restore must not index the raw NULL rowid-alias payload slot"
29355 );
29356 }
29357
29358 #[test]
29359 fn test_insert_after_missing_probe_disturbed_by_reposition_stays_sorted() {
29360 let mut db = MemDatabase::new();
29361 let root = db.create_table(1);
29362 let table = db.get_table_mut(root).unwrap();
29363 table.insert(10, vec![SqliteValue::Integer(100)]);
29364 table.insert(30, vec![SqliteValue::Integer(300)]);
29365
29366 let (rows, _) = run_write_with_storage_cursors(db, |b| {
29367 let end = b.emit_label();
29368 let done = b.emit_label();
29369 let probe_missing = b.emit_label();
29370 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29371 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29372
29373 b.emit_op(Opcode::Integer, 20, 1, 0, P4::None, 0);
29376 b.emit_jump_to_label(Opcode::NotExists, 0, 1, probe_missing, P4::None, 0);
29377 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
29378 b.resolve_label(probe_missing);
29379
29380 b.emit_op(Opcode::Integer, 10, 2, 0, P4::None, 0);
29382 b.emit_jump_to_label(Opcode::SeekRowid, 0, 2, done, P4::None, 0);
29383
29384 b.emit_op(Opcode::Integer, 200, 3, 0, P4::None, 0);
29386 b.emit_op(Opcode::MakeRecord, 3, 1, 4, P4::None, 0);
29387 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
29388
29389 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
29390 let body = b.current_addr();
29391 b.emit_op(Opcode::Rowid, 0, 5, 0, P4::None, 0);
29392 b.emit_op(Opcode::Column, 0, 0, 6, P4::None, 0);
29393 b.emit_op(Opcode::ResultRow, 5, 2, 0, P4::None, 0);
29394 let next_target =
29395 i32::try_from(body).expect("program counter should fit into i32 for tests");
29396 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
29397
29398 b.resolve_label(done);
29399 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29400 b.resolve_label(end);
29401 });
29402
29403 assert_eq!(
29404 rows,
29405 vec![
29406 vec![SqliteValue::Integer(10), SqliteValue::Integer(100)],
29407 vec![SqliteValue::Integer(20), SqliteValue::Integer(200)],
29408 vec![SqliteValue::Integer(30), SqliteValue::Integer(300)],
29409 ]
29410 );
29411 }
29412
29413 #[test]
29414 fn test_insert_default_conflict_errors_via_btree() {
29415 let _guard = VDBE_OBSERVABILITY_LOCK
29416 .lock()
29417 .unwrap_or_else(|e| e.into_inner());
29418 reset_vdbe_test_sideband_materialization_count();
29419
29420 let mut db = MemDatabase::new();
29422 let root = db.create_table(1);
29423 let table = db.get_table_mut(root).expect("table should exist");
29424 table.insert(1, vec![SqliteValue::Integer(10)]);
29425
29426 let mut b = ProgramBuilder::new();
29427 let end = b.emit_label();
29428 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29429 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29430
29431 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
29433 b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
29434 b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
29435 b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 0);
29436
29437 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29438 b.resolve_label(end);
29439
29440 let prog = b.finish().expect("program should build");
29441 let expected_capacity = estimate_make_record_buffer_capacity(&prog, PageSize::DEFAULT);
29442 let mut engine = VdbeEngine::new(prog.register_count());
29443 engine.enable_storage_cursors(true);
29444 engine.set_database(db);
29445 engine.set_reject_mem_fallback(false);
29446
29447 let before = vdbe_test_sideband_materialization_count_snapshot();
29448 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
29449 let after = vdbe_test_sideband_materialization_count_snapshot();
29450 assert_eq!(
29451 outcome,
29452 ExecOutcome::Error {
29453 code: ErrorCode::Constraint as i32,
29454 message: "PRIMARY KEY constraint failed".to_owned(),
29455 }
29456 );
29457 assert_eq!(
29458 after - before,
29459 0,
29460 "duplicate Insert should consume MakeRecord sideband bytes without materializing"
29461 );
29462 assert!(
29463 engine.make_record_lookaside.is_empty(),
29464 "duplicate Insert should return a cleared scratch buffer"
29465 );
29466 assert!(
29467 engine.make_record_lookaside.capacity() >= expected_capacity,
29468 "duplicate Insert should keep the reusable sideband allocation"
29469 );
29470
29471 let db = engine.take_database().expect("database should exist");
29472 let table = db.get_table(root).expect("table should exist");
29473 assert_eq!(table.rows.len(), 1);
29474 assert_eq!(table.rows[0].values.as_slice(), &[SqliteValue::Integer(10)]);
29475 }
29476
29477 #[test]
29478 fn test_insert_default_conflict_errors_memdb_path() {
29479 let mut db = MemDatabase::new();
29481 let root = db.create_table(1);
29482 let table = db.get_table_mut(root).expect("table should exist");
29483 table.insert(1, vec![SqliteValue::Integer(10)]);
29484
29485 let mut b = ProgramBuilder::new();
29486 let end = b.emit_label();
29487 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
29488 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
29489
29490 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
29492 b.emit_op(Opcode::Integer, 99, 4, 0, P4::None, 0);
29493 b.emit_op(Opcode::MakeRecord, 4, 1, 5, P4::None, 0);
29494 b.emit_op(Opcode::Insert, 0, 5, 1, P4::None, 0);
29495
29496 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
29497 b.resolve_label(end);
29498
29499 let prog = b.finish().expect("program should build");
29500 let mut engine = VdbeEngine::new(prog.register_count());
29501 engine.enable_storage_cursors(false);
29502 engine.set_database(db);
29503 engine.set_reject_mem_fallback(false);
29504
29505 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
29506 assert_eq!(
29507 outcome,
29508 ExecOutcome::Error {
29509 code: ErrorCode::Constraint as i32,
29510 message: "PRIMARY KEY constraint failed".to_owned(),
29511 }
29512 );
29513
29514 let db = engine.take_database().expect("database should exist");
29515 let table = db.get_table(root).expect("table should exist");
29516 assert_eq!(table.rows.len(), 1);
29517 assert_eq!(table.rows[0].values.as_slice(), &[SqliteValue::Integer(10)]);
29518 }
29519
29520 #[test]
29523 fn test_set_transaction_enables_storage_cursors() {
29524 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29525
29526 let pager = MockMvccPager;
29527 let cx = Cx::new();
29528 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29529
29530 let mut engine = VdbeEngine::new(8);
29531 assert!(engine.storage_cursors_enabled);
29532
29533 engine.set_transaction(txn);
29535 assert!(engine.storage_cursors_enabled);
29536 assert!(engine.txn_page_io.is_some());
29537 }
29538
29539 #[test]
29540 fn test_storage_cursors_enabled_by_default() {
29541 let engine = VdbeEngine::new(8);
29542 assert!(engine.storage_cursors_enabled);
29543 assert!(engine.txn_page_io.is_none());
29544 }
29545
29546 #[test]
29547 fn test_take_transaction_returns_handle() {
29548 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29549
29550 let pager = MockMvccPager;
29551 let cx = Cx::new();
29552 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29553
29554 let mut engine = VdbeEngine::new(8);
29555 engine.set_transaction(txn);
29556
29557 let recovered = engine
29559 .take_transaction()
29560 .expect("take_transaction should succeed");
29561 assert!(recovered.is_some());
29562 assert!(engine.txn_page_io.is_none());
29563 assert!(engine.storage_cursors.is_empty());
29564 }
29565
29566 #[test]
29567 fn test_set_transaction_concurrent_refills_retained_shared_state() {
29568 use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29569 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29570 use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29571
29572 let pager = MockMvccPager;
29573 let cx = Cx::new();
29574 let txn1 = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29575 let txn2 = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29576
29577 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29578 let lock_table = Arc::new(InProcessPageLockTable::new());
29579 let commit_index = Arc::new(CommitIndex::new());
29580 let ((session1, handle1), (session2, handle2)) = {
29581 let mut guard = registry
29582 .lock()
29583 .unwrap_or_else(std::sync::PoisonError::into_inner);
29584 let session1 = guard
29585 .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29586 .expect("first session should register");
29587 let session2 = guard
29588 .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29589 .expect("second session should register");
29590 let handle1 = guard
29591 .handle(session1)
29592 .expect("first session handle should exist");
29593 let handle2 = guard
29594 .handle(session2)
29595 .expect("second session handle should exist");
29596 ((session1, handle1), (session2, handle2))
29597 };
29598
29599 let mut engine = VdbeEngine::new(8);
29600 engine.set_transaction_concurrent(
29601 txn1,
29602 session1,
29603 Arc::clone(&handle1),
29604 Arc::clone(&lock_table),
29605 Arc::clone(&commit_index),
29606 0,
29607 );
29608
29609 let mut retained = engine
29610 .txn_page_io
29611 .as_ref()
29612 .expect("engine should install shared txn state")
29613 .clone();
29614 let _drained = engine
29615 .drain_transaction()
29616 .expect("drain_transaction should leave retained state behind");
29617
29618 engine.set_transaction_concurrent(
29619 txn2,
29620 session2,
29621 Arc::clone(&handle2),
29622 Arc::clone(&lock_table),
29623 Arc::clone(&commit_index),
29624 0,
29625 );
29626
29627 run_async(retained.read_page_data(&cx, PageNumber::ONE))
29628 .expect("retained clone should see the refilled transaction instead of Drained");
29629 retained.record_write_witness(&cx, WitnessKey::Page(PageNumber::ONE));
29630
29631 let guard = registry
29632 .lock()
29633 .unwrap_or_else(std::sync::PoisonError::into_inner);
29634 let first = guard
29635 .get(session1)
29636 .expect("first session should remain registered");
29637 let second = guard
29638 .get(session2)
29639 .expect("second session should remain registered");
29640 assert!(
29641 !first
29642 .read_witness_keys()
29643 .contains(&WitnessKey::Page(PageNumber::ONE)),
29644 "retained clone must stop recording reads against the drained concurrent session"
29645 );
29646 assert!(
29647 !first
29648 .write_witness_keys()
29649 .contains(&WitnessKey::Page(PageNumber::ONE)),
29650 "retained clone must stop recording writes against the drained concurrent session"
29651 );
29652 assert!(
29653 second
29654 .read_witness_keys()
29655 .contains(&WitnessKey::Page(PageNumber::ONE)),
29656 "retained clone must record reads on the refilled concurrent session"
29657 );
29658 assert!(
29659 second
29660 .write_witness_keys()
29661 .contains(&WitnessKey::Page(PageNumber::ONE)),
29662 "retained clone must record writes on the refilled concurrent session"
29663 );
29664 }
29665
29666 #[test]
29667 fn test_shared_txn_page_io_btree_read_defers_to_precise_witnesses() {
29668 use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29669 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29670 use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29671
29672 let pager = MemoryMockMvccPager;
29673 let cx = Cx::new();
29674 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29675
29676 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29677 let lock_table = Arc::new(InProcessPageLockTable::new());
29678 let commit_index = Arc::new(CommitIndex::new());
29679 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
29680 let (session_id, handle) = {
29681 let mut guard = registry
29682 .lock()
29683 .unwrap_or_else(std::sync::PoisonError::into_inner);
29684 let session_id = guard
29685 .begin_concurrent(snapshot)
29686 .expect("session should register");
29687 let handle = guard
29688 .handle(session_id)
29689 .expect("session handle should exist");
29690 (session_id, handle)
29691 };
29692
29693 let page_io = SharedTxnPageIo::with_concurrent(
29694 txn,
29695 session_id,
29696 Arc::clone(&handle),
29697 Arc::clone(&lock_table),
29698 Arc::clone(&commit_index),
29699 0,
29700 );
29701
29702 let _ = run_async(page_io.read_btree_page_data(&cx, PageNumber::ONE))
29703 .expect("B-tree page read should succeed");
29704 {
29705 let guard = handle.lock();
29706 assert!(
29707 guard.read_witness_keys().is_empty(),
29708 "B-tree page loads must not publish coarse page read witnesses"
29709 );
29710 }
29711
29712 let cell_witness = WitnessKey::Cell {
29713 btree_root: PageNumber::ONE,
29714 leaf_page: PageNumber::ONE,
29715 tag: 42,
29716 };
29717 page_io.record_read_witness(&cx, cell_witness.clone());
29718
29719 let guard = handle.lock();
29720 assert!(
29721 guard.read_witness_keys().contains(&cell_witness),
29722 "cursor-supplied cell witness must be recorded on the concurrent handle"
29723 );
29724 assert!(
29725 !guard
29726 .read_witness_keys()
29727 .contains(&WitnessKey::Page(PageNumber::ONE)),
29728 "precise B-tree witness publication must not reintroduce coarse page reads"
29729 );
29730 }
29731
29732 #[test]
29733 fn test_set_transaction_clears_stale_concurrent_context_on_retained_reuse() {
29734 use fsqlite_mvcc::{CommitIndex, ConcurrentRegistry, InProcessPageLockTable};
29735 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29736 use fsqlite_types::{CommitSeq, PageNumber, SchemaEpoch, Snapshot, WitnessKey};
29737
29738 let pager = MockMvccPager;
29739 let cx = Cx::new();
29740 let concurrent_txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
29741 let immediate_txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29742
29743 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
29744 let lock_table = Arc::new(InProcessPageLockTable::new());
29745 let commit_index = Arc::new(CommitIndex::new());
29746 let (session_id, handle) = {
29747 let mut guard = registry
29748 .lock()
29749 .unwrap_or_else(std::sync::PoisonError::into_inner);
29750 let session_id = guard
29751 .begin_concurrent(Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1)))
29752 .expect("session should register");
29753 let handle = guard
29754 .handle(session_id)
29755 .expect("session handle should exist");
29756 (session_id, handle)
29757 };
29758
29759 let mut engine = VdbeEngine::new(8);
29760 engine.set_transaction_concurrent(
29761 concurrent_txn,
29762 session_id,
29763 Arc::clone(&handle),
29764 Arc::clone(&lock_table),
29765 Arc::clone(&commit_index),
29766 0,
29767 );
29768
29769 let mut retained = engine
29770 .txn_page_io
29771 .as_ref()
29772 .expect("engine should install shared txn state")
29773 .clone();
29774 let _drained = engine
29775 .drain_transaction()
29776 .expect("drain_transaction should leave retained state behind");
29777
29778 engine.set_transaction(immediate_txn);
29779
29780 run_async(retained.read_page_data(&cx, PageNumber::ONE)).expect(
29781 "plain set_transaction should refill the retained clone instead of leaving Drained",
29782 );
29783 retained.record_write_witness(&cx, WitnessKey::Page(PageNumber::ONE));
29784
29785 let guard = registry
29786 .lock()
29787 .unwrap_or_else(std::sync::PoisonError::into_inner);
29788 let session = guard
29789 .get(session_id)
29790 .expect("original concurrent session should remain registered");
29791 assert!(
29792 !session
29793 .read_witness_keys()
29794 .contains(&WitnessKey::Page(PageNumber::ONE)),
29795 "plain set_transaction must clear stale concurrent read tracking on retained clones"
29796 );
29797 assert!(
29798 !session
29799 .write_witness_keys()
29800 .contains(&WitnessKey::Page(PageNumber::ONE)),
29801 "plain set_transaction must clear stale concurrent write tracking on retained clones"
29802 );
29803 }
29804
29805 #[test]
29806 fn test_open_storage_cursor_prefers_txn_backend() {
29807 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
29808
29809 let pager = MockMvccPager;
29810 let cx = Cx::new();
29811 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29812
29813 let mut db = MemDatabase::new();
29814 let root = db.create_table(1);
29815
29816 let mut engine = VdbeEngine::new(8);
29817 engine.set_database(db);
29818 engine.set_transaction(txn);
29819
29820 let opened = run_async(engine.open_storage_cursor(0, root, false));
29822 assert!(opened);
29823
29824 assert!(engine.storage_cursors.contains_key(&0));
29826
29827 engine.storage_cursors.clear();
29829 let _txn = engine
29830 .take_transaction()
29831 .expect("take_transaction should succeed");
29832 }
29833
29834 #[test]
29835 fn test_open_storage_cursor_txn_index_honors_desc_key_metadata() {
29836 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29837 use fsqlite_types::record::{parse_record, serialize_record};
29838
29839 let pager = MemoryMockMvccPager;
29840 let cx = Cx::new();
29841 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29842 let root = 256;
29843
29844 let mut engine = VdbeEngine::new(8);
29845 engine.set_database(MemDatabase::new());
29846 engine.set_transaction(txn);
29847 engine.set_index_desc_flags_by_root_page(HashMap::from([(root, vec![true])]));
29848
29849 assert!(
29850 run_async(engine.open_storage_cursor(0, root, true)),
29851 "writable txn-backed index cursor should open on a fresh root page"
29852 );
29853
29854 let sc = engine.storage_cursors.get_mut(&0).unwrap();
29855 let early_key = serialize_record(&[SqliteValue::Integer(10), SqliteValue::Integer(1)]);
29856 let late_key = serialize_record(&[SqliteValue::Integer(20), SqliteValue::Integer(2)]);
29857 run_async(sc.cursor.index_insert(&sc.cx, &early_key)).unwrap();
29858 run_async(sc.cursor.index_insert(&sc.cx, &late_key)).unwrap();
29859
29860 assert!(run_async(sc.cursor.first(&sc.cx)).unwrap());
29861 let first_values = parse_record(&run_async(sc.cursor.payload(&sc.cx)).unwrap()).unwrap();
29862 assert_eq!(
29863 first_values,
29864 vec![SqliteValue::Integer(20), SqliteValue::Integer(2)],
29865 "descending index cursor should order the larger key first"
29866 );
29867
29868 assert!(run_async(sc.cursor.next(&sc.cx)).unwrap());
29869 let second_values = parse_record(&run_async(sc.cursor.payload(&sc.cx)).unwrap()).unwrap();
29870 assert_eq!(
29871 second_values,
29872 vec![SqliteValue::Integer(10), SqliteValue::Integer(1)],
29873 "descending index cursor should keep the smaller key after the larger one"
29874 );
29875
29876 engine.storage_cursors.clear();
29877 let _txn = engine
29878 .take_transaction()
29879 .expect("take_transaction should succeed");
29880 }
29881
29882 #[test]
29883 fn test_open_storage_cursor_txn_zero_page_honors_page1_reserved_bytes_layout() {
29884 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29885
29886 let pager = MemoryMockMvccPager;
29887 let cx = Cx::new();
29888 let mut txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29889 let root_pgno = PageNumber::new(256).unwrap();
29890
29891 let mut header = DatabaseHeader {
29892 page_size: PageSize::DEFAULT,
29893 reserved_per_page: 32,
29894 page_count: root_pgno.get(),
29895 ..DatabaseHeader::default()
29896 };
29897 header.version_valid_for = header.change_counter;
29898 let mut page_one = vec![0u8; PageSize::DEFAULT.as_usize()];
29899 page_one[..DATABASE_HEADER_SIZE].copy_from_slice(&header.to_bytes().unwrap());
29900 run_async(txn.write_page(&cx, PageNumber::ONE, &page_one)).unwrap();
29901
29902 let mut engine = VdbeEngine::new(8);
29903 engine.set_transaction(txn);
29904
29905 assert!(
29906 run_async(engine.open_storage_cursor(0, root_pgno.get() as i32, true)),
29907 "txn-backed writable cursor should open on a fresh reserved-byte root page"
29908 );
29909
29910 let storage_cursor = engine.storage_cursors.get(&0).unwrap();
29911 assert_eq!(
29912 storage_cursor.cursor.usable_size(),
29913 PageSize::DEFAULT.get() - 32
29914 );
29915 assert_eq!(storage_cursor.cursor.page_size(), PageSize::DEFAULT.get());
29916
29917 let root_page = run_async(
29918 engine
29919 .txn_page_io
29920 .as_ref()
29921 .unwrap()
29922 .read_page(&cx, root_pgno),
29923 )
29924 .unwrap();
29925 assert_eq!(root_page[0], BtreePageType::LeafTable as u8);
29926 assert_eq!(
29927 u16::from_be_bytes([root_page[5], root_page[6]]),
29928 (PageSize::DEFAULT.get() - 32) as u16,
29929 "fresh reserved-byte root pages must start their cell content area at usable_size"
29930 );
29931
29932 engine.storage_cursors.clear();
29933 let _txn = engine
29934 .take_transaction()
29935 .expect("take_transaction should succeed");
29936 }
29937
29938 #[test]
29939 fn test_open_storage_cursor_zero_page_init_uses_owned_passthrough_write() {
29940 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
29941
29942 let _guard = VDBE_OBSERVABILITY_LOCK
29948 .lock()
29949 .unwrap_or_else(|e| e.into_inner());
29950 let prev_metrics_enabled = vdbe_metrics_enabled();
29951 reset_vdbe_metrics();
29952 set_vdbe_metrics_enabled(true);
29953
29954 let pager = MemoryMockMvccPager;
29955 let cx = Cx::new();
29956 let mut txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
29957 let root_pgno = PageNumber::new(2).unwrap();
29958
29959 let mut header = DatabaseHeader {
29960 page_size: PageSize::DEFAULT,
29961 reserved_per_page: 0,
29962 page_count: root_pgno.get(),
29963 ..DatabaseHeader::default()
29964 };
29965 header.version_valid_for = header.change_counter;
29966 let mut page_one = vec![0u8; PageSize::DEFAULT.as_usize()];
29967 page_one[..DATABASE_HEADER_SIZE].copy_from_slice(&header.to_bytes().unwrap());
29968 run_async(txn.write_page(&cx, PageNumber::ONE, &page_one)).unwrap();
29969
29970 let mut engine = VdbeEngine::new(8);
29971 engine.set_transaction(txn);
29972
29973 let before = vdbe_metrics_snapshot();
29976 assert!(
29977 run_async(engine.open_storage_cursor(0, root_pgno.get() as i32, true)),
29978 "txn-backed writable cursor should open on a fresh zeroed root page"
29979 );
29980 let after = vdbe_metrics_snapshot();
29981
29982 assert!(
29985 after.page_data_motion.owned_passthrough_total
29986 - before.page_data_motion.owned_passthrough_total
29987 >= 1,
29988 "zeroed-root init must write its owned page image via owned passthrough"
29989 );
29990 assert_eq!(
29991 after.page_data_motion.borrowed_exact_size_copies_total
29992 - before.page_data_motion.borrowed_exact_size_copies_total,
29993 0,
29994 "zeroed-root init must not clone a full page image through the borrowed lane"
29995 );
29996 assert_eq!(
29997 after.page_data_motion.owned_resized_copies_total
29998 - before.page_data_motion.owned_resized_copies_total,
29999 0,
30000 "an exactly-page-size root image must never trigger a resize copy"
30001 );
30002
30003 let root_page = run_async(
30006 engine
30007 .txn_page_io
30008 .as_ref()
30009 .unwrap()
30010 .read_page(&cx, root_pgno),
30011 )
30012 .unwrap();
30013 assert_eq!(root_page[0], BtreePageType::LeafTable as u8);
30014 assert_eq!(
30015 u32::from(u16::from_be_bytes([root_page[5], root_page[6]])),
30016 PageSize::DEFAULT.get(),
30017 "fresh reserved-byte-free root pages start their cell content area at usable_size"
30018 );
30019
30020 engine.storage_cursors.clear();
30021 let _txn = engine
30022 .take_transaction()
30023 .expect("take_transaction should succeed");
30024 set_vdbe_metrics_enabled(prev_metrics_enabled);
30025 }
30026
30027 #[test]
30028 fn test_open_storage_cursor_records_root_page_for_manual_open() {
30029 let mut engine = VdbeEngine::new(8);
30030 let mut db = MemDatabase::new();
30031 let root_page = db.allocate_root_page();
30032 engine.enable_storage_cursors(true);
30033 engine.set_database(db);
30034 engine.set_reject_mem_fallback(false);
30035
30036 assert!(
30037 run_async(engine.open_storage_cursor(7, root_page, true)),
30038 "manual storage cursor open should succeed"
30039 );
30040 assert_eq!(
30041 engine.cursor_root_pages.get(&7),
30042 Some(&root_page),
30043 "manual open must publish root-page metadata for later index collation lookups"
30044 );
30045 assert_eq!(
30046 engine
30047 .storage_cursors
30048 .get(&7)
30049 .expect("storage cursor should exist")
30050 .root_page,
30051 root_page,
30052 "cursor metadata and published root-page map must stay in sync"
30053 );
30054 }
30055
30056 #[test]
30057 fn test_storage_cursor_no_conflict_scratch_matches_legacy_probe_semantics() {
30058 let _guard = VDBE_OBSERVABILITY_LOCK
30059 .lock()
30060 .unwrap_or_else(|e| e.into_inner());
30061 let (mut engine, conflict_key, miss_key) =
30062 build_storage_index_engine_with_duplicate_prefixes();
30063 let cursor = engine
30064 .storage_cursors
30065 .get_mut(&0)
30066 .expect("storage cursor should exist");
30067
30068 let legacy_conflict = legacy_storage_cursor_no_conflict_prefix_match(cursor, &conflict_key)
30069 .expect("legacy conflict probe should succeed");
30070 let scratch_conflict = run_async(storage_cursor_no_conflict_prefix_match(
30071 0,
30072 cursor,
30073 &conflict_key,
30074 ))
30075 .expect("scratch conflict probe should succeed");
30076 assert_eq!(scratch_conflict, legacy_conflict);
30077 assert!(scratch_conflict, "duplicate prefix should conflict");
30078
30079 let legacy_miss = legacy_storage_cursor_no_conflict_prefix_match(cursor, &miss_key)
30080 .expect("legacy miss probe should succeed");
30081 let scratch_miss = run_async(storage_cursor_no_conflict_prefix_match(
30082 0, cursor, &miss_key,
30083 ))
30084 .expect("scratch miss probe should succeed");
30085 assert_eq!(scratch_miss, legacy_miss);
30086 assert!(!scratch_miss, "distinct prefix should not conflict");
30087
30088 let malformed = vec![0x80];
30089 let legacy_malformed = legacy_storage_cursor_no_conflict_prefix_match(cursor, &malformed)
30090 .expect("legacy malformed probe should not raise");
30091 let scratch_malformed = run_async(storage_cursor_no_conflict_prefix_match(
30092 0, cursor, &malformed,
30093 ))
30094 .expect("scratch malformed probe should not raise");
30095 assert_eq!(scratch_malformed, legacy_malformed);
30096 assert!(
30097 !scratch_malformed,
30098 "malformed probes should not report conflicts"
30099 );
30100 }
30101
30102 #[test]
30103 fn test_storage_cursor_no_conflict_collated_probe_honors_nocase() {
30104 let _guard = VDBE_OBSERVABILITY_LOCK
30105 .lock()
30106 .unwrap_or_else(|e| e.into_inner());
30107 let (mut engine, conflict_key, miss_key) =
30108 build_storage_index_engine_with_nocase_prefixes();
30109 let coll_arc = Arc::clone(&engine.collation_registry);
30110 let coll_guard = coll_arc.lock().unwrap_or_else(|e| e.into_inner());
30111 let root_page = engine
30112 .cursor_root_pages
30113 .get(&0)
30114 .copied()
30115 .expect("storage cursor should record its root page");
30116 let desc_flags = engine.index_desc_flags_for_root(root_page);
30117 let collations = engine.index_collations_for_root(root_page);
30118 let cursor = engine
30119 .storage_cursors
30120 .get_mut(&0)
30121 .expect("storage cursor should exist");
30122
30123 assert!(
30124 run_async(storage_cursor_no_conflict_prefix_match_collated(
30125 0,
30126 cursor,
30127 &conflict_key,
30128 &desc_flags,
30129 &collations,
30130 &coll_guard,
30131 ))
30132 .expect("collated conflict probe should succeed"),
30133 "NOCASE-equivalent text must be treated as a duplicate prefix"
30134 );
30135 assert!(
30136 !run_async(storage_cursor_no_conflict_prefix_match_collated(
30137 0,
30138 cursor,
30139 &miss_key,
30140 &desc_flags,
30141 &collations,
30142 &coll_guard,
30143 ))
30144 .expect("collated miss probe should succeed"),
30145 "distinct NOCASE text must remain insertable"
30146 );
30147 }
30148
30149 #[test]
30150 fn test_storage_cursor_no_conflict_reuses_payload_and_value_scratch() {
30151 let _guard = VDBE_OBSERVABILITY_LOCK
30152 .lock()
30153 .unwrap_or_else(|e| e.into_inner());
30154 let (mut engine, conflict_key, _) = build_storage_index_engine_with_duplicate_prefixes();
30155 let iterations = 256;
30156 let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
30157
30158 let run_legacy = |cursor: &mut StorageCursor| {
30159 fsqlite_types::record::reset_record_profile();
30160 fsqlite_btree::reset_btree_copy_profile();
30161 fsqlite_btree::set_btree_copy_profile_enabled(true);
30162 let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30163 let mut matches = 0usize;
30164 for _ in 0..iterations {
30165 if legacy_storage_cursor_no_conflict_prefix_match(cursor, &conflict_key)
30166 .expect("legacy probe should succeed")
30167 {
30168 matches += 1;
30169 }
30170 }
30171 (
30172 matches,
30173 fsqlite_types::record::record_profile_snapshot(),
30174 fsqlite_btree::btree_copy_profile_snapshot(),
30175 )
30176 };
30177
30178 let run_scratch = |cursor: &mut StorageCursor| {
30179 fsqlite_types::record::reset_record_profile();
30180 fsqlite_btree::reset_btree_copy_profile();
30181 fsqlite_btree::set_btree_copy_profile_enabled(true);
30182 let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30183 let mut matches = 0usize;
30184 let mut payload_ptr = None;
30185 let mut value_ptr = None;
30186 for _ in 0..iterations {
30187 if run_async(storage_cursor_no_conflict_prefix_match(
30188 0,
30189 cursor,
30190 &conflict_key,
30191 ))
30192 .expect("scratch probe should succeed")
30193 {
30194 matches += 1;
30195 }
30196 let payload_buf_ptr = cursor.payload_buf.as_ptr() as usize;
30197 let cur_vals_ptr = cursor.cur_vals_buf.as_ptr() as usize;
30198 if let Some(expected) = payload_ptr {
30199 assert_eq!(
30200 payload_buf_ptr, expected,
30201 "payload scratch buffer should be reused across probes"
30202 );
30203 } else {
30204 payload_ptr = Some(payload_buf_ptr);
30205 }
30206 if let Some(expected) = value_ptr {
30207 assert_eq!(
30208 cur_vals_ptr, expected,
30209 "value scratch buffer should be reused across probes"
30210 );
30211 } else {
30212 value_ptr = Some(cur_vals_ptr);
30213 }
30214 }
30215 (
30216 matches,
30217 fsqlite_types::record::record_profile_snapshot(),
30218 fsqlite_btree::btree_copy_profile_snapshot(),
30219 )
30220 };
30221
30222 let (legacy_matches, legacy_record_profile, legacy_copy_profile) = {
30223 let cursor = engine
30224 .storage_cursors
30225 .get_mut(&0)
30226 .expect("storage cursor should exist");
30227 run_legacy(cursor)
30228 };
30229 let (scratch_matches, scratch_record_profile, scratch_copy_profile) = {
30230 let cursor = engine
30231 .storage_cursors
30232 .get_mut(&0)
30233 .expect("storage cursor should exist");
30234 run_scratch(cursor)
30235 };
30236
30237 assert_eq!(legacy_matches, iterations);
30238 assert_eq!(scratch_matches, iterations);
30239 assert!(
30240 legacy_record_profile
30241 .callsite_breakdown
30242 .vdbe_engine
30243 .parse_record_calls
30244 >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30245 "legacy loop should fully parse both probe and entry on every iteration"
30246 );
30247 assert_eq!(
30248 scratch_record_profile
30249 .callsite_breakdown
30250 .vdbe_engine
30251 .parse_record_calls,
30252 0,
30253 "scratch path should eliminate full parse_record allocations"
30254 );
30255 assert!(
30256 scratch_record_profile
30257 .callsite_breakdown
30258 .vdbe_engine
30259 .parse_record_into_calls
30260 >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30261 "scratch path should decode probe and entry into reusable buffers"
30262 );
30263 assert!(
30264 legacy_copy_profile.owned_payload_materialization_calls
30265 >= u64::try_from(iterations).unwrap_or(u64::MAX),
30266 "legacy path should materialize an owned payload for every probe"
30267 );
30268 assert_eq!(
30269 scratch_copy_profile.owned_payload_materialization_calls, 0,
30270 "scratch path should avoid owned payload materialization"
30271 );
30272 assert!(
30273 scratch_copy_profile.local_payload_copy_calls
30274 >= u64::try_from(iterations).unwrap_or(u64::MAX),
30275 "scratch path should reuse the caller-owned payload buffer"
30276 );
30277
30278 eprintln!(
30279 concat!(
30280 "BD_DB300_4_3_2_CHURN_JSON=",
30281 "{{",
30282 "\"bead_id\":\"bd-db300.4.3.2\",",
30283 "\"hot_path\":\"storage_cursor_no_conflict_index_probe\",",
30284 "\"iterations\":{},",
30285 "\"legacy\":{{",
30286 "\"parse_record_calls\":{},",
30287 "\"parse_record_into_calls\":{},",
30288 "\"owned_payload_materialization_calls\":{},",
30289 "\"owned_payload_materialization_bytes\":{},",
30290 "\"local_payload_copy_calls\":{}",
30291 "}},",
30292 "\"scratch\":{{",
30293 "\"parse_record_calls\":{},",
30294 "\"parse_record_into_calls\":{},",
30295 "\"owned_payload_materialization_calls\":{},",
30296 "\"owned_payload_materialization_bytes\":{},",
30297 "\"local_payload_copy_calls\":{}",
30298 "}}",
30299 "}}"
30300 ),
30301 iterations,
30302 legacy_record_profile
30303 .callsite_breakdown
30304 .vdbe_engine
30305 .parse_record_calls,
30306 legacy_record_profile
30307 .callsite_breakdown
30308 .vdbe_engine
30309 .parse_record_into_calls,
30310 legacy_copy_profile.owned_payload_materialization_calls,
30311 legacy_copy_profile.owned_payload_materialization_bytes,
30312 legacy_copy_profile.local_payload_copy_calls,
30313 scratch_record_profile
30314 .callsite_breakdown
30315 .vdbe_engine
30316 .parse_record_calls,
30317 scratch_record_profile
30318 .callsite_breakdown
30319 .vdbe_engine
30320 .parse_record_into_calls,
30321 scratch_copy_profile.owned_payload_materialization_calls,
30322 scratch_copy_profile.owned_payload_materialization_bytes,
30323 scratch_copy_profile.local_payload_copy_calls,
30324 );
30325
30326 fsqlite_btree::set_btree_copy_profile_enabled(false);
30327 }
30328
30329 #[test]
30330 fn test_storage_cursor_no_conflict_mixed_boundaries_reduce_allocator_pressure_and_preserve_reset_safety()
30331 {
30332 let _guard = VDBE_OBSERVABILITY_LOCK
30333 .lock()
30334 .unwrap_or_else(|e| e.into_inner());
30335 let (mut engine, conflict_key, miss_key) =
30336 build_storage_index_engine_with_duplicate_prefixes();
30337 let malformed_key = vec![0x80];
30338 let conflict_target = [SqliteValue::Integer(7)];
30339 let miss_target = [SqliteValue::Integer(9)];
30340 let iterations = 128usize;
30341 let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
30342
30343 let run_legacy = |cursor: &mut StorageCursor| {
30344 fsqlite_types::record::reset_record_profile();
30345 fsqlite_btree::reset_btree_copy_profile();
30346 fsqlite_btree::set_btree_copy_profile_enabled(true);
30347 let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30348 let mut matches = 0usize;
30349 for _ in 0..iterations {
30350 for (probe, expected) in [
30351 (conflict_key.as_slice(), true),
30352 (miss_key.as_slice(), false),
30353 (malformed_key.as_slice(), false),
30354 (conflict_key.as_slice(), true),
30355 ] {
30356 let actual = legacy_storage_cursor_no_conflict_prefix_match(cursor, probe)
30357 .expect("legacy probe should succeed");
30358 assert_eq!(actual, expected);
30359 if actual {
30360 matches += 1;
30361 }
30362 }
30363 }
30364 (
30365 matches,
30366 fsqlite_types::record::record_profile_snapshot(),
30367 fsqlite_btree::btree_copy_profile_snapshot(),
30368 )
30369 };
30370
30371 let run_scratch = |cursor: &mut StorageCursor| {
30372 fsqlite_types::record::reset_record_profile();
30373 fsqlite_btree::reset_btree_copy_profile();
30374 fsqlite_btree::set_btree_copy_profile_enabled(true);
30375 let _scope = enter_record_profile_scope(RecordProfileScope::VdbeEngine);
30376 let mut matches = 0usize;
30377 let mut payload_capacity = None;
30378 let mut target_capacity = None;
30379 let mut current_capacity = None;
30380
30381 for _ in 0..iterations {
30382 for (boundary, probe, expected, expected_target) in [
30383 (
30384 "conflict",
30385 conflict_key.as_slice(),
30386 true,
30387 Some(conflict_target.as_slice()),
30388 ),
30389 (
30390 "miss",
30391 miss_key.as_slice(),
30392 false,
30393 Some(miss_target.as_slice()),
30394 ),
30395 ("malformed", malformed_key.as_slice(), false, None),
30396 (
30397 "recovery",
30398 conflict_key.as_slice(),
30399 true,
30400 Some(conflict_target.as_slice()),
30401 ),
30402 ] {
30403 let actual =
30404 run_async(storage_cursor_no_conflict_prefix_match(0, cursor, probe))
30405 .expect("scratch probe should succeed");
30406 assert_eq!(
30407 actual, expected,
30408 "{boundary} probe should preserve semantics"
30409 );
30410 if actual {
30411 matches += 1;
30412 }
30413
30414 match expected_target {
30415 Some(expected_target) => {
30416 assert_eq!(
30417 cursor.target_vals_buf.as_slice(),
30418 expected_target,
30419 "{boundary} probe should refresh target scratch without stale values"
30420 );
30421 }
30422 None => {
30423 assert!(
30424 cursor.target_vals_buf.is_empty(),
30425 "{boundary} probe should clear malformed target scratch"
30426 );
30427 }
30428 }
30429
30430 let observed_payload_capacity = cursor.payload_buf.capacity();
30431 if let Some(expected_capacity) = payload_capacity {
30432 assert_eq!(
30433 observed_payload_capacity, expected_capacity,
30434 "{boundary} probe should retain payload scratch capacity"
30435 );
30436 } else if observed_payload_capacity > 0 {
30437 payload_capacity = Some(observed_payload_capacity);
30438 }
30439
30440 let observed_target_capacity = cursor.target_vals_buf.capacity();
30441 if let Some(expected_capacity) = target_capacity {
30442 assert_eq!(
30443 observed_target_capacity, expected_capacity,
30444 "{boundary} probe should retain target scratch capacity"
30445 );
30446 } else if observed_target_capacity > 0 {
30447 target_capacity = Some(observed_target_capacity);
30448 }
30449
30450 let observed_current_capacity = cursor.cur_vals_buf.capacity();
30451 if let Some(expected_capacity) = current_capacity {
30452 assert_eq!(
30453 observed_current_capacity, expected_capacity,
30454 "{boundary} probe should retain current-entry scratch capacity"
30455 );
30456 } else if observed_current_capacity > 0 {
30457 current_capacity = Some(observed_current_capacity);
30458 }
30459 }
30460 }
30461
30462 (
30463 matches,
30464 fsqlite_types::record::record_profile_snapshot(),
30465 fsqlite_btree::btree_copy_profile_snapshot(),
30466 payload_capacity.unwrap_or_else(|| cursor.payload_buf.capacity()),
30467 target_capacity.unwrap_or_else(|| cursor.target_vals_buf.capacity()),
30468 current_capacity.unwrap_or_else(|| cursor.cur_vals_buf.capacity()),
30469 )
30470 };
30471
30472 let (legacy_matches, legacy_record_profile, legacy_copy_profile) = {
30473 let cursor = engine
30474 .storage_cursors
30475 .get_mut(&0)
30476 .expect("storage cursor should exist");
30477 run_legacy(cursor)
30478 };
30479 let (
30480 scratch_matches,
30481 scratch_record_profile,
30482 scratch_copy_profile,
30483 payload_capacity,
30484 target_capacity,
30485 current_capacity,
30486 ) = {
30487 let cursor = engine
30488 .storage_cursors
30489 .get_mut(&0)
30490 .expect("storage cursor should exist");
30491 run_scratch(cursor)
30492 };
30493
30494 assert_eq!(legacy_matches, iterations * 2);
30495 assert_eq!(scratch_matches, iterations * 2);
30496 assert_eq!(
30497 scratch_record_profile
30498 .callsite_breakdown
30499 .vdbe_engine
30500 .parse_record_calls,
30501 0,
30502 "mixed-boundary scratch path should avoid parse_record allocations entirely"
30503 );
30504 assert!(
30505 legacy_record_profile
30506 .callsite_breakdown
30507 .vdbe_engine
30508 .parse_record_calls
30509 > scratch_record_profile
30510 .callsite_breakdown
30511 .vdbe_engine
30512 .parse_record_calls,
30513 "legacy path should incur more parse_record churn under mixed boundaries"
30514 );
30515 assert_eq!(
30516 scratch_copy_profile.owned_payload_materialization_calls, 0,
30517 "scratch path should avoid owned payload materialization under mixed boundaries"
30518 );
30519 assert!(
30520 legacy_copy_profile.owned_payload_materialization_calls
30521 >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30522 "legacy path should materialize owned payloads for both found probes per iteration"
30523 );
30524 assert!(
30525 scratch_copy_profile.local_payload_copy_calls
30526 >= u64::try_from(iterations * 2).unwrap_or(u64::MAX),
30527 "scratch path should reuse caller-owned payload storage on found probes"
30528 );
30529 assert!(payload_capacity > 0);
30530 assert!(target_capacity > 0);
30531 assert!(current_capacity > 0);
30532
30533 eprintln!(
30534 concat!(
30535 "BD_DB300_4_3_3_RESET_CHURN_JSON=",
30536 "{{",
30537 "\"bead_id\":\"bd-db300.4.3.3\",",
30538 "\"hot_path\":\"storage_cursor_no_conflict_mixed_boundaries\",",
30539 "\"iterations\":{},",
30540 "\"boundary_sequence\":\"conflict,miss,malformed,recovery\",",
30541 "\"legacy\":{{",
30542 "\"parse_record_calls\":{},",
30543 "\"owned_payload_materialization_calls\":{}",
30544 "}},",
30545 "\"scratch\":{{",
30546 "\"parse_record_calls\":{},",
30547 "\"owned_payload_materialization_calls\":{},",
30548 "\"local_payload_copy_calls\":{},",
30549 "\"payload_capacity\":{},",
30550 "\"target_capacity\":{},",
30551 "\"current_capacity\":{}",
30552 "}}",
30553 "}}"
30554 ),
30555 iterations,
30556 legacy_record_profile
30557 .callsite_breakdown
30558 .vdbe_engine
30559 .parse_record_calls,
30560 legacy_copy_profile.owned_payload_materialization_calls,
30561 scratch_record_profile
30562 .callsite_breakdown
30563 .vdbe_engine
30564 .parse_record_calls,
30565 scratch_copy_profile.owned_payload_materialization_calls,
30566 scratch_copy_profile.local_payload_copy_calls,
30567 payload_capacity,
30568 target_capacity,
30569 current_capacity,
30570 );
30571
30572 fsqlite_btree::set_btree_copy_profile_enabled(false);
30573 }
30574
30575 #[test]
30576 fn test_no_conflict_opcode_uses_storage_cursor_scratch_probe_path() {
30577 let _guard = VDBE_OBSERVABILITY_LOCK
30578 .lock()
30579 .unwrap_or_else(|e| e.into_inner());
30580 let (mut engine, conflict_key, miss_key) =
30581 build_storage_index_engine_with_duplicate_prefixes();
30582 let program = build_no_conflict_opcode_probe_program(conflict_key, miss_key, vec![0x80]);
30583 let rows = execute_program_with_engine(&mut engine, &program);
30584
30585 assert_eq!(
30586 rows,
30587 vec![
30588 vec![SqliteValue::Integer(0)],
30589 vec![SqliteValue::Integer(1)],
30590 vec![SqliteValue::Integer(1)],
30591 ],
30592 "NoConflict should treat duplicate prefixes as conflicts and malformed probes as insertable"
30593 );
30594 }
30595
30596 #[test]
30597 fn test_no_conflict_opcode_honors_nocase_collation() {
30598 let _guard = VDBE_OBSERVABILITY_LOCK
30599 .lock()
30600 .unwrap_or_else(|e| e.into_inner());
30601 let (mut engine, conflict_key, miss_key) =
30602 build_storage_index_engine_with_nocase_prefixes();
30603 let program = build_no_conflict_opcode_probe_program(conflict_key, miss_key, vec![0x80]);
30604 let rows = execute_program_with_engine(&mut engine, &program);
30605
30606 assert_eq!(
30607 rows,
30608 vec![
30609 vec![SqliteValue::Integer(0)],
30610 vec![SqliteValue::Integer(1)],
30611 vec![SqliteValue::Integer(1)],
30612 ],
30613 "NoConflict must honor per-index NOCASE equality when probing storage indexes"
30614 );
30615 }
30616
30617 #[test]
30618 fn test_found_opcode_honors_nocase_collation() {
30619 let _guard = VDBE_OBSERVABILITY_LOCK
30620 .lock()
30621 .unwrap_or_else(|e| e.into_inner());
30622 let (mut engine, _, _) = build_storage_index_engine_with_nocase_prefixes();
30623 let conflict_key =
30624 encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(1)]);
30625 let miss_key =
30626 encode_record(&[SqliteValue::Text("gamma".into()), SqliteValue::Integer(99)]);
30627 let program = build_found_opcode_probe_program(conflict_key, miss_key);
30628 let rows = execute_program_with_engine(&mut engine, &program);
30629
30630 assert_eq!(
30631 rows,
30632 vec![vec![SqliteValue::Integer(1)], vec![SqliteValue::Integer(0)]],
30633 "Found must treat NOCASE-equivalent index keys as exact matches"
30634 );
30635 }
30636
30637 #[test]
30638 fn test_not_found_opcode_honors_nocase_collation() {
30639 let _guard = VDBE_OBSERVABILITY_LOCK
30640 .lock()
30641 .unwrap_or_else(|e| e.into_inner());
30642 let (mut engine, _, _) = build_storage_index_engine_with_nocase_prefixes();
30643 let conflict_key =
30644 encode_record(&[SqliteValue::Text("ALPHA".into()), SqliteValue::Integer(1)]);
30645 let miss_key =
30646 encode_record(&[SqliteValue::Text("gamma".into()), SqliteValue::Integer(99)]);
30647 let program = build_not_found_opcode_probe_program(conflict_key, miss_key);
30648 let rows = execute_program_with_engine(&mut engine, &program);
30649
30650 assert_eq!(
30651 rows,
30652 vec![vec![SqliteValue::Integer(0)], vec![SqliteValue::Integer(1)]],
30653 "NotFound must respect NOCASE equality when probing storage indexes"
30654 );
30655 }
30656
30657 #[test]
30658 fn test_seekge_honors_nocase_collation_for_full_index_keys() {
30659 let _guard = VDBE_OBSERVABILITY_LOCK
30660 .lock()
30661 .unwrap_or_else(|e| e.into_inner());
30662 let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30663
30664 let mut b = ProgramBuilder::new();
30665 let end = b.emit_label();
30666 let done = b.emit_label();
30667 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30668
30669 let r_text = b.alloc_reg();
30670 let r_rowid = b.alloc_reg();
30671 let r_seek_key = b.alloc_reg();
30672 let r_found_text = b.alloc_reg();
30673 let r_found_rowid = b.alloc_reg();
30674
30675 b.emit_op(
30676 Opcode::String8,
30677 0,
30678 r_text,
30679 0,
30680 P4::Str("ALPHA".to_owned()),
30681 0,
30682 );
30683 b.emit_op(Opcode::Integer, 1, r_rowid, 0, P4::None, 0);
30684 b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30685 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30686 b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30687 b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30688 b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30689
30690 b.resolve_label(done);
30691 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30692 b.resolve_label(end);
30693
30694 let program = b.finish().expect("program should build");
30695 let rows = execute_program_with_engine(&mut engine, &program);
30696
30697 assert_eq!(
30698 rows,
30699 vec![vec![
30700 SqliteValue::Text("alpha".into()),
30701 SqliteValue::Integer(1),
30702 ]],
30703 "SeekGE must land on the smallest collated key >= the probe, not the first raw-binary successor"
30704 );
30705 }
30706
30707 #[test]
30708 fn test_seekgt_honors_nocase_collation_for_full_index_keys() {
30709 let _guard = VDBE_OBSERVABILITY_LOCK
30710 .lock()
30711 .unwrap_or_else(|e| e.into_inner());
30712 let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30713
30714 let mut b = ProgramBuilder::new();
30715 let end = b.emit_label();
30716 let done = b.emit_label();
30717 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30718
30719 let r_text = b.alloc_reg();
30720 let r_rowid = b.alloc_reg();
30721 let r_seek_key = b.alloc_reg();
30722 let r_found_text = b.alloc_reg();
30723 let r_found_rowid = b.alloc_reg();
30724
30725 b.emit_op(
30726 Opcode::String8,
30727 0,
30728 r_text,
30729 0,
30730 P4::Str("alpha".to_owned()),
30731 0,
30732 );
30733 b.emit_op(Opcode::Integer, 1, r_rowid, 0, P4::None, 0);
30734 b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30735 b.emit_jump_to_label(Opcode::SeekGT, 0, r_seek_key, done, P4::None, 0);
30736 b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30737 b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30738 b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30739
30740 b.resolve_label(done);
30741 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30742 b.resolve_label(end);
30743
30744 let program = b.finish().expect("program should build");
30745 let rows = execute_program_with_engine(&mut engine, &program);
30746
30747 assert_eq!(
30748 rows,
30749 vec![vec![
30750 SqliteValue::Text("ALPHA".into()),
30751 SqliteValue::Integer(2),
30752 ]],
30753 "SeekGT must skip only collated-equal predecessors and land on the next greater key"
30754 );
30755 }
30756
30757 #[test]
30758 fn test_seeklt_honors_nocase_collation_for_full_index_keys() {
30759 let _guard = VDBE_OBSERVABILITY_LOCK
30760 .lock()
30761 .unwrap_or_else(|e| e.into_inner());
30762 let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30763
30764 let mut b = ProgramBuilder::new();
30765 let end = b.emit_label();
30766 let done = b.emit_label();
30767 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30768
30769 let r_text = b.alloc_reg();
30770 let r_rowid = b.alloc_reg();
30771 let r_seek_key = b.alloc_reg();
30772 let r_found_text = b.alloc_reg();
30773 let r_found_rowid = b.alloc_reg();
30774
30775 b.emit_op(
30776 Opcode::String8,
30777 0,
30778 r_text,
30779 0,
30780 P4::Str("ALPHA".to_owned()),
30781 0,
30782 );
30783 b.emit_op(Opcode::Integer, 2, r_rowid, 0, P4::None, 0);
30784 b.emit_op(Opcode::MakeRecord, r_text, 2, r_seek_key, P4::None, 0);
30785 b.emit_jump_to_label(Opcode::SeekLT, 0, r_seek_key, done, P4::None, 0);
30786 b.emit_op(Opcode::Column, 0, 0, r_found_text, P4::None, 0);
30787 b.emit_op(Opcode::IdxRowid, 0, r_found_rowid, 0, P4::None, 0);
30788 b.emit_op(Opcode::ResultRow, r_found_text, 2, 0, P4::None, 0);
30789
30790 b.resolve_label(done);
30791 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30792 b.resolve_label(end);
30793
30794 let program = b.finish().expect("program should build");
30795 let rows = execute_program_with_engine(&mut engine, &program);
30796
30797 assert_eq!(
30798 rows,
30799 vec![vec![
30800 SqliteValue::Text("alpha".into()),
30801 SqliteValue::Integer(1),
30802 ]],
30803 "SeekLT must return the greatest collated key strictly below the probe"
30804 );
30805 }
30806
30807 #[test]
30808 fn test_idxinsert_unique_no_conflict_inserts_distinct_prefix_after_cursor_disturbance() {
30809 let _guard = VDBE_OBSERVABILITY_LOCK
30810 .lock()
30811 .unwrap_or_else(|e| e.into_inner());
30812 let (mut engine, _, _) = build_storage_index_engine_with_duplicate_prefixes();
30813
30814 let disturb_key = encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(1)]);
30815 let inserted_key = encode_record(&[SqliteValue::Integer(9), SqliteValue::Integer(500)]);
30816
30817 let mut b = ProgramBuilder::new();
30818 let end = b.emit_label();
30819 let disturbed = b.emit_label();
30820 let found = b.emit_label();
30821 let done = b.emit_label();
30822 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30823
30824 let r_disturb_key = b.alloc_reg();
30825 let r_insert_key = b.alloc_reg();
30826 let r_probe_key = b.alloc_reg();
30827 let r_out = b.alloc_reg();
30828
30829 b.emit_op(Opcode::Blob, 0, r_disturb_key, 0, P4::Blob(disturb_key), 0);
30830 b.emit_jump_to_label(Opcode::Found, 0, r_disturb_key, disturbed, P4::None, 0);
30831 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
30832 b.resolve_label(disturbed);
30833
30834 b.emit_op(
30835 Opcode::Blob,
30836 0,
30837 r_insert_key,
30838 0,
30839 P4::Blob(inserted_key.clone()),
30840 0,
30841 );
30842 b.emit_op(
30843 Opcode::IdxInsert,
30844 0,
30845 r_insert_key,
30846 1,
30847 P4::Table("idx_col".to_owned()),
30848 1,
30849 );
30850
30851 b.emit_op(Opcode::Blob, 0, r_probe_key, 0, P4::Blob(inserted_key), 0);
30852 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
30853 b.emit_jump_to_label(Opcode::Found, 0, r_probe_key, found, P4::None, 0);
30854 b.emit_jump_to_label(Opcode::Goto, 0, 0, done, P4::None, 0);
30855 b.resolve_label(found);
30856 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
30857 b.resolve_label(done);
30858
30859 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
30860 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30861 b.resolve_label(end);
30862
30863 let program = b.finish().expect("program should build");
30864 let rows = execute_program_with_engine(&mut engine, &program);
30865
30866 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
30867 }
30868
30869 #[test]
30870 fn test_count_index_eq_run_consumes_duplicate_run_and_leaves_cursor_on_next_key() {
30871 let mut engine = VdbeEngine::new(8);
30872 install_duplicate_prefix_index_fixture(&mut engine, 0);
30873
30874 let mut b = ProgramBuilder::new();
30875 let end = b.emit_label();
30876 let done = b.emit_label();
30877 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30878
30879 let r_probe = b.alloc_reg();
30880 let r_min_rowid = b.alloc_reg();
30881 let r_seek_key = b.alloc_reg();
30882 let r_count = b.alloc_reg();
30883 let r_next_key = b.alloc_reg();
30884
30885 b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
30886 b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30887 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30888 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30889 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30890 b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30891 b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
30892 b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
30893 b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
30894
30895 b.resolve_label(done);
30896 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30897 b.resolve_label(end);
30898
30899 let program = b.finish().expect("program should build");
30900 let rows = execute_program_with_engine(&mut engine, &program);
30901
30902 assert_eq!(
30903 rows,
30904 vec![vec![SqliteValue::Integer(128), SqliteValue::Integer(8)]],
30905 "CountIndexEqRun should count the full duplicate run and leave the cursor on the next key"
30906 );
30907 }
30908
30909 #[test]
30910 fn test_count_index_eq_run_uses_cursor_row_decode_scratch() {
30911 let mut engine = VdbeEngine::new(8);
30912 install_duplicate_prefix_index_fixture(&mut engine, 0);
30913
30914 let mut b = ProgramBuilder::new();
30915 let end = b.emit_label();
30916 let done = b.emit_label();
30917 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30918
30919 let r_probe = b.alloc_reg();
30920 let r_min_rowid = b.alloc_reg();
30921 let r_seek_key = b.alloc_reg();
30922 let r_count = b.alloc_reg();
30923 let r_current_key = b.alloc_reg();
30924
30925 b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
30926 b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30927 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30928 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30929 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30930 b.emit_op(Opcode::Column, 0, 0, r_current_key, P4::None, 0);
30931 b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30932
30933 b.resolve_label(done);
30934 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30935 b.resolve_label(end);
30936
30937 let program = b.finish().expect("program should build");
30938 let _ = execute_program_with_engine(&mut engine, &program);
30939
30940 let cursor = engine
30941 .storage_cursors
30942 .get(&0)
30943 .expect("cursor should still exist");
30944
30945 assert!(
30946 cursor.cur_vals_buf.is_empty(),
30947 "CountIndexEqRun should not populate the legacy full-record decode scratch"
30948 );
30949 assert!(
30950 cursor.row_decode.cached_value_ready(0),
30951 "CountIndexEqRun should leave the first index key cached in row-decode scratch"
30952 );
30953 }
30954
30955 #[test]
30956 fn test_count_index_eq_run_honors_nocase_collation() {
30957 let mut engine = build_storage_index_engine_with_nocase_mixed_case_ordering();
30958
30959 let mut b = ProgramBuilder::new();
30960 let end = b.emit_label();
30961 let done = b.emit_label();
30962 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
30963
30964 let r_probe = b.alloc_reg();
30965 let r_min_rowid = b.alloc_reg();
30966 let r_seek_key = b.alloc_reg();
30967 let r_count = b.alloc_reg();
30968 let r_next_key = b.alloc_reg();
30969
30970 b.emit_op(
30971 Opcode::String8,
30972 0,
30973 r_probe,
30974 0,
30975 P4::Str("ALPHA".to_owned()),
30976 0,
30977 );
30978 b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
30979 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
30980 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
30981 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
30982 b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
30983 b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
30984 b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
30985 b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
30986
30987 b.resolve_label(done);
30988 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
30989 b.resolve_label(end);
30990
30991 let program = b.finish().expect("program should build");
30992 let rows = execute_program_with_engine(&mut engine, &program);
30993
30994 assert_eq!(
30995 rows,
30996 vec![vec![
30997 SqliteValue::Integer(2),
30998 SqliteValue::Text("beta".into())
30999 ]],
31000 "CountIndexEqRun must consume a full NOCASE-equivalent run and stop on the next distinct key"
31001 );
31002 }
31003
31004 #[test]
31005 fn test_count_index_eq_run_integer_fast_lane_falls_back_for_float_keys() {
31006 let mut engine = VdbeEngine::new(8);
31007 let mut db = MemDatabase::new();
31008 let index_root = db.allocate_root_page();
31009
31010 engine.enable_storage_cursors(true);
31011 engine.set_database(db);
31012 engine.set_reject_mem_fallback(false);
31013
31014 assert!(
31015 run_async(engine.open_storage_cursor(0, index_root, true)),
31016 "index storage cursor should open"
31017 );
31018
31019 let cursor = engine
31020 .storage_cursors
31021 .get_mut(&0)
31022 .expect("storage cursor should exist");
31023 let key = encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(1)]);
31024 run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31025 .expect("float-key index entry should insert");
31026 assert!(run_async(cursor.cursor.first(&cursor.cx)).expect("cursor first should succeed"));
31027
31028 let matches = run_async(storage_cursor_current_first_index_key_equals(
31029 cursor,
31030 &SqliteValue::Integer(7),
31031 false,
31032 "CountIndexEqRun: malformed index entry record",
31033 ))
31034 .expect("float-key comparison should succeed");
31035
31036 assert!(
31037 matches,
31038 "integer probe should still match a numerically equal float key via generic fallback"
31039 );
31040 assert!(
31041 cursor.row_decode.cached_value_ready(0),
31042 "generic fallback should still decode and cache the float key"
31043 );
31044 }
31045
31046 #[test]
31047 fn test_count_index_eq_run_integer_probe_preserves_float_fallback_through_run_loop() {
31048 let mut engine = VdbeEngine::new(8);
31049 let mut db = MemDatabase::new();
31050 let index_root = db.allocate_root_page();
31051
31052 engine.enable_storage_cursors(true);
31053 engine.set_database(db);
31054 engine.set_reject_mem_fallback(false);
31055
31056 assert!(
31057 run_async(engine.open_storage_cursor(0, index_root, true)),
31058 "index storage cursor should open"
31059 );
31060
31061 let cursor = engine
31062 .storage_cursors
31063 .get_mut(&0)
31064 .expect("storage cursor should exist");
31065 for key in [
31066 encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(1)]),
31067 encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(2)]),
31068 encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(3)]),
31069 ] {
31070 run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31071 .expect("index entry should insert");
31072 }
31073
31074 let mut b = ProgramBuilder::new();
31075 let end = b.emit_label();
31076 let done = b.emit_label();
31077 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31078
31079 let r_probe = b.alloc_reg();
31080 let r_min_rowid = b.alloc_reg();
31081 let r_seek_key = b.alloc_reg();
31082 let r_count = b.alloc_reg();
31083 let r_next_key = b.alloc_reg();
31084
31085 b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
31086 b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
31087 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31088 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31089 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
31090 b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
31091 b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
31092 b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
31093 b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
31094
31095 b.resolve_label(done);
31096 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31097 b.resolve_label(end);
31098
31099 let program = b.finish().expect("program should build");
31100 let rows = execute_program_with_engine(&mut engine, &program);
31101
31102 assert_eq!(
31103 rows,
31104 vec![vec![SqliteValue::Integer(2), SqliteValue::Integer(8)]],
31105 "integer-probe CountIndexEqRun should still count numerically equal float keys before stopping on the next distinct key"
31106 );
31107 }
31108
31109 #[test]
31110 fn test_count_index_eq_run_local_integer_segment_preserves_float_fallback() {
31111 let mut engine = VdbeEngine::new(8);
31112 let mut db = MemDatabase::new();
31113 let index_root = db.allocate_root_page();
31114
31115 engine.enable_storage_cursors(true);
31116 engine.set_database(db);
31117 engine.set_reject_mem_fallback(false);
31118
31119 assert!(
31120 run_async(engine.open_storage_cursor(0, index_root, true)),
31121 "index storage cursor should open"
31122 );
31123
31124 let cursor = engine
31125 .storage_cursors
31126 .get_mut(&0)
31127 .expect("storage cursor should exist");
31128 for key in [
31129 encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(1)]),
31130 encode_record(&[SqliteValue::Integer(7), SqliteValue::Integer(2)]),
31131 encode_record(&[SqliteValue::Float(7.0), SqliteValue::Integer(3)]),
31132 encode_record(&[SqliteValue::Integer(8), SqliteValue::Integer(4)]),
31133 ] {
31134 run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31135 .expect("index entry should insert");
31136 }
31137
31138 let mut b = ProgramBuilder::new();
31139 let end = b.emit_label();
31140 let done = b.emit_label();
31141 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31142
31143 let r_probe = b.alloc_reg();
31144 let r_min_rowid = b.alloc_reg();
31145 let r_seek_key = b.alloc_reg();
31146 let r_count = b.alloc_reg();
31147 let r_next_key = b.alloc_reg();
31148
31149 b.emit_op(Opcode::Integer, 7, r_probe, 0, P4::None, 0);
31150 b.emit_op(Opcode::Int64, 0, r_min_rowid, 0, P4::Int64(i64::MIN), 0);
31151 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31152 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31153 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
31154 b.emit_op(Opcode::CountIndexEqRun, 0, r_count, r_probe, P4::None, 0);
31155 b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, done, P4::None, 0);
31156 b.emit_op(Opcode::Column, 0, 0, r_next_key, P4::None, 0);
31157 b.emit_op(Opcode::ResultRow, r_count, 2, 0, P4::None, 0);
31158
31159 b.resolve_label(done);
31160 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31161 b.resolve_label(end);
31162
31163 let program = b.finish().expect("program should build");
31164 let rows = execute_program_with_engine(&mut engine, &program);
31165
31166 assert_eq!(
31167 rows,
31168 vec![vec![SqliteValue::Integer(3), SqliteValue::Integer(8)]],
31169 "CountIndexEqRun should preserve the float generic fallback after consuming a local integer segment"
31170 );
31171 }
31172
31173 #[test]
31174 fn test_like_const_fast_handles_match_not_and_null() {
31175 let rows = run_program(|b| {
31176 let end = b.emit_label();
31177 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31178
31179 let r_input = b.alloc_reg();
31180 let r_result = b.alloc_reg();
31181
31182 b.emit_op(
31183 Opcode::String8,
31184 0,
31185 r_input,
31186 0,
31187 P4::Str("Document 123".to_owned()),
31188 0,
31189 );
31190 b.emit_op(
31191 Opcode::LikeConstFast,
31192 r_input,
31193 r_result,
31194 SqlLikeFastPathKind::Prefix.opcode_tag(),
31195 P4::Str("Document 1".to_owned()),
31196 0,
31197 );
31198 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31199
31200 b.emit_op(Opcode::String8, 0, r_input, 0, P4::Str("zzz".to_owned()), 0);
31201 b.emit_op(
31202 Opcode::LikeConstFast,
31203 r_input,
31204 r_result,
31205 SqlLikeFastPathKind::Prefix.opcode_tag(),
31206 P4::Str("Document 1".to_owned()),
31207 1,
31208 );
31209 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31210
31211 b.emit_op(Opcode::Null, 0, r_input, 0, P4::None, 0);
31212 b.emit_op(
31213 Opcode::LikeConstFast,
31214 r_input,
31215 r_result,
31216 SqlLikeFastPathKind::Prefix.opcode_tag(),
31217 P4::Str("Document 1".to_owned()),
31218 0,
31219 );
31220 b.emit_op(Opcode::ResultRow, r_result, 1, 0, P4::None, 0);
31221
31222 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31223 b.resolve_label(end);
31224 });
31225
31226 assert_eq!(
31227 rows,
31228 vec![
31229 vec![SqliteValue::Integer(1)],
31230 vec![SqliteValue::Integer(1)],
31231 vec![SqliteValue::Null],
31232 ],
31233 "LikeConstFast should preserve LIKE match, NOT LIKE inversion, and NULL propagation"
31234 );
31235 }
31236
31237 #[test]
31238 fn test_no_conflict_opcode_reset_for_reuse_reseeds_clean_scratch_state() {
31239 let _guard = VDBE_OBSERVABILITY_LOCK
31240 .lock()
31241 .unwrap_or_else(|e| e.into_inner());
31242 let conflict_key = encode_record(&[SqliteValue::Integer(7)]);
31243 let miss_key = encode_record(&[SqliteValue::Integer(9)]);
31244 let program = build_no_conflict_opcode_probe_program(
31245 conflict_key.clone(),
31246 miss_key.clone(),
31247 vec![0x80],
31248 );
31249 let expected_rows = vec![
31250 vec![SqliteValue::Integer(0)],
31251 vec![SqliteValue::Integer(1)],
31252 vec![SqliteValue::Integer(1)],
31253 ];
31254
31255 let mut engine = VdbeEngine::new(program.register_count());
31256 let (installed_conflict_key, installed_miss_key) =
31257 install_duplicate_prefix_index_fixture(&mut engine, 0);
31258 assert_eq!(installed_conflict_key, conflict_key);
31259 assert_eq!(installed_miss_key, miss_key);
31260
31261 let first_rows = execute_program_with_engine(&mut engine, &program);
31262 assert_eq!(first_rows, expected_rows);
31263
31264 let reset_cx = Cx::new();
31265 engine.reset_for_reuse(program.register_count(), &reset_cx, PageSize::DEFAULT);
31266 assert!(engine.storage_cursors.is_empty());
31267 assert!(engine.results().is_empty());
31268
31269 let (reinstalled_conflict_key, reinstalled_miss_key) =
31270 install_duplicate_prefix_index_fixture(&mut engine, 0);
31271 assert_eq!(reinstalled_conflict_key, conflict_key);
31272 assert_eq!(reinstalled_miss_key, miss_key);
31273 let cursor = engine
31274 .storage_cursors
31275 .get(&0)
31276 .expect("storage cursor should be reinstalled");
31277 assert!(cursor.payload_buf.is_empty());
31278 assert!(cursor.target_vals_buf.is_empty());
31279 assert!(cursor.cur_vals_buf.is_empty());
31280
31281 let second_rows = execute_program_with_engine(&mut engine, &program);
31282 assert_eq!(second_rows, expected_rows);
31283 }
31284
31285 #[test]
31286 fn test_compare_index_prefix_keys_honors_desc_flags() {
31287 let registry = Mutex::new(CollationRegistry::new());
31288 let lhs = vec![SqliteValue::Integer(20), SqliteValue::Integer(2)];
31289 let rhs = vec![SqliteValue::Integer(10), SqliteValue::Integer(1)];
31290 let coll_guard = registry.lock().unwrap();
31291
31292 assert_eq!(
31293 compare_index_prefix_keys(&lhs, &rhs, 1, &[true], &[], &coll_guard),
31294 Ordering::Less,
31295 "descending index comparison should treat the larger key as earlier"
31296 );
31297 assert_eq!(
31298 compare_index_prefix_keys(&lhs, &rhs, 1, &[false], &[], &coll_guard),
31299 Ordering::Greater,
31300 "ascending index comparison should keep natural integer ordering"
31301 );
31302 assert_eq!(
31303 compare_index_prefix_keys(&lhs, &rhs, 0, &[true], &[], &coll_guard),
31304 Ordering::Less,
31305 "zero key_columns should still compare the leading term"
31306 );
31307 }
31308
31309 #[test]
31310 fn test_storage_cursor_first_index_key_compare_honors_desc_flag() {
31311 let mut engine = VdbeEngine::new(8);
31312 let mut db = MemDatabase::new();
31313 let index_root = db.allocate_root_page();
31314
31315 engine.enable_storage_cursors(true);
31316 engine.set_database(db);
31317 engine.set_reject_mem_fallback(false);
31318 engine.set_index_desc_flags_by_root_page(HashMap::from([(index_root, vec![true])]));
31319
31320 assert!(
31321 run_async(engine.open_storage_cursor(0, index_root, true)),
31322 "descending index storage cursor should open"
31323 );
31324
31325 let cursor = engine
31326 .storage_cursors
31327 .get_mut(&0)
31328 .expect("storage cursor should exist");
31329 for key in [
31330 encode_record(&[SqliteValue::Integer(10), SqliteValue::Integer(1)]),
31331 encode_record(&[SqliteValue::Integer(20), SqliteValue::Integer(2)]),
31332 ] {
31333 run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31334 .expect("descending index key should insert");
31335 }
31336 assert!(
31337 run_async(cursor.cursor.first(&cursor.cx)).expect("first should work"),
31338 "descending index should contain entries"
31339 );
31340
31341 let registry = Arc::clone(&engine.collation_registry);
31342 let coll_guard = registry.lock().unwrap_or_else(|err| err.into_inner());
31343 let cmp = run_async(storage_cursor_current_first_index_key_compare(
31344 cursor,
31345 &SqliteValue::Integer(10),
31346 false,
31347 "IdxCmp: malformed index record at cursor position",
31348 true,
31349 None,
31350 &coll_guard,
31351 ))
31352 .expect("first-key compare should succeed");
31353
31354 assert_eq!(
31355 cmp,
31356 Ordering::Less,
31357 "larger integer keys sort before smaller keys in a DESC index"
31358 );
31359 }
31360
31361 #[test]
31362 fn test_idxgt_single_column_prefix_ignores_trailing_rowid() {
31363 let mut engine = VdbeEngine::new(8);
31364 let mut db = MemDatabase::new();
31365 let index_root = db.allocate_root_page();
31366
31367 engine.enable_storage_cursors(true);
31368 engine.set_database(db);
31369 engine.set_reject_mem_fallback(false);
31370
31371 assert!(
31372 run_async(engine.open_storage_cursor(0, index_root, true)),
31373 "index storage cursor should open"
31374 );
31375
31376 let cursor = engine
31377 .storage_cursors
31378 .get_mut(&0)
31379 .expect("storage cursor should exist");
31380 for key in [
31381 encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(1)]),
31382 encode_record(&[SqliteValue::Text("alpha".into()), SqliteValue::Integer(2)]),
31383 encode_record(&[SqliteValue::Text("beta".into()), SqliteValue::Integer(3)]),
31384 ] {
31385 run_async(cursor.cursor.index_insert(&cursor.cx, &key))
31386 .expect("index key should insert");
31387 }
31388
31389 let mut b = ProgramBuilder::new();
31390 let done = b.emit_label();
31391 let end = b.emit_label();
31392 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31393
31394 let r_probe = b.alloc_reg();
31395 let r_low_rowid = b.alloc_reg();
31396 let r_seek_key = b.alloc_reg();
31397 let r_out = b.alloc_reg();
31398
31399 b.emit_op(
31400 Opcode::String8,
31401 0,
31402 r_probe,
31403 0,
31404 P4::Str("alpha".to_owned()),
31405 0,
31406 );
31407 b.emit_op(Opcode::Int64, 0, r_low_rowid, 0, P4::Int64(i64::MIN), 0);
31408 b.emit_op(Opcode::MakeRecord, r_probe, 2, r_seek_key, P4::None, 0);
31409 b.emit_jump_to_label(Opcode::SeekGE, 0, r_seek_key, done, P4::None, 0);
31410 b.emit_jump_to_label(Opcode::IdxGT, 0, r_seek_key, done, P4::None, 1);
31411 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
31412 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
31413
31414 b.resolve_label(done);
31415 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31416 b.resolve_label(end);
31417
31418 let program = b.finish().expect("program should build");
31419 let rows = execute_program_with_engine(&mut engine, &program);
31420
31421 assert_eq!(
31422 rows,
31423 vec![vec![SqliteValue::Integer(1)]],
31424 "IdxGT with p5=1 must compare only the equality prefix, not the rowid suffix"
31425 );
31426 }
31427
31428 #[test]
31429 fn test_distinct_key_collated_honors_rtrim() {
31430 let base = distinct_key_collated(&[SqliteValue::Text("abc".into())], Some("RTRIM"));
31431 let padded = distinct_key_collated(&[SqliteValue::Text("abc ".into())], Some("rtrim"));
31432 let tabbed = distinct_key_collated(&[SqliteValue::Text("abc\t".into())], Some("RTRIM"));
31433
31434 assert_eq!(
31435 base, padded,
31436 "RTRIM DISTINCT keys must ignore trailing ASCII spaces"
31437 );
31438 assert_ne!(
31439 base, tabbed,
31440 "RTRIM DISTINCT keys must not trim non-space suffixes like tabs"
31441 );
31442 }
31443
31444 #[test]
31445 fn test_open_storage_cursor_write_init_failure_does_not_fallback_to_mem() {
31446 use fsqlite_mvcc::{ConcurrentRegistry, InProcessPageLockTable};
31447 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31448
31449 let pager = MockMvccPager;
31450 let cx = Cx::new();
31451 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31452
31453 let mut engine = VdbeEngine::new(8);
31454 let root = 256;
31457
31458 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
31461 let lock_table = Arc::new(InProcessPageLockTable::new());
31462 let commit_index = Arc::new(CommitIndex::new());
31463 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
31464 let (session_id, handle) = {
31465 let mut guard = registry
31466 .lock()
31467 .unwrap_or_else(std::sync::PoisonError::into_inner);
31468 let session_id = guard
31469 .begin_concurrent(snapshot)
31470 .expect("session should register");
31471 let handle = guard.handle(session_id).expect("handle should exist");
31472 (session_id, handle)
31473 };
31474 handle.lock().mark_aborted();
31475 engine.set_transaction_concurrent(txn, session_id, handle, lock_table, commit_index, 5000);
31476
31477 let opened = run_async(engine.open_storage_cursor(0, root, true));
31478 assert!(
31479 !opened,
31480 "write-init errors must fail cursor open instead of silently falling back to Mem"
31481 );
31482 assert!(
31483 !engine.storage_cursors.contains_key(&0),
31484 "failed open must not leave a cursor installed"
31485 );
31486 }
31487
31488 #[test]
31489 fn test_open_storage_cursor_write_read_failure_does_not_fallback_to_mem() {
31490 use std::path::PathBuf;
31491
31492 use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
31493 use fsqlite_vfs::MemoryVfs;
31494
31495 let vfs = MemoryVfs::new();
31496 let path = PathBuf::from("/vdbe_write_read_failure_no_mem_fallback.db");
31497 let cx = Cx::new();
31498 let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
31499 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31500
31501 let mut db = MemDatabase::new();
31502 let root = 2;
31503 db.create_table_at(root, 1);
31504
31505 let mut engine = VdbeEngine::new(8);
31506 engine.set_database(db);
31507 engine.set_transaction(txn);
31508 engine.set_reject_mem_fallback(false);
31509
31510 let opened = run_async(engine.open_storage_cursor(0, root, true));
31511 assert!(
31512 !opened,
31513 "writable cursor opens must fail when pager reads error instead of falling back to Mem"
31514 );
31515 assert!(
31516 !engine.storage_cursors.contains_key(&0),
31517 "failed open must not leave a cursor installed"
31518 );
31519 }
31520
31521 #[test]
31522 fn test_open_storage_cursor_falls_back_to_mem_without_txn() {
31523 let mut db = MemDatabase::new();
31524 let root = db.create_table(1);
31525 db.get_table_mut(root)
31526 .unwrap()
31527 .insert(1, vec![SqliteValue::Integer(100)]);
31528
31529 let mut engine = VdbeEngine::new(8);
31530 engine.enable_storage_cursors(true);
31531 engine.set_database(db);
31532 engine.set_reject_mem_fallback(false);
31533
31534 let opened = run_async(engine.open_storage_cursor(0, root, false));
31536 assert!(opened);
31537 assert!(engine.storage_cursors.contains_key(&0));
31538 }
31539
31540 #[test]
31541 fn test_open_storage_cursor_zero_page_with_txn_does_not_fallback_to_mem() {
31542 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31543
31544 let pager = MockMvccPager;
31545 let cx = Cx::new();
31546 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31547
31548 let mut engine = VdbeEngine::new(8);
31549 engine.set_transaction(txn);
31550
31551 let opened = run_async(engine.open_storage_cursor(0, 256, false));
31554 assert!(
31555 !opened,
31556 "transaction-backed opens must not silently fall back to MemPageStore"
31557 );
31558 assert!(
31559 !engine.storage_cursors.contains_key(&0),
31560 "failed open must not leave a cursor installed"
31561 );
31562 }
31563
31564 #[test]
31565 fn test_open_storage_cursor_write_invalid_page_does_not_fallback_to_mem() {
31566 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31567
31568 let pager = MockMvccPager;
31569 let cx = Cx::new();
31570 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31571
31572 let mut db = MemDatabase::new();
31573 let root = 256;
31574 db.create_table_at(root, 1);
31575
31576 let mut engine = VdbeEngine::new(8);
31577 engine.set_database(db);
31578 engine.set_transaction(txn);
31579 engine.set_reject_mem_fallback(false);
31580
31581 let opened = run_async(engine.open_storage_cursor(0, root, true));
31582 assert!(
31583 !opened,
31584 "writable cursor opens must fail on invalid pager pages instead of falling back to Mem"
31585 );
31586 assert!(
31587 !engine.storage_cursors.contains_key(&0),
31588 "failed open must not leave a cursor installed"
31589 );
31590 }
31591
31592 #[test]
31593 fn test_txn_cursor_open_close_lifecycle() {
31594 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
31599
31600 let pager = MockMvccPager;
31601 let cx = Cx::new();
31602 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
31603
31604 let mut db = MemDatabase::new();
31605 let root = db.create_table(1);
31606
31607 let mut b = ProgramBuilder::new();
31608 let end = b.emit_label();
31609 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31610 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31612 b.emit_op(Opcode::Close, 0, 0, 0, P4::None, 0);
31614 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31615 b.resolve_label(end);
31616 let prog = b.finish().expect("program should build");
31617
31618 let mut engine = VdbeEngine::new(prog.register_count());
31619 engine.set_database(db);
31620 engine.set_transaction(txn);
31621
31622 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
31623 assert_eq!(outcome, ExecOutcome::Done);
31624
31625 engine.storage_cursors.clear();
31627 assert!(
31628 engine
31629 .take_transaction()
31630 .expect("take_transaction should succeed")
31631 .is_some()
31632 );
31633 }
31634
31635 #[test]
31638 fn test_seek_ge_with_storage_cursor() {
31639 let mut db = MemDatabase::new();
31642 let root = db.create_table(1);
31643 let table = db.get_table_mut(root).unwrap();
31644 table.insert(3, vec![SqliteValue::Integer(30)]);
31645 table.insert(5, vec![SqliteValue::Integer(50)]);
31646 table.insert(7, vec![SqliteValue::Integer(70)]);
31647 table.insert(9, vec![SqliteValue::Integer(90)]);
31648
31649 let rows = run_with_storage_cursors(db, |b| {
31650 let end = b.emit_label();
31651 let not_found = b.emit_label();
31652 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31653 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31654
31655 b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31657 b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31658
31659 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31661 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31662
31663 b.resolve_label(not_found);
31664 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31665 b.resolve_label(end);
31666 });
31667
31668 assert_eq!(rows.len(), 1);
31669 assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); }
31671
31672 #[test]
31673 fn test_seek_ge_not_exact_match() {
31674 let mut db = MemDatabase::new();
31677 let root = db.create_table(1);
31678 let table = db.get_table_mut(root).unwrap();
31679 table.insert(3, vec![SqliteValue::Integer(30)]);
31680 table.insert(5, vec![SqliteValue::Integer(50)]);
31681 table.insert(7, vec![SqliteValue::Integer(70)]);
31682 table.insert(9, vec![SqliteValue::Integer(90)]);
31683
31684 let rows = run_with_storage_cursors(db, |b| {
31685 let end = b.emit_label();
31686 let not_found = b.emit_label();
31687 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31688 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31689
31690 b.emit_op(Opcode::Integer, 4, 1, 0, P4::None, 0);
31692 b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31693
31694 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31695 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31696
31697 b.resolve_label(not_found);
31698 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31699 b.resolve_label(end);
31700 });
31701
31702 assert_eq!(rows.len(), 1);
31703 assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); }
31705
31706 #[test]
31707 fn test_seek_gt_with_storage_cursor() {
31708 let mut db = MemDatabase::new();
31711 let root = db.create_table(1);
31712 let table = db.get_table_mut(root).unwrap();
31713 table.insert(3, vec![SqliteValue::Integer(30)]);
31714 table.insert(5, vec![SqliteValue::Integer(50)]);
31715 table.insert(7, vec![SqliteValue::Integer(70)]);
31716 table.insert(9, vec![SqliteValue::Integer(90)]);
31717
31718 let rows = run_with_storage_cursors(db, |b| {
31719 let end = b.emit_label();
31720 let not_found = b.emit_label();
31721 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31722 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31723
31724 b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31726 b.emit_jump_to_label(Opcode::SeekGT, 0, 1, not_found, P4::None, 0);
31727
31728 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31729 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31730
31731 b.resolve_label(not_found);
31732 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31733 b.resolve_label(end);
31734 });
31735
31736 assert_eq!(rows.len(), 1);
31737 assert_eq!(rows[0], vec![SqliteValue::Integer(70)]); }
31739
31740 #[test]
31741 fn test_seek_le_with_storage_cursor() {
31742 let mut db = MemDatabase::new();
31745 let root = db.create_table(1);
31746 let table = db.get_table_mut(root).unwrap();
31747 table.insert(3, vec![SqliteValue::Integer(30)]);
31748 table.insert(5, vec![SqliteValue::Integer(50)]);
31749 table.insert(7, vec![SqliteValue::Integer(70)]);
31750 table.insert(9, vec![SqliteValue::Integer(90)]);
31751
31752 let rows = run_with_storage_cursors(db, |b| {
31753 let end = b.emit_label();
31754 let not_found = b.emit_label();
31755 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31756 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31757
31758 b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31760 b.emit_jump_to_label(Opcode::SeekLE, 0, 1, not_found, P4::None, 0);
31761
31762 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31763 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31764
31765 b.resolve_label(not_found);
31766 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31767 b.resolve_label(end);
31768 });
31769
31770 assert_eq!(rows.len(), 1);
31771 assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); }
31773
31774 #[test]
31775 fn test_seek_le_not_exact_match() {
31776 let mut db = MemDatabase::new();
31779 let root = db.create_table(1);
31780 let table = db.get_table_mut(root).unwrap();
31781 table.insert(3, vec![SqliteValue::Integer(30)]);
31782 table.insert(5, vec![SqliteValue::Integer(50)]);
31783 table.insert(7, vec![SqliteValue::Integer(70)]);
31784 table.insert(9, vec![SqliteValue::Integer(90)]);
31785
31786 let rows = run_with_storage_cursors(db, |b| {
31787 let end = b.emit_label();
31788 let not_found = b.emit_label();
31789 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31790 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31791
31792 b.emit_op(Opcode::Integer, 6, 1, 0, P4::None, 0);
31794 b.emit_jump_to_label(Opcode::SeekLE, 0, 1, not_found, P4::None, 0);
31795
31796 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31797 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31798
31799 b.resolve_label(not_found);
31800 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31801 b.resolve_label(end);
31802 });
31803
31804 assert_eq!(rows.len(), 1);
31805 assert_eq!(rows[0], vec![SqliteValue::Integer(50)]); }
31807
31808 #[test]
31809 fn test_seek_lt_with_storage_cursor() {
31810 let mut db = MemDatabase::new();
31813 let root = db.create_table(1);
31814 let table = db.get_table_mut(root).unwrap();
31815 table.insert(3, vec![SqliteValue::Integer(30)]);
31816 table.insert(5, vec![SqliteValue::Integer(50)]);
31817 table.insert(7, vec![SqliteValue::Integer(70)]);
31818 table.insert(9, vec![SqliteValue::Integer(90)]);
31819
31820 let rows = run_with_storage_cursors(db, |b| {
31821 let end = b.emit_label();
31822 let not_found = b.emit_label();
31823 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31824 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31825
31826 b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31828 b.emit_jump_to_label(Opcode::SeekLT, 0, 1, not_found, P4::None, 0);
31829
31830 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
31831 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31832
31833 b.resolve_label(not_found);
31834 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31835 b.resolve_label(end);
31836 });
31837
31838 assert_eq!(rows.len(), 1);
31839 assert_eq!(rows[0], vec![SqliteValue::Integer(30)]); }
31841
31842 #[test]
31843 fn test_seek_ge_empty_table_jumps() {
31844 let mut db = MemDatabase::new();
31846 let root = db.create_table(1);
31847 let rows = run_with_storage_cursors(db, |b| {
31850 let end = b.emit_label();
31851 let not_found = b.emit_label();
31852 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31853 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31854
31855 b.emit_op(Opcode::Integer, 5, 1, 0, P4::None, 0);
31856 b.emit_jump_to_label(Opcode::SeekGE, 0, 1, not_found, P4::None, 0);
31857
31858 b.emit_op(Opcode::Integer, 999, 2, 0, P4::None, 0);
31860 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31861
31862 b.resolve_label(not_found);
31863 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31865 b.resolve_label(end);
31866 });
31867
31868 assert_eq!(rows.len(), 0);
31870 }
31871
31872 #[test]
31873 fn test_seek_lt_no_smaller_row_jumps() {
31874 let mut db = MemDatabase::new();
31876 let root = db.create_table(1);
31877 let table = db.get_table_mut(root).unwrap();
31878 table.insert(3, vec![SqliteValue::Integer(30)]);
31879 table.insert(5, vec![SqliteValue::Integer(50)]);
31880
31881 let rows = run_with_storage_cursors(db, |b| {
31882 let end = b.emit_label();
31883 let not_found = b.emit_label();
31884 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31885 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
31886
31887 b.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
31889 b.emit_jump_to_label(Opcode::SeekLT, 0, 1, not_found, P4::None, 0);
31890
31891 b.emit_op(Opcode::Integer, 999, 2, 0, P4::None, 0);
31893 b.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
31894
31895 b.resolve_label(not_found);
31896 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31897 b.resolve_label(end);
31898 });
31899
31900 assert_eq!(rows.len(), 0);
31902 }
31903
31904 #[test]
31907 fn test_swiss_index_metrics_emitted_on_cursor_ops() {
31908 let mut db = MemDatabase::new();
31913 let root = db.create_table(2);
31914 assert!(root > 0, "create_table should return a valid root page");
31915
31916 db.get_table_mut(root).unwrap().insert_row(
31918 1,
31919 vec![SqliteValue::Integer(42), SqliteValue::Text("a".into())],
31920 );
31921 let table = db.get_table(root).unwrap();
31922 assert_eq!(table.rows.len(), 1, "table should contain the inserted row");
31923 }
31924
31925 #[test]
31926 fn test_swiss_index_replaces_hashmap_in_engine() {
31927 use fsqlite_btree::instrumentation::reset_btree_metrics;
31930
31931 reset_btree_metrics();
31932
31933 let rows = run_program(|b| {
31937 let end = b.emit_label();
31938 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31939 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
31940 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
31941 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31942 b.resolve_label(end);
31943 });
31944
31945 assert_eq!(rows.len(), 1);
31946 assert_eq!(rows[0][0], SqliteValue::Integer(42));
31947
31948 }
31952
31953 static VDBE_OBSERVABILITY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
31960
31961 fn run_sorter_metric_program() -> Vec<Vec<SqliteValue>> {
31962 run_program(|b| {
31963 let end = b.emit_label();
31964 let loop_start = b.emit_label();
31965 let empty = b.emit_label();
31966 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
31967
31968 let r_value = b.alloc_reg();
31969 let r_record = b.alloc_reg();
31970 let r_sorted = b.alloc_reg();
31971
31972 b.emit_op(Opcode::SorterOpen, 0, 1, 0, P4::None, 0);
31973
31974 for value in [50, 30, 10, 40, 20] {
31975 b.emit_op(Opcode::Integer, value, r_value, 0, P4::None, 0);
31976 b.emit_op(Opcode::MakeRecord, r_value, 1, r_record, P4::None, 0);
31977 b.emit_op(Opcode::SorterInsert, 0, r_record, 0, P4::None, 0);
31978 }
31979
31980 b.emit_jump_to_label(Opcode::SorterSort, 0, 0, empty, P4::None, 0);
31981 b.resolve_label(loop_start);
31982 b.emit_op(Opcode::SorterData, 0, r_sorted, 0, P4::None, 0);
31983 b.emit_op(Opcode::ResultRow, r_sorted, 1, 0, P4::None, 0);
31984 b.emit_jump_to_label(Opcode::SorterNext, 0, 0, loop_start, P4::None, 0);
31985 b.resolve_label(empty);
31986 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
31987 b.resolve_label(end);
31988 })
31989 }
31990
31991 #[test]
31992 fn test_sort_metrics_emitted_on_sorter_sort() {
31993 let _guard = VDBE_OBSERVABILITY_LOCK
31994 .lock()
31995 .unwrap_or_else(|e| e.into_inner());
31996 let prev_metrics_enabled = vdbe_metrics_enabled();
31997 set_vdbe_metrics_enabled(true);
31998 let before = vdbe_metrics_snapshot();
32001 let rows = run_sorter_metric_program();
32002
32003 assert_eq!(rows.len(), 5);
32004 let after = vdbe_metrics_snapshot();
32005 let delta_sort_rows = after.sort_rows_total - before.sort_rows_total;
32006 let delta_spill = after.sort_spill_pages_total - before.sort_spill_pages_total;
32007 assert!(
32008 delta_sort_rows >= 5,
32009 "sort_rows_total delta should be >= 5, got {delta_sort_rows}",
32010 );
32011 assert_eq!(delta_spill, 0, "no spill expected for small dataset");
32013 set_vdbe_metrics_enabled(prev_metrics_enabled);
32014 }
32015
32016 #[test]
32017 fn test_vdbe_metrics_can_be_disabled_off_hot_path() {
32018 let _guard = VDBE_OBSERVABILITY_LOCK
32019 .lock()
32020 .unwrap_or_else(|e| e.into_inner());
32021 let prev_metrics_enabled = vdbe_metrics_enabled();
32022 reset_vdbe_metrics();
32023 set_vdbe_metrics_enabled(false);
32024
32025 let before = vdbe_metrics_snapshot();
32026 let rows = run_sorter_metric_program();
32027 let after = vdbe_metrics_snapshot();
32028
32029 assert_eq!(rows.len(), 5);
32030 assert_eq!(after.opcodes_executed_total, before.opcodes_executed_total);
32031 assert_eq!(after.statements_total, before.statements_total);
32032 assert_eq!(
32033 after.statement_duration_us_total,
32034 before.statement_duration_us_total
32035 );
32036 assert_eq!(after.sort_rows_total, before.sort_rows_total);
32037 assert_eq!(after.sort_spill_pages_total, before.sort_spill_pages_total);
32038
32039 set_vdbe_metrics_enabled(prev_metrics_enabled);
32040 }
32041
32042 #[test]
32043 fn test_decode_record_with_metrics_flag_bypasses_global_reload() {
32044 let _guard = VDBE_OBSERVABILITY_LOCK
32045 .lock()
32046 .unwrap_or_else(|e| e.into_inner());
32047 let prev_metrics_enabled = vdbe_metrics_enabled();
32048 reset_vdbe_metrics();
32049 set_vdbe_metrics_enabled(false);
32050
32051 let record = SqliteValue::Blob(
32052 encode_record(&[SqliteValue::Integer(7), SqliteValue::Text("hello".into())]).into(),
32053 );
32054
32055 let before = vdbe_metrics_snapshot();
32056 let decoded_without_metrics =
32057 decode_record_with_metrics(&record, false).expect("record should decode");
32058 let after_without_metrics = vdbe_metrics_snapshot();
32059 assert_eq!(
32060 decoded_without_metrics,
32061 vec![SqliteValue::Integer(7), SqliteValue::Text("hello".into())]
32062 );
32063 assert_eq!(
32064 after_without_metrics.record_decode_calls_total,
32065 before.record_decode_calls_total
32066 );
32067 assert_eq!(
32068 after_without_metrics.decoded_values_total,
32069 before.decoded_values_total
32070 );
32071
32072 let decoded_with_metrics =
32073 decode_record_with_metrics(&record, true).expect("record should decode");
32074 let after_with_metrics = vdbe_metrics_snapshot();
32075 assert_eq!(decoded_with_metrics, decoded_without_metrics);
32076 assert_eq!(
32077 after_with_metrics.record_decode_calls_total,
32078 after_without_metrics.record_decode_calls_total + 1
32079 );
32080 assert_eq!(
32081 after_with_metrics.decoded_values_total,
32082 after_without_metrics.decoded_values_total + 2
32083 );
32084
32085 set_vdbe_metrics_enabled(prev_metrics_enabled);
32086 }
32087
32088 #[test]
32089 fn test_storage_cursor_decode_cache_metrics_track_hits_misses_and_position_changes() {
32090 let _guard = VDBE_OBSERVABILITY_LOCK
32091 .lock()
32092 .unwrap_or_else(|e| e.into_inner());
32093 let prev_metrics_enabled = vdbe_metrics_enabled();
32094 reset_vdbe_metrics();
32095 set_vdbe_metrics_enabled(true);
32096
32097 let mut db = MemDatabase::new();
32098 let root = db.create_table(1);
32099 let table = db.get_table_mut(root).expect("table should exist");
32100 table.insert(1, vec![SqliteValue::Integer(10)]);
32101 table.insert(2, vec![SqliteValue::Integer(20)]);
32102
32103 let before = vdbe_metrics_snapshot();
32104 let rows = run_with_storage_cursors(db, |b| {
32105 let end = b.emit_label();
32106 let eof = b.emit_label();
32107 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32108 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
32109 b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32110
32111 let body = b.current_addr();
32112 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
32113 b.emit_op(Opcode::Column, 0, 0, 2, P4::None, 0);
32114 b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32115
32116 let next_target =
32117 i32::try_from(body).expect("program counter should fit into i32 for tests");
32118 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
32119 b.resolve_label(eof);
32120 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32121 b.resolve_label(end);
32122 });
32123 let after = vdbe_metrics_snapshot();
32124
32125 assert_eq!(
32126 rows,
32127 vec![
32128 vec![SqliteValue::Integer(10), SqliteValue::Integer(10)],
32129 vec![SqliteValue::Integer(20), SqliteValue::Integer(20)],
32130 ]
32131 );
32132 assert_eq!(
32133 after.decode_cache_hits_total - before.decode_cache_hits_total,
32134 2
32135 );
32136 assert_eq!(
32137 after.decode_cache_misses_total - before.decode_cache_misses_total,
32138 2
32139 );
32140 assert_eq!(
32141 after.decode_cache_invalidations_position_total
32142 - before.decode_cache_invalidations_position_total,
32143 1
32144 );
32145 assert_eq!(
32146 after.decode_cache_invalidations_write_total
32147 - before.decode_cache_invalidations_write_total,
32148 0
32149 );
32150 assert_eq!(
32151 after.decode_cache_invalidations_pseudo_total
32152 - before.decode_cache_invalidations_pseudo_total,
32153 0
32154 );
32155
32156 set_vdbe_metrics_enabled(prev_metrics_enabled);
32157 }
32158
32159 #[test]
32160 fn test_pseudo_cursor_decode_cache_metrics_track_hits_and_blob_changes() {
32161 let _guard = VDBE_OBSERVABILITY_LOCK
32162 .lock()
32163 .unwrap_or_else(|e| e.into_inner());
32164 let prev_metrics_enabled = vdbe_metrics_enabled();
32165 reset_vdbe_metrics();
32166 set_vdbe_metrics_enabled(true);
32167
32168 let mut engine = VdbeEngine::new(2);
32169 engine.collect_vdbe_metrics = true;
32170 engine.cursors.insert(0, MemCursor::new_pseudo(1));
32171
32172 engine.set_reg_fast(
32173 1,
32174 SqliteValue::Blob(
32175 encode_record(&[SqliteValue::Integer(7), SqliteValue::Text("alpha".into())]).into(),
32176 ),
32177 );
32178
32179 let before = vdbe_metrics_snapshot();
32180 assert_eq!(
32181 run_async(engine.cursor_column(0, 0)).expect("pseudo row should decode"),
32182 SqliteValue::Integer(7)
32183 );
32184 assert_eq!(
32185 run_async(engine.cursor_column(0, 0)).expect("pseudo row cache should hit"),
32186 SqliteValue::Integer(7)
32187 );
32188
32189 engine.set_reg_fast(
32190 1,
32191 SqliteValue::Blob(
32192 encode_record(&[SqliteValue::Integer(9), SqliteValue::Text("beta".into())]).into(),
32193 ),
32194 );
32195 assert_eq!(
32196 run_async(engine.cursor_column(0, 0)).expect("changed pseudo row should decode"),
32197 SqliteValue::Integer(9)
32198 );
32199 let after = vdbe_metrics_snapshot();
32200
32201 assert_eq!(
32202 after.decode_cache_hits_total - before.decode_cache_hits_total,
32203 1
32204 );
32205 assert_eq!(
32206 after.decode_cache_misses_total - before.decode_cache_misses_total,
32207 2
32208 );
32209 assert_eq!(
32210 after.decode_cache_invalidations_pseudo_total
32211 - before.decode_cache_invalidations_pseudo_total,
32212 1
32213 );
32214 assert_eq!(
32215 after.decode_cache_invalidations_position_total
32216 - before.decode_cache_invalidations_position_total,
32217 0
32218 );
32219 assert_eq!(
32220 after.decode_cache_invalidations_write_total
32221 - before.decode_cache_invalidations_write_total,
32222 0
32223 );
32224
32225 set_vdbe_metrics_enabled(prev_metrics_enabled);
32226 }
32227
32228 #[test]
32229 fn test_sorter_decode_cache_metrics_track_hits_misses_and_position_changes() {
32230 let _guard = VDBE_OBSERVABILITY_LOCK
32231 .lock()
32232 .unwrap_or_else(|e| e.into_inner());
32233 let prev_metrics_enabled = vdbe_metrics_enabled();
32234 reset_vdbe_metrics();
32235 set_vdbe_metrics_enabled(true);
32236
32237 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32238 sorter
32239 .insert_row(
32240 vec![SqliteValue::Integer(1)],
32241 encode_record(&[SqliteValue::Integer(1), SqliteValue::Text("alpha".into())]),
32242 )
32243 .expect("sorter insert should succeed");
32244 sorter
32245 .insert_row(
32246 vec![SqliteValue::Integer(2)],
32247 encode_record(&[SqliteValue::Integer(2), SqliteValue::Text("beta".into())]),
32248 )
32249 .expect("sorter insert should succeed");
32250 sorter.sort().expect("sorter sort should succeed");
32251 sorter.position = Some(0);
32252
32253 let mut engine = VdbeEngine::new(1);
32254 engine.collect_vdbe_metrics = true;
32255 engine.sorters.insert(0, sorter);
32256
32257 let before = vdbe_metrics_snapshot();
32258 assert_eq!(
32259 run_async(engine.cursor_column(0, 1)).expect("first sorter decode should succeed"),
32260 SqliteValue::Text("alpha".into())
32261 );
32262 assert_eq!(
32263 run_async(engine.cursor_column(0, 1)).expect("second sorter read should hit cache"),
32264 SqliteValue::Text("alpha".into())
32265 );
32266 engine
32267 .sorters
32268 .get_mut(&0)
32269 .expect("sorter cursor should exist")
32270 .position = Some(1);
32271 assert_eq!(
32272 run_async(engine.cursor_column(0, 1)).expect("next sorter row should decode"),
32273 SqliteValue::Text("beta".into())
32274 );
32275 let after = vdbe_metrics_snapshot();
32276
32277 assert_eq!(
32278 after.decode_cache_hits_total - before.decode_cache_hits_total,
32279 1
32280 );
32281 assert_eq!(
32282 after.decode_cache_misses_total - before.decode_cache_misses_total,
32283 2
32284 );
32285 assert_eq!(
32286 after.decode_cache_invalidations_position_total
32287 - before.decode_cache_invalidations_position_total,
32288 1
32289 );
32290 assert_eq!(
32291 after.decode_cache_invalidations_write_total
32292 - before.decode_cache_invalidations_write_total,
32293 0
32294 );
32295 assert_eq!(
32296 after.decode_cache_invalidations_pseudo_total
32297 - before.decode_cache_invalidations_pseudo_total,
32298 0
32299 );
32300
32301 set_vdbe_metrics_enabled(prev_metrics_enabled);
32302 }
32303
32304 #[test]
32305 fn test_storage_cursor_decode_cache_hits_wide_row_tail_column_after_eager_decode() {
32306 let _guard = VDBE_OBSERVABILITY_LOCK
32307 .lock()
32308 .unwrap_or_else(|e| e.into_inner());
32309 let prev_metrics_enabled = vdbe_metrics_enabled();
32310 reset_vdbe_metrics();
32311 set_vdbe_metrics_enabled(true);
32312
32313 let mut db = MemDatabase::new();
32314 let root = db.create_table(65);
32315 let row = (0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>();
32316 db.get_table_mut(root)
32317 .expect("table should exist")
32318 .insert(1, row);
32319
32320 let before = vdbe_metrics_snapshot();
32321 let rows = run_with_storage_cursors(db, |b| {
32322 let end = b.emit_label();
32323 let eof = b.emit_label();
32324 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32325 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(65), 0);
32326 b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32327 b.emit_op(Opcode::Column, 0, 64, 1, P4::None, 0);
32328 b.emit_op(Opcode::Column, 0, 64, 2, P4::None, 0);
32329 b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32330 b.resolve_label(eof);
32331 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32332 b.resolve_label(end);
32333 });
32334 let after = vdbe_metrics_snapshot();
32335
32336 assert_eq!(
32337 rows,
32338 vec![vec![SqliteValue::Integer(64), SqliteValue::Integer(64)]]
32339 );
32340 assert_eq!(
32341 after.decode_cache_hits_total - before.decode_cache_hits_total,
32342 1
32343 );
32344 assert_eq!(
32345 after.decode_cache_misses_total - before.decode_cache_misses_total,
32346 1
32347 );
32348
32349 set_vdbe_metrics_enabled(prev_metrics_enabled);
32350 }
32351
32352 #[test]
32353 fn test_sorter_decode_cache_hits_wide_row_tail_column_after_eager_decode() {
32354 let _guard = VDBE_OBSERVABILITY_LOCK
32355 .lock()
32356 .unwrap_or_else(|e| e.into_inner());
32357 let prev_metrics_enabled = vdbe_metrics_enabled();
32358 reset_vdbe_metrics();
32359 set_vdbe_metrics_enabled(true);
32360
32361 let record = encode_record(&(0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>());
32362 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32363 sorter
32364 .insert_row(vec![SqliteValue::Integer(0)], record)
32365 .expect("sorter insert should succeed");
32366 sorter.sort().expect("sorter sort should succeed");
32367 sorter.position = Some(0);
32368
32369 let mut engine = VdbeEngine::new(1);
32370 engine.collect_vdbe_metrics = true;
32371 engine.sorters.insert(0, sorter);
32372
32373 let before = vdbe_metrics_snapshot();
32374 assert_eq!(
32375 run_async(engine.cursor_column(0, 64))
32376 .expect("first wide sorter decode should succeed"),
32377 SqliteValue::Integer(64)
32378 );
32379 assert_eq!(
32380 run_async(engine.cursor_column(0, 64))
32381 .expect("second wide sorter read should hit cache"),
32382 SqliteValue::Integer(64)
32383 );
32384 let after = vdbe_metrics_snapshot();
32385
32386 assert_eq!(
32387 after.decode_cache_hits_total - before.decode_cache_hits_total,
32388 1
32389 );
32390 assert_eq!(
32391 after.decode_cache_misses_total - before.decode_cache_misses_total,
32392 1
32393 );
32394
32395 set_vdbe_metrics_enabled(prev_metrics_enabled);
32396 }
32397
32398 #[test]
32399 fn test_storage_cursor_wide_row_reuse_hits_cache_without_second_parse_into() {
32400 let _guard = VDBE_OBSERVABILITY_LOCK
32401 .lock()
32402 .unwrap_or_else(|e| e.into_inner());
32403 let prev_metrics_enabled = vdbe_metrics_enabled();
32404 let _record_profile_guard = RecordProfileThreadOverrideGuard::enabled();
32405 reset_vdbe_metrics();
32406 set_vdbe_metrics_enabled(true);
32407 fsqlite_types::record::reset_record_profile();
32408
32409 let mut db = MemDatabase::new();
32410 let root = db.create_table(65);
32411 let row = (0_i64..65).map(SqliteValue::Integer).collect::<Vec<_>>();
32412 db.get_table_mut(root)
32413 .expect("table should exist")
32414 .insert(1, row);
32415
32416 let before_metrics = vdbe_metrics_snapshot();
32417 let before_record_profile = fsqlite_types::record::record_profile_snapshot();
32418 let rows = run_with_storage_cursors(db, |b| {
32419 let end = b.emit_label();
32420 let eof = b.emit_label();
32421 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32422 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(65), 0);
32423 b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof, P4::None, 0);
32424 b.emit_op(Opcode::Column, 0, 64, 1, P4::None, 0);
32425 b.emit_op(Opcode::Column, 0, 64, 2, P4::None, 0);
32426 b.emit_op(Opcode::ResultRow, 1, 2, 0, P4::None, 0);
32427 b.resolve_label(eof);
32428 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32429 b.resolve_label(end);
32430 });
32431 let after_metrics = vdbe_metrics_snapshot();
32432 let after_record_profile = fsqlite_types::record::record_profile_snapshot();
32433
32434 assert_eq!(
32435 rows,
32436 vec![vec![SqliteValue::Integer(64), SqliteValue::Integer(64)]]
32437 );
32438 assert_eq!(
32439 after_metrics.decode_cache_hits_total - before_metrics.decode_cache_hits_total,
32440 1
32441 );
32442 assert_eq!(
32443 after_metrics.decode_cache_misses_total - before_metrics.decode_cache_misses_total,
32444 1
32445 );
32446 assert_eq!(
32447 after_record_profile
32448 .callsite_breakdown
32449 .vdbe_engine
32450 .parse_record_into_calls
32451 - before_record_profile
32452 .callsite_breakdown
32453 .vdbe_engine
32454 .parse_record_into_calls,
32455 1
32456 );
32457
32458 set_vdbe_metrics_enabled(prev_metrics_enabled);
32459 }
32460
32461 #[test]
32462 #[ignore = "manual perf evidence for bd-db300.2.2"]
32463 fn bench_vdbe_metrics_toggle_execute_hot_path() {
32464 let _guard = VDBE_OBSERVABILITY_LOCK
32465 .lock()
32466 .unwrap_or_else(|e| e.into_inner());
32467 let prev_metrics_enabled = vdbe_metrics_enabled();
32468 let iterations = 20_000;
32469
32470 let mut b = ProgramBuilder::new();
32471 let end = b.emit_label();
32472 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32473 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32474 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32475 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32476 b.resolve_label(end);
32477 let prog = b.finish().expect("program should build");
32478
32479 let run = |metrics_enabled| {
32480 set_vdbe_metrics_enabled(metrics_enabled);
32481 reset_vdbe_metrics();
32482 let mut engine = VdbeEngine::new(prog.register_count());
32483 let start = Instant::now();
32484 for _ in 0..iterations {
32485 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
32486 assert_eq!(outcome, ExecOutcome::Done);
32487 engine.results.clear();
32488 }
32489 start.elapsed()
32490 };
32491
32492 let metrics_disabled = run(false);
32493 let metrics_enabled = run(true);
32494 eprintln!(
32495 "bd-db300.2.2 execute hot-path benchmark: iterations={iterations}, metrics_disabled_us={}, metrics_enabled_us={}",
32496 metrics_disabled.as_micros(),
32497 metrics_enabled.as_micros()
32498 );
32499
32500 set_vdbe_metrics_enabled(prev_metrics_enabled);
32501 }
32502
32503 #[test]
32504 fn test_jit_scaffold_metrics_compile_and_cache_hit() {
32505 let _guard = VDBE_OBSERVABILITY_LOCK
32506 .lock()
32507 .unwrap_or_else(|e| e.into_inner());
32508 let prev_enabled = vdbe_jit_enabled();
32509 let prev_threshold = vdbe_jit_hot_threshold();
32510 let prev_capacity = vdbe_jit_cache_capacity();
32511
32512 set_vdbe_jit_enabled(true);
32514 let _ = set_vdbe_jit_hot_threshold(2);
32515 let _ = set_vdbe_jit_cache_capacity(8);
32516 let before = vdbe_jit_metrics_snapshot();
32517
32518 for _ in 0..3 {
32519 let rows = run_program(|b| {
32520 let end = b.emit_label();
32521 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32522 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32523 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32524 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32525 b.resolve_label(end);
32526 });
32527 assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
32528 }
32529
32530 let after = vdbe_jit_metrics_snapshot();
32531 let delta_compilations = after.jit_compilations_total - before.jit_compilations_total;
32532 let delta_cache_hits = after.jit_cache_hits_total - before.jit_cache_hits_total;
32533 assert!(
32534 delta_compilations >= 1,
32535 "expected at least one JIT compile delta, got {delta_compilations}",
32536 );
32537 assert!(
32538 delta_cache_hits >= 1,
32539 "expected at least one JIT cache hit delta, got {delta_cache_hits}",
32540 );
32541 assert!(
32542 after.cache_entries >= 1,
32543 "expected non-empty JIT cache after hot runs"
32544 );
32545
32546 set_vdbe_jit_enabled(prev_enabled);
32547 let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32548 let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32549 }
32550
32551 #[test]
32552 fn test_jit_disabled_leaves_runtime_state_cold() {
32553 let _guard = VDBE_OBSERVABILITY_LOCK
32554 .lock()
32555 .unwrap_or_else(|e| e.into_inner());
32556 let prev_enabled = vdbe_jit_enabled();
32557 let prev_threshold = vdbe_jit_hot_threshold();
32558 let prev_capacity = vdbe_jit_cache_capacity();
32559
32560 set_vdbe_jit_enabled(false);
32561 let _ = set_vdbe_jit_hot_threshold(1);
32562 let _ = set_vdbe_jit_cache_capacity(8);
32563 reset_vdbe_jit_metrics();
32564 let before = vdbe_jit_metrics_snapshot();
32565
32566 for _ in 0..3 {
32567 let rows = run_program(|b| {
32568 let end = b.emit_label();
32569 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32570 b.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
32571 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32572 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32573 b.resolve_label(end);
32574 });
32575 assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
32576 }
32577
32578 let after = vdbe_jit_metrics_snapshot();
32579 assert_eq!(after.jit_compilations_total, before.jit_compilations_total);
32580 assert_eq!(
32581 after.jit_compile_failures_total,
32582 before.jit_compile_failures_total
32583 );
32584 assert_eq!(after.jit_triggers_total, before.jit_triggers_total);
32585 assert_eq!(after.jit_cache_hits_total, before.jit_cache_hits_total);
32586 assert_eq!(after.jit_cache_misses_total, before.jit_cache_misses_total);
32587 assert_eq!(after.cache_entries, before.cache_entries);
32588
32589 set_vdbe_jit_enabled(prev_enabled);
32590 let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32591 let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32592 }
32593
32594 #[test]
32595 fn test_jit_scaffold_compile_failure_falls_back_to_interpreter() {
32596 let _guard = VDBE_OBSERVABILITY_LOCK
32597 .lock()
32598 .unwrap_or_else(|e| e.into_inner());
32599 let prev_enabled = vdbe_jit_enabled();
32600 let prev_threshold = vdbe_jit_hot_threshold();
32601 let prev_capacity = vdbe_jit_cache_capacity();
32602
32603 set_vdbe_jit_enabled(true);
32605 let _ = set_vdbe_jit_hot_threshold(1);
32606 let _ = set_vdbe_jit_cache_capacity(8);
32607 let before = vdbe_jit_metrics_snapshot();
32608
32609 let mut db = MemDatabase::new();
32610 let root = db.create_table(1);
32611 db.get_table_mut(root)
32612 .expect("table should exist")
32613 .insert_row(7, vec![SqliteValue::Integer(700)]);
32614
32615 let rows = run_with_storage_cursors(db, |b| {
32616 let start = b.emit_label();
32617 b.emit_jump_to_label(Opcode::Init, 0, 0, start, P4::None, 0);
32618 b.resolve_label(start);
32619 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
32620 let empty = b.emit_label();
32621 b.emit_jump_to_label(Opcode::Rewind, 0, 0, empty, P4::None, 0);
32622 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
32623 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32624 b.resolve_label(empty);
32625 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32626 });
32627
32628 assert_eq!(rows, vec![vec![SqliteValue::Integer(700)]]);
32629 let after = vdbe_jit_metrics_snapshot();
32630 let delta_failures = after.jit_compile_failures_total - before.jit_compile_failures_total;
32631 assert!(
32632 delta_failures >= 1,
32633 "expected at least one JIT compile failure delta, got {delta_failures}",
32634 );
32635
32636 set_vdbe_jit_enabled(prev_enabled);
32637 let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32638 let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32639 }
32640
32641 #[test]
32642 fn test_jit_scaffold_distinguishes_programs_that_only_differ_in_p4() {
32643 let _guard = VDBE_OBSERVABILITY_LOCK
32644 .lock()
32645 .unwrap_or_else(|e| e.into_inner());
32646 let prev_enabled = vdbe_jit_enabled();
32647 let prev_threshold = vdbe_jit_hot_threshold();
32648 let prev_capacity = vdbe_jit_cache_capacity();
32649
32650 set_vdbe_jit_enabled(true);
32651 let _ = set_vdbe_jit_hot_threshold(1);
32652 let _ = set_vdbe_jit_cache_capacity(8);
32653 reset_vdbe_jit_metrics();
32654 let before = vdbe_jit_metrics_snapshot();
32655
32656 let alpha_rows = run_program(|b| {
32657 let end = b.emit_label();
32658 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32659 b.emit_op(Opcode::String8, 0, 1, 0, P4::Str("alpha".to_owned()), 0);
32660 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32661 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32662 b.resolve_label(end);
32663 });
32664 assert_eq!(alpha_rows, vec![vec![SqliteValue::Text("alpha".into())]]);
32665
32666 let beta_rows = run_program(|b| {
32667 let end = b.emit_label();
32668 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
32669 b.emit_op(Opcode::String8, 0, 1, 0, P4::Str("beta".to_owned()), 0);
32670 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
32671 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
32672 b.resolve_label(end);
32673 });
32674 assert_eq!(beta_rows, vec![vec![SqliteValue::Text("beta".into())]]);
32675
32676 let after = vdbe_jit_metrics_snapshot();
32677 let delta_compilations = after.jit_compilations_total - before.jit_compilations_total;
32678 let delta_cache_hits = after.jit_cache_hits_total - before.jit_cache_hits_total;
32679 assert!(
32680 delta_compilations >= 2,
32681 "expected both distinct P4 programs to compile separately, got {delta_compilations}",
32682 );
32683 assert_eq!(
32684 delta_cache_hits, 0,
32685 "programs that only differ in P4 must not alias in the JIT cache",
32686 );
32687
32688 reset_vdbe_jit_metrics();
32689 set_vdbe_jit_enabled(prev_enabled);
32690 let _ = set_vdbe_jit_hot_threshold(prev_threshold);
32691 let _ = set_vdbe_jit_cache_capacity(prev_capacity);
32692 }
32693
32694 #[test]
32695 fn test_jit_cache_hit_arc_clone_microbench() {
32696 let value_sources = (0..24).map(InsertValueSource::Binding).collect::<Vec<_>>();
32697 let compiled = CompiledProgram::SimpleInsert(SimpleInsertTemplate {
32698 cursor_id: 0,
32699 root_page: 2,
32700 num_cols: i32::try_from(value_sources.len()).expect("test value count fits i32"),
32701 value_sources,
32702 affinity: Some("BBBBBBBBBBBBBBBBBBBBBBBB".to_owned()),
32703 record_p4: P4::None,
32704 record_builder: CompiledRecordBuilder::Generic,
32705 insert_flags: 2,
32706 });
32707 let shared = Arc::new(compiled.clone());
32708 let iterations = 50_000_u64;
32709
32710 let direct_started = Instant::now();
32711 let mut direct_acc = 0_u64;
32712 for _ in 0..iterations {
32713 let cloned = std::hint::black_box(compiled.clone());
32714 direct_acc = direct_acc.wrapping_add(estimate_compiled_program_size(&cloned));
32715 }
32716 let direct_clone_ns = direct_started.elapsed().as_nanos();
32717
32718 let arc_started = Instant::now();
32719 let mut arc_acc = 0_usize;
32720 for _ in 0..iterations {
32721 let cloned = std::hint::black_box(Arc::clone(&shared));
32722 arc_acc = arc_acc.wrapping_add(Arc::strong_count(&cloned));
32723 }
32724 let arc_clone_ns = arc_started.elapsed().as_nanos();
32725
32726 std::hint::black_box((direct_acc, arc_acc));
32727 eprintln!(
32728 "bd-db300.2 jit cache-hit clone microbench: iterations={iterations}, deep_clone_ns={direct_clone_ns}, arc_clone_ns={arc_clone_ns}, template_size_bytes={}",
32729 estimate_compiled_program_size(shared.as_ref())
32730 );
32731 assert!(direct_acc > 0);
32732 assert!(arc_acc > 0);
32733 assert_eq!(Arc::strong_count(&shared), 1);
32734 }
32735
32736 #[test]
32737 fn test_sorter_spill_to_disk_under_low_threshold() {
32738 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32744 sorter.spill_threshold = 1;
32746
32747 for value in [50i64, 30, 10, 40, 20] {
32751 let vals = vec![SqliteValue::Integer(value)];
32752 let blob = serialize_record(&vals);
32753 sorter
32754 .insert_row(vals, blob)
32755 .expect("insert should succeed");
32756 }
32757
32758 assert!(
32760 !sorter.spill_runs.is_empty(),
32761 "low threshold should cause spills"
32762 );
32763 let spill_count = sorter.spill_runs.len();
32764 assert!(
32765 sorter.spill_pages_total > 0,
32766 "spill_pages_total should be > 0"
32767 );
32768
32769 sorter.sort().expect("sort should succeed");
32771
32772 assert!(sorter.spill_runs.is_empty(), "runs should be drained");
32774
32775 let values: Vec<i64> = sorter
32777 .rows
32778 .iter()
32779 .map(|r| r.values[0].to_integer())
32780 .collect();
32781 assert_eq!(values, vec![10, 20, 30, 40, 50]);
32782
32783 assert!(
32785 sorter.rows_sorted_total >= 5,
32786 "rows_sorted_total should be >= 5, got {}",
32787 sorter.rows_sorted_total
32788 );
32789 assert!(spill_count >= 1, "at least one spill run expected");
32791 }
32792
32793 #[test]
32794 fn test_sorter_reset_cleans_spill_state() {
32795 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32797 sorter.spill_threshold = 1;
32798
32799 for value in [3i64, 1, 2] {
32800 sorter
32801 .insert_row(vec![SqliteValue::Integer(value)], vec![])
32802 .expect("insert should succeed");
32803 }
32804 assert!(!sorter.spill_runs.is_empty());
32805
32806 sorter.reset();
32807
32808 assert!(sorter.rows.is_empty(), "rows should be cleared");
32809 assert!(sorter.position.is_none(), "position should be None");
32810 assert!(sorter.spill_runs.is_empty(), "spill_runs should be cleared");
32811 assert_eq!(sorter.memory_used, 0, "memory_used should be 0");
32812 }
32813
32814 #[test]
32815 fn test_sorter_desc_key_order_with_external_merge() {
32816 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Desc], Vec::new());
32819 sorter.spill_threshold = 1;
32820
32821 for value in [10i64, 50, 30, 20, 40] {
32822 let vals = vec![SqliteValue::Integer(value)];
32823 let blob = serialize_record(&vals);
32824 sorter
32825 .insert_row(vals, blob)
32826 .expect("insert should succeed");
32827 }
32828
32829 sorter.sort().expect("sort should succeed");
32830
32831 let values: Vec<i64> = sorter
32832 .rows
32833 .iter()
32834 .map(|r| r.values[0].to_integer())
32835 .collect();
32836 assert_eq!(values, vec![50, 40, 30, 20, 10]);
32837 }
32838
32839 #[test]
32840 fn test_sorter_multi_column_key_with_mixed_order() {
32841 let mut sorter =
32843 SorterCursor::new(2, vec![SortKeyOrder::Asc, SortKeyOrder::Desc], Vec::new());
32844
32845 for (group, value) in [(1i64, 30i64), (2, 10), (1, 20), (2, 40), (1, 10)] {
32847 sorter
32848 .insert_row(
32849 vec![SqliteValue::Integer(group), SqliteValue::Integer(value)],
32850 vec![],
32851 )
32852 .expect("insert should succeed");
32853 }
32854
32855 sorter.sort().expect("sort should succeed");
32856
32857 let values: Vec<(i64, i64)> = sorter
32858 .rows
32859 .iter()
32860 .map(|r| (r.values[0].to_integer(), r.values[1].to_integer()))
32861 .collect();
32862 assert_eq!(values, vec![(1, 30), (1, 20), (1, 10), (2, 40), (2, 10)]);
32865 }
32866
32867 #[test]
32868 fn test_sorter_memory_estimation() {
32869 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32871 assert_eq!(sorter.memory_used, 0);
32872
32873 sorter
32874 .insert_row(vec![SqliteValue::Integer(42)], vec![])
32875 .expect("insert should succeed");
32876 let after_one = sorter.memory_used;
32877 assert!(after_one > 0, "memory should increase after insert");
32878
32879 sorter
32880 .insert_row(vec![SqliteValue::Text("hello world".into())], vec![])
32881 .expect("insert should succeed");
32882 let after_two = sorter.memory_used;
32883 assert!(
32884 after_two > after_one,
32885 "memory should increase with text value"
32886 );
32887 }
32888
32889 #[test]
32890 fn test_sorter_empty_sort() {
32891 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32893 sorter.sort().expect("empty sort should succeed");
32894 assert!(sorter.rows.is_empty());
32895 }
32896
32897 #[test]
32898 fn test_sorter_pure_inmemory_sort_path() {
32899 let mut sorter = SorterCursor::new(1, vec![SortKeyOrder::Asc], Vec::new());
32901 for value in [5i64, 3, 1, 4, 2] {
32904 sorter
32905 .insert_row(vec![SqliteValue::Integer(value)], vec![])
32906 .expect("insert should succeed");
32907 }
32908
32909 assert!(sorter.spill_runs.is_empty(), "no spill expected");
32910 sorter.sort().expect("sort should succeed");
32911
32912 let values: Vec<i64> = sorter
32913 .rows
32914 .iter()
32915 .map(|r| r.values[0].to_integer())
32916 .collect();
32917 assert_eq!(values, vec![1, 2, 3, 4, 5]);
32918 assert_eq!(sorter.rows_sorted_total, 5);
32919 }
32920
32921 #[test]
32922 fn test_sorter_top_n_limit_keeps_only_best_rows() {
32923 let mut sorter = SorterCursor::with_collation_registry(
32924 1,
32925 vec![SortKeyOrder::Asc],
32926 Vec::new(),
32927 Arc::new(Mutex::new(CollationRegistry::new())),
32928 Some(3),
32929 );
32930
32931 for value in [9i64, 1, 7, 3, 2] {
32932 sorter
32933 .insert_row(vec![SqliteValue::Integer(value)], vec![])
32934 .expect("insert should succeed");
32935 }
32936
32937 assert_eq!(
32938 sorter.rows.len(),
32939 3,
32940 "top-N sorter should prune rows during insertion"
32941 );
32942 sorter.sort().expect("sort should succeed");
32943
32944 let values: Vec<i64> = sorter
32945 .rows
32946 .iter()
32947 .map(|r| r.values[0].to_integer())
32948 .collect();
32949 assert_eq!(values, vec![1, 2, 3]);
32950 }
32951
32952 #[test]
32953 fn test_sorter_top_n_limit_desc_uses_heap_then_final_sort() {
32954 let mut sorter = SorterCursor::with_collation_registry(
32955 1,
32956 vec![SortKeyOrder::Desc],
32957 Vec::new(),
32958 Arc::new(Mutex::new(CollationRegistry::new())),
32959 Some(3),
32960 );
32961
32962 for value in [1i64, 2, 3, 4, 5] {
32963 sorter
32964 .insert_row(vec![SqliteValue::Integer(value)], vec![])
32965 .expect("insert should succeed");
32966 }
32967
32968 let mut retained: Vec<i64> = sorter
32969 .rows
32970 .iter()
32971 .map(|r| r.values[0].to_integer())
32972 .collect();
32973 retained.sort_unstable();
32974 assert_eq!(retained, vec![3, 4, 5]);
32975
32976 sorter.sort().expect("sort should succeed");
32977 let sorted: Vec<i64> = sorter
32978 .rows
32979 .iter()
32980 .map(|r| r.values[0].to_integer())
32981 .collect();
32982 assert_eq!(sorted, vec![5, 4, 3]);
32983 }
32984
32985 #[test]
32986 fn test_sorter_top_n_equal_keys_keep_earliest_source_rows() {
32987 let mut sorter = SorterCursor::with_collation_registry(
32988 1,
32989 vec![SortKeyOrder::Asc],
32990 Vec::new(),
32991 Arc::new(Mutex::new(CollationRegistry::new())),
32992 Some(2),
32993 );
32994
32995 for payload in [10i64, 20, 30] {
32996 let values = [SqliteValue::Integer(1), SqliteValue::Integer(payload)];
32997 sorter
32998 .insert_row(
32999 vec![values[0].clone()],
33000 fsqlite_types::record::serialize_record(&values),
33001 )
33002 .expect("insert should succeed");
33003 }
33004
33005 sorter.sort().expect("sort should succeed");
33006 let payloads: Vec<i64> = sorter
33007 .rows
33008 .iter()
33009 .map(|row| {
33010 fsqlite_types::record::parse_record(&row.blob)
33011 .expect("retained record should decode")[1]
33012 .to_integer()
33013 })
33014 .collect();
33015 assert_eq!(payloads, vec![10, 20]);
33016 }
33017
33018 #[cfg(not(target_arch = "wasm32"))]
33019 #[test]
33020 fn test_sorter_top_n_downgrades_before_discard_to_preserve_stable_spill_input() {
33021 let mut sorter = SorterCursor::with_collation_registry(
33022 1,
33023 vec![SortKeyOrder::Asc],
33024 Vec::new(),
33025 Arc::new(Mutex::new(CollationRegistry::new())),
33026 Some(10),
33027 );
33028 sorter.spill_threshold = usize::MAX;
33029
33030 let make_row = |payload| {
33031 let record = [SqliteValue::Integer(7), SqliteValue::Integer(payload)];
33032 (
33033 vec![record[0].clone()],
33034 fsqlite_types::record::serialize_record(&record),
33035 )
33036 };
33037
33038 for payload in [10i64, 20] {
33039 let (key, blob) = make_row(payload);
33040 sorter
33041 .insert_row(key, blob)
33042 .expect("heap-building insert should succeed");
33043 }
33044 let heap_payloads: Vec<i64> = sorter
33045 .rows
33046 .iter()
33047 .map(|row| {
33048 fsqlite_types::record::parse_record(&row.blob).expect("heap row should decode")[1]
33049 .to_integer()
33050 })
33051 .collect();
33052 assert_eq!(
33053 heap_payloads,
33054 vec![20, 10],
33055 "setup should prove the bounded heap reordered equal-key rows"
33056 );
33057
33058 let (third_key, third_blob) = make_row(30);
33059 let third_size = SorterCursor::estimate_row_size(&third_key, &third_blob);
33060 sorter.spill_threshold = sorter.memory_used.saturating_add(third_size);
33061 sorter
33062 .insert_row(third_key, third_blob)
33063 .expect("downgrading insert should spill successfully");
33064
33065 assert!(
33066 sorter.top_n_limit.is_none(),
33067 "complete input prefix should downgrade to the spillable sorter"
33068 );
33069 assert!(
33070 !sorter.spill_runs.is_empty(),
33071 "downgrading insert should activate the ordinary spill path"
33072 );
33073
33074 let (fourth_key, fourth_blob) = make_row(40);
33075 sorter
33076 .insert_row(fourth_key, fourth_blob)
33077 .expect("post-downgrade insert should succeed");
33078 sorter.sort().expect("external merge should succeed");
33079
33080 let payloads: Vec<i64> = sorter
33081 .rows
33082 .iter()
33083 .map(|row| {
33084 fsqlite_types::record::parse_record(&row.blob).expect("merged row should decode")[1]
33085 .to_integer()
33086 })
33087 .collect();
33088 assert_eq!(
33089 payloads,
33090 vec![10, 20, 30, 40],
33091 "downgrade must preserve every row and stable equal-key source order"
33092 );
33093 }
33094
33095 #[cfg(target_arch = "wasm32")]
33096 #[test]
33097 fn test_sorter_top_n_stays_bounded_without_native_spill_support() {
33098 let mut sorter = SorterCursor::with_collation_registry(
33099 1,
33100 vec![SortKeyOrder::Asc],
33101 Vec::new(),
33102 Arc::new(Mutex::new(CollationRegistry::new())),
33103 Some(2),
33104 );
33105 sorter.spill_threshold = 1;
33106
33107 for value in [3i64, 2, 1] {
33108 sorter
33109 .insert_row(vec![SqliteValue::Integer(value)], Vec::new())
33110 .expect("bounded insert should succeed");
33111 }
33112
33113 assert_eq!(
33114 sorter.top_n_limit,
33115 Some(2),
33116 "wasm must keep its effective top-N memory bound"
33117 );
33118 sorter
33119 .sort()
33120 .expect("bounded in-memory sort should succeed");
33121 let values: Vec<i64> = sorter
33122 .rows
33123 .iter()
33124 .map(|row| row.values[0].to_integer())
33125 .collect();
33126 assert_eq!(values, vec![1, 2]);
33127 }
33128
33129 #[test]
33130 fn test_sorter_top_n_preflight_respects_multikey_null_and_nocase_order() {
33131 let registry = Arc::new(Mutex::new(CollationRegistry::new()));
33132 let mut sorter = SorterCursor::with_collation_registry(
33133 2,
33134 vec![SortKeyOrder::AscNullsLast, SortKeyOrder::Desc],
33135 vec![None, Some("NOCASE".to_owned())],
33136 Arc::clone(®istry),
33137 Some(1),
33138 );
33139 sorter
33140 .insert_row(
33141 vec![SqliteValue::Integer(2), SqliteValue::Text("beta".into())],
33142 Vec::new(),
33143 )
33144 .expect("insert should succeed");
33145
33146 let collations = registry.lock().unwrap_or_else(|error| error.into_inner());
33147 assert!(sorter.would_retain_top_n(
33148 &[SqliteValue::Integer(1), SqliteValue::Text("alpha".into()),],
33149 &collations,
33150 ));
33151 assert!(!sorter.would_retain_top_n(
33152 &[SqliteValue::Integer(2), SqliteValue::Text("BETA".into()),],
33153 &collations,
33154 ));
33155 assert!(!sorter.would_retain_top_n(
33156 &[SqliteValue::Null, SqliteValue::Text("zeta".into())],
33157 &collations,
33158 ));
33159 }
33160
33161 #[test]
33164 fn test_reject_mem_fallback_default_on() {
33165 let engine = VdbeEngine::new(8);
33167 assert!(engine.reject_mem_fallback);
33168 }
33169
33170 #[test]
33171 fn test_set_reject_mem_fallback() {
33172 let mut engine = VdbeEngine::new(8);
33173 engine.set_reject_mem_fallback(true);
33174 assert!(engine.reject_mem_fallback);
33175 engine.set_reject_mem_fallback(false);
33176 assert!(!engine.reject_mem_fallback);
33177 }
33178
33179 #[test]
33180 fn test_open_storage_cursor_mem_fallback_without_parity_cert() {
33181 let mut db = MemDatabase::new();
33184 let root = db.create_table(1);
33185 let table = db.get_table_mut(root).expect("table should exist");
33186 table.insert(1, vec![SqliteValue::Integer(42)]);
33187
33188 let mut engine = VdbeEngine::new(8);
33189 engine.set_database(db);
33190 engine.set_reject_mem_fallback(false);
33192
33193 assert!(run_async(engine.open_storage_cursor(0, root, false)));
33195 assert!(engine.storage_cursors.get(&0).is_some());
33196 }
33197
33198 #[test]
33199 fn test_open_storage_cursor_rejected_in_parity_cert_mode() {
33200 let mut db = MemDatabase::new();
33204 let root = db.create_table(1);
33205 let table = db.get_table_mut(root).expect("table should exist");
33206 table.insert(1, vec![SqliteValue::Integer(42)]);
33207
33208 let mut engine = VdbeEngine::new(8);
33209 engine.set_database(db);
33210 engine.set_reject_mem_fallback(true);
33212
33213 assert!(!run_async(engine.open_storage_cursor(0, root, false)));
33215 assert!(engine.storage_cursors.get(&0).is_none());
33216 }
33217
33218 #[test]
33219 fn test_open_storage_cursor_invalid_page_number() {
33220 let mut engine = VdbeEngine::new(8);
33222 assert!(!run_async(engine.open_storage_cursor(0, 0, false)));
33223 }
33224
33225 #[test]
33226 fn test_bd_2ttd8_set_transaction_enables_storage_cursors() {
33227 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33229
33230 let pager = MockMvccPager;
33231 let cx = Cx::new();
33232 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33233
33234 let mut engine = VdbeEngine::new(8);
33235 engine.set_transaction(txn);
33236 assert!(engine.storage_cursors_enabled);
33237 assert!(engine.txn_page_io.is_some());
33238 }
33239
33240 #[test]
33241 fn test_storage_only_table_program_executes_without_attached_memdb() {
33242 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
33243
33244 let pager = MemoryMockMvccPager;
33245 let cx = Cx::new();
33246 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33247 let root = 256;
33248
33249 let mut b = ProgramBuilder::new();
33250 let end = b.emit_label();
33251 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33252 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33253 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33254 b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0);
33255 b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0);
33256 b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
33257 b.emit_op(Opcode::Count, 0, 4, 0, P4::None, 0);
33258 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
33259 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33260 b.resolve_label(end);
33261
33262 let prog = b.finish().expect("program should build");
33263 assert!(
33264 !prog.requires_attached_memdb(),
33265 "storage-only table bytecode should not require an attached MemDatabase"
33266 );
33267
33268 let mut engine = VdbeEngine::new(prog.register_count());
33269 engine.set_transaction(txn);
33270 engine.set_reject_mem_fallback(true);
33271
33272 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33273 assert_eq!(outcome, ExecOutcome::Done);
33274 assert!(
33275 engine.all_cursors_are_txn_backed(),
33276 "storage-only hot path must stay on txn-backed cursors"
33277 );
33278 let rows: Vec<_> = engine
33279 .take_results()
33280 .into_iter()
33281 .map(|row| row.into_vec())
33282 .collect();
33283 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33284 assert!(
33285 engine.take_database().is_none(),
33286 "the engine should not need an attached MemDatabase for this hot path"
33287 );
33288 }
33289
33290 #[test]
33291 fn test_storage_only_nested_loop_join_executes_without_attached_memdb() {
33292 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
33293
33294 let pager = MemoryMockMvccPager;
33295 let cx = Cx::new();
33296 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
33297 let orders_root = 256;
33298 let customers_root = 257;
33299
33300 let mut b = ProgramBuilder::new();
33301 let end = b.emit_label();
33302 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33303 b.emit_op(Opcode::OpenWrite, 0, orders_root, 0, P4::Int(2), 0);
33304 b.emit_op(Opcode::OpenWrite, 1, customers_root, 0, P4::Int(2), 0);
33305
33306 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33307 b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33308 b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("ann".to_owned()), 0);
33309 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33310 b.emit_op(Opcode::Insert, 1, 4, 1, P4::None, 0);
33311
33312 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
33313 b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
33314 b.emit_op(Opcode::String8, 0, 3, 0, P4::Str("bob".to_owned()), 0);
33315 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33316 b.emit_op(Opcode::Insert, 1, 4, 1, P4::None, 0);
33317
33318 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
33319 b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33320 b.emit_op(Opcode::Integer, 10, 3, 0, P4::None, 0);
33321 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33322 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33323
33324 b.emit_op(Opcode::Integer, 2, 1, 0, P4::None, 0);
33325 b.emit_op(Opcode::Integer, 2, 2, 0, P4::None, 0);
33326 b.emit_op(Opcode::Integer, 20, 3, 0, P4::None, 0);
33327 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33328 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33329
33330 b.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
33331 b.emit_op(Opcode::Integer, 1, 2, 0, P4::None, 0);
33332 b.emit_op(Opcode::Integer, 30, 3, 0, P4::None, 0);
33333 b.emit_op(Opcode::MakeRecord, 2, 2, 4, P4::None, 0);
33334 b.emit_op(Opcode::Insert, 0, 4, 1, P4::None, 0);
33335
33336 let done = b.emit_label();
33337 let next_order = b.emit_label();
33338 b.emit_jump_to_label(Opcode::Rewind, 0, 0, done, P4::None, 0);
33339 let order_body = b.current_addr();
33340 b.emit_op(Opcode::Column, 0, 0, 10, P4::None, 0);
33341 b.emit_op(Opcode::Column, 0, 1, 11, P4::None, 0);
33342
33343 b.emit_jump_to_label(Opcode::Rewind, 1, 0, next_order, P4::None, 0);
33344 let customer_body = b.current_addr();
33345 b.emit_op(Opcode::Column, 1, 0, 12, P4::None, 0);
33346 let no_match = b.emit_label();
33347 b.emit_jump_to_label(Opcode::Ne, 10, 12, no_match, P4::None, 0);
33348 b.emit_op(Opcode::Column, 1, 1, 13, P4::None, 0);
33349 b.emit_op(Opcode::SCopy, 11, 14, 0, P4::None, 0);
33350 b.emit_op(Opcode::ResultRow, 13, 2, 0, P4::None, 0);
33351 b.resolve_label(no_match);
33352 b.emit_op(
33353 Opcode::Next,
33354 1,
33355 i32::try_from(customer_body).expect("test program address should fit in i32"),
33356 0,
33357 P4::None,
33358 0,
33359 );
33360
33361 b.resolve_label(next_order);
33362 b.emit_op(
33363 Opcode::Next,
33364 0,
33365 i32::try_from(order_body).expect("test program address should fit in i32"),
33366 0,
33367 P4::None,
33368 0,
33369 );
33370 b.resolve_label(done);
33371 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33372 b.resolve_label(end);
33373
33374 let prog = b.finish().expect("program should build");
33375 assert!(
33376 !prog.requires_attached_memdb(),
33377 "pager-backed nested-loop join bytecode must not require an attached MemDatabase"
33378 );
33379
33380 let mut engine = VdbeEngine::new(prog.register_count());
33381 engine.set_transaction(txn);
33382 engine.set_reject_mem_fallback(true);
33383
33384 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33385 assert_eq!(outcome, ExecOutcome::Done);
33386 assert!(
33387 engine.all_cursors_are_txn_backed(),
33388 "join scan cursors must stay on txn-backed storage cursors"
33389 );
33390 let rows: Vec<_> = engine
33391 .take_results()
33392 .into_iter()
33393 .map(|row| row.into_vec())
33394 .collect();
33395 assert_eq!(
33396 rows,
33397 vec![
33398 vec![SqliteValue::Text("ann".into()), SqliteValue::Integer(10)],
33399 vec![SqliteValue::Text("bob".into()), SqliteValue::Integer(20)],
33400 vec![SqliteValue::Text("ann".into()), SqliteValue::Integer(30)],
33401 ]
33402 );
33403 assert!(
33404 engine.take_database().is_none(),
33405 "the engine should not need an attached MemDatabase for join scans"
33406 );
33407 }
33408
33409 #[test]
33410 fn test_open_read_opcode_with_mem_fallback() {
33411 let mut db = MemDatabase::new();
33414 let root = db.create_table(1);
33415 let table = db.get_table_mut(root).expect("table should exist");
33416 table.insert(1, vec![SqliteValue::Integer(100)]);
33417
33418 let mut b = ProgramBuilder::new();
33419 let end = b.emit_label();
33420 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33421 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
33422 let halt_label = b.emit_label();
33424 b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt_label, P4::None, 0);
33425 b.emit_op(Opcode::Column, 0, 0, 1, P4::None, 0);
33427 b.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
33428 b.resolve_label(halt_label);
33429 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33430 b.resolve_label(end);
33431
33432 let prog = b.finish().expect("program should build");
33433 let mut engine = VdbeEngine::new(prog.register_count());
33434 engine.set_database(db);
33435 engine.set_reject_mem_fallback(false);
33437
33438 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33439 assert_eq!(outcome, ExecOutcome::Done);
33440
33441 let results: Vec<_> = engine
33442 .take_results()
33443 .into_iter()
33444 .map(|v| v.into_vec())
33445 .collect();
33446 assert_eq!(results.len(), 1);
33447 assert_eq!(results[0], vec![SqliteValue::Integer(100)]);
33448 }
33449
33450 #[test]
33451 fn test_open_write_insert_delete_cursor_lifecycle() {
33452 let mut db = MemDatabase::new();
33455 let root = db.create_table(1);
33456
33457 let mut b = ProgramBuilder::new();
33458 let end = b.emit_label();
33459 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33460 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33461
33462 b.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0); b.emit_op(Opcode::Integer, 42, 2, 0, P4::None, 0); b.emit_op(Opcode::MakeRecord, 2, 1, 3, P4::None, 0); b.emit_op(Opcode::Insert, 0, 3, 1, P4::None, 0);
33467
33468 let eof_label = b.emit_label();
33470 b.emit_jump_to_label(Opcode::Rewind, 0, 0, eof_label, P4::None, 0);
33471 b.emit_op(Opcode::Column, 0, 0, 4, P4::None, 0);
33472 b.emit_op(Opcode::ResultRow, 4, 1, 0, P4::None, 0);
33473 b.emit_op(Opcode::Delete, 0, 0, 0, P4::None, 0);
33475 b.resolve_label(eof_label);
33476
33477 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33478 b.resolve_label(end);
33479
33480 let prog = b.finish().expect("program should build");
33481 let mut engine = VdbeEngine::new(prog.register_count());
33482 engine.set_database(db);
33483 engine.set_reject_mem_fallback(false);
33485
33486 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
33487 assert_eq!(outcome, ExecOutcome::Done);
33488
33489 let results: Vec<_> = engine
33490 .take_results()
33491 .into_iter()
33492 .map(|v| v.into_vec())
33493 .collect();
33494 assert_eq!(results.len(), 1);
33495 assert_eq!(results[0], vec![SqliteValue::Integer(42)]);
33496 }
33497
33498 #[test]
33499 fn test_parity_cert_rejects_open_read_without_txn() {
33500 let mut db = MemDatabase::new();
33503 let root = db.create_table(1);
33504 let table = db.get_table_mut(root).expect("table should exist");
33505 table.insert(1, vec![SqliteValue::Integer(1)]);
33506
33507 let mut b = ProgramBuilder::new();
33508 let end = b.emit_label();
33509 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33510 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
33511 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33512 b.resolve_label(end);
33513
33514 let prog = b.finish().expect("program should build");
33515 let mut engine = VdbeEngine::new(prog.register_count());
33516 engine.set_database(db);
33517 engine.set_reject_mem_fallback(true);
33518
33519 let result = run_async(engine.execute(&prog));
33520 assert!(
33521 result.is_err(),
33522 "OpenRead should fail in parity-cert mode without txn"
33523 );
33524 }
33525
33526 #[test]
33527 fn test_parity_cert_rejects_open_write_without_txn() {
33528 let mut db = MemDatabase::new();
33530 let root = db.create_table(1);
33531
33532 let mut b = ProgramBuilder::new();
33533 let end = b.emit_label();
33534 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33535 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
33536 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33537 b.resolve_label(end);
33538
33539 let prog = b.finish().expect("program should build");
33540 let mut engine = VdbeEngine::new(prog.register_count());
33541 engine.set_database(db);
33542 engine.set_reject_mem_fallback(true);
33543
33544 let result = run_async(engine.execute(&prog));
33545 assert!(
33546 result.is_err(),
33547 "OpenWrite should fail in parity-cert mode without txn"
33548 );
33549 }
33550
33551 #[test]
33554 fn test_cursor_backend_kind_mem() {
33555 let mut db = MemDatabase::new();
33556 let root = db.create_table(1);
33557
33558 let mut engine = VdbeEngine::new(8);
33559 engine.set_database(db);
33560 engine.set_reject_mem_fallback(false);
33561
33562 assert!(run_async(engine.open_storage_cursor(0, root, false)));
33563 assert!(
33564 engine.has_mem_cursor(),
33565 "cursor should be mem-backed without txn"
33566 );
33567 assert!(!engine.all_cursors_are_txn_backed());
33568 }
33569
33570 #[test]
33571 fn test_cursor_backend_kind_txn() {
33572 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33573 let pager = MockMvccPager;
33574 let cx = Cx::new();
33575 let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33576
33577 let mut engine = VdbeEngine::new(8);
33578 engine.set_database(MemDatabase::new());
33579 engine.set_transaction(txn);
33580
33581 assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33583 assert!(
33584 engine.all_cursors_are_txn_backed(),
33585 "cursor should be txn-backed with pager transaction"
33586 );
33587 assert!(!engine.has_mem_cursor());
33588 }
33589
33590 #[test]
33591 fn test_validate_parity_cert_invariant_no_cursors() {
33592 let mut engine = VdbeEngine::new(8);
33593 engine.set_reject_mem_fallback(true);
33594 assert!(
33595 engine.validate_parity_cert_invariant().is_ok(),
33596 "vacuously valid with no cursors"
33597 );
33598 }
33599
33600 #[test]
33601 fn test_validate_parity_cert_invariant_with_txn_cursor() {
33602 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33603 let pager = MockMvccPager;
33604 let cx = Cx::new();
33605 let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33606
33607 let mut engine = VdbeEngine::new(8);
33608 engine.set_database(MemDatabase::new());
33609 engine.set_transaction(txn);
33610 engine.set_reject_mem_fallback(true);
33611
33612 assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33613 assert!(
33614 engine.validate_parity_cert_invariant().is_ok(),
33615 "txn-backed cursor satisfies parity-cert invariant"
33616 );
33617 }
33618
33619 #[test]
33620 fn test_validate_parity_cert_invariant_disabled_allows_mem() {
33621 let mut db = MemDatabase::new();
33622 let root = db.create_table(1);
33623
33624 let mut engine = VdbeEngine::new(8);
33625 engine.set_database(db);
33626 engine.set_reject_mem_fallback(false);
33628 assert!(run_async(engine.open_storage_cursor(0, root, false)));
33629 assert!(
33630 engine.validate_parity_cert_invariant().is_ok(),
33631 "parity-cert disabled should always pass"
33632 );
33633 }
33634
33635 #[test]
33636 fn test_all_cursors_txn_backed_vacuous() {
33637 let engine = VdbeEngine::new(8);
33638 assert!(
33639 engine.all_cursors_are_txn_backed(),
33640 "no cursors → vacuously true"
33641 );
33642 assert!(!engine.has_mem_cursor(), "no cursors → no mem cursor");
33643 }
33644
33645 #[test]
33646 fn test_cursor_kind_str_values() {
33647 let mut db = MemDatabase::new();
33648 let root = db.create_table(1);
33649
33650 let mut engine = VdbeEngine::new(8);
33651 engine.set_database(db);
33652 engine.set_reject_mem_fallback(false);
33653 run_async(engine.open_storage_cursor(0, root, false));
33654
33655 let sc = engine.storage_cursors.get(&0).unwrap();
33656 assert_eq!(sc.cursor.kind_str(), "mem");
33657 }
33658
33659 #[test]
33660 fn test_ratchet_prevents_mem_cursor_creation_in_parity_mode() {
33661 let mut db = MemDatabase::new();
33665 let root = db.create_table(1);
33666 let table = db.get_table_mut(root).unwrap();
33667 table.insert(1, vec![SqliteValue::Integer(99)]);
33668
33669 let mut engine = VdbeEngine::new(8);
33670 engine.set_database(db);
33671 engine.set_reject_mem_fallback(true);
33672
33673 let opened = run_async(engine.open_storage_cursor(0, root, false));
33675 assert!(
33676 !opened,
33677 "ratchet must prevent cursor creation in parity-cert mode"
33678 );
33679
33680 assert!(engine.validate_parity_cert_invariant().is_ok());
33682 assert!(!engine.has_mem_cursor());
33683 }
33684
33685 #[test]
33686 fn test_ratchet_allows_txn_cursor_in_parity_mode() {
33687 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33688 let pager = MockMvccPager;
33689 let cx = Cx::new();
33690 let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33691
33692 let mut engine = VdbeEngine::new(8);
33693 engine.set_database(MemDatabase::new());
33694 engine.set_transaction(txn);
33695 engine.set_reject_mem_fallback(true);
33696
33697 let opened = run_async(engine.open_storage_cursor(0, 1, false));
33699 assert!(opened, "txn-backed cursor should work in parity-cert mode");
33700 assert!(engine.all_cursors_are_txn_backed());
33701 assert!(engine.validate_parity_cert_invariant().is_ok());
33702 }
33703
33704 #[test]
33705 fn test_ratchet_multiple_cursors_mixed_rejection() {
33706 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
33707 let pager = MockMvccPager;
33708 let cx = Cx::new();
33709 let txn = run_async(pager.begin(&cx, TransactionMode::Deferred)).unwrap();
33710
33711 let mut engine = VdbeEngine::new(8);
33712 engine.set_database(MemDatabase::new());
33713 engine.set_transaction(txn);
33714 engine.set_reject_mem_fallback(true);
33715
33716 assert!(run_async(engine.open_storage_cursor(0, 1, false)));
33718 assert!(engine.all_cursors_are_txn_backed());
33719
33720 assert!(run_async(engine.open_storage_cursor(1, 1, false)));
33723 assert!(engine.all_cursors_are_txn_backed());
33724 assert!(engine.validate_parity_cert_invariant().is_ok());
33725 }
33726
33727 #[test]
33730 fn test_rowset_add_and_read_returns_sorted() {
33731 let rows = run_program(|b| {
33733 let end = b.emit_label();
33734 let exhausted = b.emit_label();
33735 let loop_start = b.emit_label();
33736 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33737
33738 let r_val = b.alloc_reg();
33739 let r_out = b.alloc_reg();
33740 let rowset_reg = b.alloc_reg();
33741
33742 for v in [30, 10, 20] {
33744 b.emit_op(Opcode::Integer, v, r_val, 0, P4::None, 0);
33745 b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33746 }
33747
33748 b.resolve_label(loop_start);
33750 b.emit_jump_to_label(
33751 Opcode::RowSetRead,
33752 rowset_reg,
33753 r_out,
33754 exhausted,
33755 P4::None,
33756 0,
33757 );
33758 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33759 b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33760
33761 b.resolve_label(exhausted);
33762 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33763 b.resolve_label(end);
33764 });
33765
33766 let vals: Vec<i64> = rows.into_iter().map(|row| row[0].to_integer()).collect();
33767 assert_eq!(vals, vec![10, 20, 30]);
33768 }
33769
33770 #[test]
33771 fn test_rowset_deduplicates() {
33772 let rows = run_program(|b| {
33774 let end = b.emit_label();
33775 let exhausted = b.emit_label();
33776 let loop_start = b.emit_label();
33777 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33778
33779 let r_val = b.alloc_reg();
33780 let r_out = b.alloc_reg();
33781 let rowset_reg = b.alloc_reg();
33782
33783 for v in [5, 5, 5] {
33784 b.emit_op(Opcode::Integer, v, r_val, 0, P4::None, 0);
33785 b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33786 }
33787
33788 b.resolve_label(loop_start);
33789 b.emit_jump_to_label(
33790 Opcode::RowSetRead,
33791 rowset_reg,
33792 r_out,
33793 exhausted,
33794 P4::None,
33795 0,
33796 );
33797 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33798 b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33799
33800 b.resolve_label(exhausted);
33801 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33802 b.resolve_label(end);
33803 });
33804
33805 assert_eq!(rows.len(), 1);
33806 assert_eq!(rows[0][0].to_integer(), 5);
33807 }
33808
33809 #[test]
33810 fn test_rowset_test_jumps_if_found() {
33811 let rows = run_program(|b| {
33813 let end = b.emit_label();
33814 let found = b.emit_label();
33815 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33816
33817 let r_val = b.alloc_reg();
33818 let r_out = b.alloc_reg();
33819 let rowset_reg = b.alloc_reg();
33820
33821 b.emit_op(Opcode::Integer, 42, r_val, 0, P4::None, 0);
33823 b.emit_op(Opcode::RowSetAdd, rowset_reg, r_val, 0, P4::None, 0);
33824
33825 b.emit_jump_to_label(Opcode::RowSetTest, rowset_reg, r_val, found, P4::None, 0);
33828 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
33830 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33831 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33832
33833 b.resolve_label(found);
33835 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
33836 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33837 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33838 b.resolve_label(end);
33839 });
33840
33841 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33842 }
33843
33844 #[test]
33845 fn test_rowset_test_falls_through_and_adds_if_not_found() {
33846 let rows = run_program(|b| {
33848 let end = b.emit_label();
33849 let found = b.emit_label();
33850 let exhausted = b.emit_label();
33851 let loop_start = b.emit_label();
33852 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33853
33854 let r_val = b.alloc_reg();
33855 let r_out = b.alloc_reg();
33856 let rowset_reg = b.alloc_reg();
33857
33858 b.emit_op(Opcode::Integer, 99, r_val, 0, P4::None, 0);
33860 b.emit_jump_to_label(Opcode::RowSetTest, rowset_reg, r_val, found, P4::None, 0);
33861
33862 b.resolve_label(loop_start);
33864 b.emit_jump_to_label(
33865 Opcode::RowSetRead,
33866 rowset_reg,
33867 r_out,
33868 exhausted,
33869 P4::None,
33870 0,
33871 );
33872 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33873 b.emit_jump_to_label(Opcode::Goto, 0, 0, loop_start, P4::None, 0);
33874
33875 b.resolve_label(found);
33876 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33877 b.resolve_label(exhausted);
33878 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33879 b.resolve_label(end);
33880 });
33881
33882 assert_eq!(rows.len(), 1);
33883 assert_eq!(rows[0][0].to_integer(), 99);
33884 }
33885
33886 #[test]
33889 fn test_fk_counter_and_fk_if_zero() {
33890 let rows = run_program(|b| {
33892 let end = b.emit_label();
33893 let is_zero = b.emit_label();
33894 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33895
33896 let r_out = b.alloc_reg();
33897
33898 b.emit_op(Opcode::FkCounter, 0, 3, 0, P4::None, 0);
33900 b.emit_jump_to_label(Opcode::FkIfZero, 0, 0, is_zero, P4::None, 0);
33902 b.emit_op(Opcode::FkCounter, 0, -3, 0, P4::None, 0);
33904 b.emit_jump_to_label(Opcode::FkIfZero, 0, 0, is_zero, P4::None, 0);
33906 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
33908 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33909 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33910
33911 b.resolve_label(is_zero);
33912 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
33913 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
33914 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33915 b.resolve_label(end);
33916 });
33917
33918 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
33919 }
33920
33921 #[test]
33924 fn test_memmax_stores_larger_value() {
33925 let rows = run_program(|b| {
33926 let end = b.emit_label();
33927 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33928
33929 let r1 = b.alloc_reg();
33930 let r2 = b.alloc_reg();
33931
33932 b.emit_op(Opcode::Integer, 50, r1, 0, P4::None, 0);
33934 b.emit_op(Opcode::Integer, 30, r2, 0, P4::None, 0);
33935 b.emit_op(Opcode::MemMax, r1, r2, 0, P4::None, 0);
33936 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
33937
33938 b.emit_op(Opcode::Integer, 10, r1, 0, P4::None, 0);
33941 b.emit_op(Opcode::Integer, 50, r2, 0, P4::None, 0);
33942 b.emit_op(Opcode::MemMax, r1, r2, 0, P4::None, 0);
33943 b.emit_op(Opcode::ResultRow, r2, 1, 0, P4::None, 0);
33944
33945 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33946 b.resolve_label(end);
33947 });
33948
33949 assert_eq!(
33950 rows,
33951 vec![
33952 vec![SqliteValue::Integer(50)],
33953 vec![SqliteValue::Integer(50)],
33954 ]
33955 );
33956 }
33957
33958 #[test]
33961 fn test_offset_limit_combines_values() {
33962 let rows = run_program(|b| {
33963 let end = b.emit_label();
33964 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
33965
33966 let r_limit = b.alloc_reg();
33967 let r_offset = b.alloc_reg();
33968 let r_combined = b.alloc_reg();
33969
33970 b.emit_op(Opcode::Integer, 10, r_limit, 0, P4::None, 0);
33972 b.emit_op(Opcode::Integer, 5, r_offset, 0, P4::None, 0);
33973 b.emit_op(
33974 Opcode::OffsetLimit,
33975 r_limit,
33976 r_offset,
33977 r_combined,
33978 P4::None,
33979 0,
33980 );
33981 b.emit_op(Opcode::ResultRow, r_combined, 1, 0, P4::None, 0);
33982
33983 b.emit_op(Opcode::Integer, -1, r_limit, 0, P4::None, 0);
33985 b.emit_op(
33986 Opcode::OffsetLimit,
33987 r_limit,
33988 r_offset,
33989 r_combined,
33990 P4::None,
33991 0,
33992 );
33993 b.emit_op(Opcode::ResultRow, r_combined, 1, 0, P4::None, 0);
33994
33995 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
33996 b.resolve_label(end);
33997 });
33998
33999 assert_eq!(
34000 rows,
34001 vec![
34002 vec![SqliteValue::Integer(15)],
34003 vec![SqliteValue::Integer(-1)],
34004 ]
34005 );
34006 }
34007
34008 #[test]
34011 fn test_decr_jump_zero_via_hot_path() {
34012 let rows = run_program(|b| {
34030 let end = b.emit_label();
34031 let skip1 = b.emit_label();
34032 let skip2 = b.emit_label();
34033 let skip3 = b.emit_label();
34034 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34035
34036 let r_counter = b.alloc_reg();
34037 let r_hit = b.alloc_reg();
34038 b.emit_op(Opcode::Integer, 2, r_counter, 0, P4::None, 0);
34039 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34040
34041 b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip1, P4::None, 0);
34042 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34043 b.resolve_label(skip1);
34044 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34045
34046 b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip2, P4::None, 0);
34047 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34048 b.resolve_label(skip2);
34049 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34050
34051 b.emit_jump_to_label(Opcode::DecrJumpZero, r_counter, 0, skip3, P4::None, 0);
34052 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34053 b.resolve_label(skip3);
34054 b.emit_op(Opcode::ResultRow, r_counter, 1, 0, P4::None, 0);
34055
34056 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34057 b.resolve_label(end);
34058 });
34059
34060 assert_eq!(
34061 rows,
34062 vec![
34063 vec![SqliteValue::Integer(1)],
34064 vec![SqliteValue::Null],
34065 vec![SqliteValue::Integer(0)],
34066 ]
34067 );
34068 }
34069
34070 #[test]
34073 fn test_if_pos_via_hot_path() {
34074 let rows = run_program(|b| {
34091 let end = b.emit_label();
34092 let skip1 = b.emit_label();
34093 let skip2 = b.emit_label();
34094 let skip3 = b.emit_label();
34095 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34096
34097 let r_offset = b.alloc_reg();
34098 let r_hit = b.alloc_reg();
34099 b.emit_op(Opcode::Integer, 2, r_offset, 0, P4::None, 0);
34100 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34101
34102 b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip1, P4::None, 0);
34104 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34105 b.resolve_label(skip1);
34106 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34107
34108 b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip2, P4::None, 0);
34109 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34110 b.resolve_label(skip2);
34111 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34112
34113 b.emit_jump_to_label(Opcode::IfPos, r_offset, 1, skip3, P4::None, 0);
34114 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34115 b.resolve_label(skip3);
34116 b.emit_op(Opcode::ResultRow, r_offset, 1, 0, P4::None, 0);
34117
34118 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34119 b.resolve_label(end);
34120 });
34121
34122 assert_eq!(
34123 rows,
34124 vec![
34125 vec![SqliteValue::Integer(0)],
34126 vec![SqliteValue::Null],
34127 vec![SqliteValue::Integer(0)],
34128 ]
34129 );
34130 }
34131
34132 #[test]
34135 fn test_is_null_via_hot_path() {
34136 let rows = run_program(|b| {
34156 let end = b.emit_label();
34157 let skip1 = b.emit_label();
34158 let skip2 = b.emit_label();
34159 let skip3 = b.emit_label();
34160 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34161
34162 let r_null = b.alloc_reg();
34163 let r_int = b.alloc_reg();
34164 let r_hit = b.alloc_reg();
34165 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34169 b.emit_op(Opcode::Integer, 7, r_int, 0, P4::None, 0);
34170 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34171
34172 b.emit_jump_to_label(Opcode::IsNull, r_null, 0, skip1, P4::None, 0);
34173 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34174 b.resolve_label(skip1);
34175 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34176
34177 b.emit_jump_to_label(Opcode::IsNull, r_int, 0, skip2, P4::None, 0);
34178 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34179 b.resolve_label(skip2);
34180 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34181
34182 b.emit_jump_to_label(Opcode::IsNull, r_null, 0, skip3, P4::None, 0);
34183 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34184 b.resolve_label(skip3);
34185 b.emit_op(Opcode::ResultRow, r_int, 1, 0, P4::None, 0);
34186
34187 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34188 b.resolve_label(end);
34189 });
34190
34191 assert_eq!(
34192 rows,
34193 vec![
34194 vec![SqliteValue::Integer(0)],
34195 vec![SqliteValue::Integer(1)],
34196 vec![SqliteValue::Integer(7)],
34197 ]
34198 );
34199 }
34200
34201 #[test]
34202 fn test_not_null_main_dispatch() {
34203 let rows = run_program(|b| {
34208 let end = b.emit_label();
34209 let skip1 = b.emit_label();
34210 let skip2 = b.emit_label();
34211 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34212
34213 let r_null = b.alloc_reg();
34214 let r_int = b.alloc_reg();
34215 let r_hit = b.alloc_reg();
34216 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34217 b.emit_op(Opcode::Integer, 7, r_int, 0, P4::None, 0);
34218 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34219
34220 b.emit_jump_to_label(Opcode::NotNull, r_null, 0, skip1, P4::None, 0);
34221 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34222 b.resolve_label(skip1);
34223
34224 b.emit_jump_to_label(Opcode::NotNull, r_int, 0, skip2, P4::None, 0);
34225 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34226 b.resolve_label(skip2);
34227
34228 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34229 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34230 b.resolve_label(end);
34231 });
34232
34233 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34234 }
34235
34236 #[test]
34237 fn test_if_not_via_hot_path() {
34238 let rows = run_program(|b| {
34263 let end = b.emit_label();
34264 let skip1 = b.emit_label();
34265 let skip2 = b.emit_label();
34266 let skip3 = b.emit_label();
34267 let skip4 = b.emit_label();
34268 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34269
34270 let r_truthy = b.alloc_reg();
34271 let r_falsy = b.alloc_reg();
34272 let r_null = b.alloc_reg();
34273 let r_hit = b.alloc_reg();
34274 b.emit_op(Opcode::Integer, 1, r_truthy, 0, P4::None, 0);
34275 b.emit_op(Opcode::Integer, 0, r_falsy, 0, P4::None, 0);
34276 b.emit_op(Opcode::Null, 0, r_null, 0, P4::None, 0);
34277
34278 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34280 b.emit_jump_to_label(Opcode::IfNot, r_truthy, 0, skip1, P4::None, 0);
34281 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34282 b.resolve_label(skip1);
34283 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34284
34285 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34287 b.emit_jump_to_label(Opcode::IfNot, r_falsy, 0, skip2, P4::None, 0);
34288 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34289 b.resolve_label(skip2);
34290 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34291
34292 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34294 b.emit_jump_to_label(Opcode::IfNot, r_null, 0, skip3, P4::None, 0);
34295 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34296 b.resolve_label(skip3);
34297 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34298
34299 b.emit_op(Opcode::Integer, 0, r_hit, 0, P4::None, 0);
34301 b.emit_jump_to_label(Opcode::IfNot, r_null, 1, skip4, P4::None, 0);
34302 b.emit_op(Opcode::AddImm, r_hit, 1, 0, P4::None, 0);
34303 b.resolve_label(skip4);
34304 b.emit_op(Opcode::ResultRow, r_hit, 1, 0, P4::None, 0);
34305
34306 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34307 b.resolve_label(end);
34308 });
34309
34310 assert_eq!(
34311 rows,
34312 vec![
34313 vec![SqliteValue::Integer(1)],
34314 vec![SqliteValue::Integer(0)],
34315 vec![SqliteValue::Integer(1)],
34316 vec![SqliteValue::Integer(0)],
34317 ]
34318 );
34319 }
34320
34321 #[test]
34324 fn test_rowid_via_hot_path() {
34325 let mut db = MemDatabase::new();
34342 let root = db.create_table(1);
34343 let table = db.get_table_mut(root).unwrap();
34344 table.insert(7, vec![SqliteValue::Integer(70)]);
34345 table.insert(11, vec![SqliteValue::Integer(110)]);
34346
34347 let (rows, _) = run_write_with_storage_cursors(db, |b| {
34348 let halt = b.emit_label();
34349
34350 let r_unopened = b.alloc_reg();
34355 b.emit_op(Opcode::Rowid, 99, r_unopened, 0, P4::None, 0);
34356 b.emit_op(Opcode::ResultRow, r_unopened, 1, 0, P4::None, 0);
34357
34358 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
34361 b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt, P4::None, 0);
34362
34363 let body = b.current_addr();
34366 let r_out = b.alloc_reg();
34367 b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
34368 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34369 let next_target =
34370 i32::try_from(body).expect("program counter should fit into i32 for tests");
34371 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
34372
34373 b.resolve_label(halt);
34374 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34375 });
34376
34377 assert_eq!(
34378 rows,
34379 vec![
34380 vec![SqliteValue::Null],
34381 vec![SqliteValue::Integer(7)],
34382 vec![SqliteValue::Integer(11)],
34383 ]
34384 );
34385 }
34386
34387 #[test]
34390 fn test_idx_rowid_via_hot_path() {
34391 let mut db = MemDatabase::new();
34398 let root = db.create_table(1);
34399 let table = db.get_table_mut(root).unwrap();
34400 table.insert(13, vec![SqliteValue::Integer(130)]);
34401 table.insert(17, vec![SqliteValue::Integer(170)]);
34402
34403 let (rows, _) = run_write_with_storage_cursors(db, |b| {
34404 let halt = b.emit_label();
34405
34406 let r_unopened = b.alloc_reg();
34408 b.emit_op(Opcode::IdxRowid, 99, r_unopened, 0, P4::None, 0);
34409 b.emit_op(Opcode::ResultRow, r_unopened, 1, 0, P4::None, 0);
34410
34411 b.emit_op(Opcode::OpenWrite, 0, root, 0, P4::Int(1), 0);
34412 b.emit_jump_to_label(Opcode::Rewind, 0, 0, halt, P4::None, 0);
34413
34414 let body = b.current_addr();
34415 let r_out = b.alloc_reg();
34416 b.emit_op(Opcode::IdxRowid, 0, r_out, 0, P4::None, 0);
34417 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34418 let next_target =
34419 i32::try_from(body).expect("program counter should fit into i32 for tests");
34420 b.emit_op(Opcode::Next, 0, next_target, 0, P4::None, 0);
34421
34422 b.resolve_label(halt);
34423 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34424 });
34425
34426 assert_eq!(
34427 rows,
34428 vec![
34429 vec![SqliteValue::Null],
34430 vec![SqliteValue::Integer(13)],
34431 vec![SqliteValue::Integer(17)],
34432 ]
34433 );
34434 }
34435
34436 #[test]
34439 fn test_if_not_zero_decrements_and_jumps() {
34440 let rows = run_program(|b| {
34442 let end = b.emit_label();
34443 let loop_start = b.emit_label();
34444 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34445
34446 let r_counter = b.alloc_reg();
34447 let r_count = b.alloc_reg();
34448
34449 b.emit_op(Opcode::Integer, 3, r_counter, 0, P4::None, 0);
34450 b.emit_op(Opcode::Integer, 0, r_count, 0, P4::None, 0);
34451
34452 b.resolve_label(loop_start);
34453 b.emit_op(Opcode::AddImm, r_count, 1, 0, P4::None, 0);
34455 b.emit_jump_to_label(Opcode::IfNotZero, r_counter, 0, loop_start, P4::None, 0);
34457
34458 b.emit_op(Opcode::ResultRow, r_count, 1, 0, P4::None, 0);
34460 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34461 b.resolve_label(end);
34462 });
34463
34464 assert_eq!(rows, vec![vec![SqliteValue::Integer(4)]]);
34468 }
34469
34470 #[test]
34473 fn test_pagecount_returns_table_count() {
34474 let rows = run_program(|b| {
34475 let end = b.emit_label();
34476 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34477
34478 let r_out = b.alloc_reg();
34479 b.emit_op(Opcode::Pagecount, 0, r_out, 0, P4::None, 0);
34480 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34481 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34482 b.resolve_label(end);
34483 });
34484
34485 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34487 }
34488
34489 #[test]
34490 fn test_max_pgcnt_returns_large_value() {
34491 let rows = run_program(|b| {
34492 let end = b.emit_label();
34493 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34494
34495 let r_out = b.alloc_reg();
34496 b.emit_op(Opcode::MaxPgcnt, 0, r_out, 0, P4::None, 0);
34497 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34498 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34499 b.resolve_label(end);
34500 });
34501
34502 assert_eq!(rows, vec![vec![SqliteValue::Integer(1_073_741_823)]]);
34503 }
34504
34505 #[test]
34506 fn test_journal_mode_returns_wal() {
34507 let rows = run_program(|b| {
34508 let end = b.emit_label();
34509 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34510
34511 let r_out = b.alloc_reg();
34512 b.emit_op(Opcode::JournalMode, 0, r_out, 0, P4::None, 0);
34513 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34514 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34515 b.resolve_label(end);
34516 });
34517
34518 assert_eq!(rows, vec![vec![SqliteValue::Text("wal".into())]]);
34519 }
34520
34521 #[test]
34522 fn test_integrity_ck_returns_ok() {
34523 let rows = run_program(|b| {
34524 let end = b.emit_label();
34525 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34526
34527 let r_root = b.alloc_reg();
34528 let r_out = b.alloc_reg();
34529 b.emit_op(Opcode::Integer, 1, r_root, 0, P4::None, 0);
34530 b.emit_op(Opcode::IntegrityCk, r_root, r_out, 1, P4::None, 0);
34531 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34532 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34533 b.resolve_label(end);
34534 });
34535
34536 assert_eq!(rows, vec![vec![SqliteValue::Text("ok".into())]]);
34537 }
34538
34539 #[test]
34542 fn test_vacuum_is_noop() {
34543 let rows = run_program(|b| {
34544 let end = b.emit_label();
34545 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34546
34547 let r_out = b.alloc_reg();
34548 b.emit_op(Opcode::Vacuum, 0, 0, 0, P4::None, 0);
34549 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34550 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34551 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34552 b.resolve_label(end);
34553 });
34554
34555 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34556 }
34557 #[derive(Debug, Clone, PartialEq)]
34558 struct RecordedFilterCall {
34559 idx_num: i32,
34560 idx_str: Option<String>,
34561 args: Vec<SqliteValue>,
34562 }
34563
34564 #[derive(Clone)]
34566 struct MockVtabCursor {
34567 rows: Vec<Vec<SqliteValue>>,
34568 pos: usize,
34569 filtered: bool,
34570 filter_capture: Option<Arc<std::sync::Mutex<Option<RecordedFilterCall>>>>,
34571 cancel_on_filter: bool,
34572 interrupt_on_filter: bool,
34573 cancel_on_column: Option<Cx>,
34574 }
34575
34576 impl MockVtabCursor {
34577 fn new(rows: Vec<Vec<SqliteValue>>) -> Self {
34578 Self {
34579 rows,
34580 pos: 0,
34581 filtered: false,
34582 filter_capture: None,
34583 cancel_on_filter: false,
34584 interrupt_on_filter: false,
34585 cancel_on_column: None,
34586 }
34587 }
34588
34589 fn with_filter_capture(
34590 rows: Vec<Vec<SqliteValue>>,
34591 filter_capture: Arc<std::sync::Mutex<Option<RecordedFilterCall>>>,
34592 ) -> Self {
34593 Self {
34594 rows,
34595 pos: 0,
34596 filtered: false,
34597 filter_capture: Some(filter_capture),
34598 cancel_on_filter: false,
34599 interrupt_on_filter: false,
34600 cancel_on_column: None,
34601 }
34602 }
34603
34604 fn with_filter_child_cancel(rows: Vec<Vec<SqliteValue>>) -> Self {
34605 Self {
34606 rows,
34607 pos: 0,
34608 filtered: false,
34609 filter_capture: None,
34610 cancel_on_filter: true,
34611 interrupt_on_filter: false,
34612 cancel_on_column: None,
34613 }
34614 }
34615
34616 fn with_filter_interrupt(rows: Vec<Vec<SqliteValue>>) -> Self {
34617 Self {
34618 rows,
34619 pos: 0,
34620 filtered: false,
34621 filter_capture: None,
34622 cancel_on_filter: false,
34623 interrupt_on_filter: true,
34624 cancel_on_column: None,
34625 }
34626 }
34627
34628 fn with_column_cancel(rows: Vec<Vec<SqliteValue>>, cancel_cx: Cx) -> Self {
34629 Self {
34630 rows,
34631 pos: 0,
34632 filtered: false,
34633 filter_capture: None,
34634 cancel_on_filter: false,
34635 interrupt_on_filter: false,
34636 cancel_on_column: Some(cancel_cx),
34637 }
34638 }
34639 }
34640
34641 impl VirtualTableCursor for MockVtabCursor {
34642 fn filter(
34643 &mut self,
34644 cx: &Cx,
34645 idx_num: i32,
34646 idx_str: Option<&str>,
34647 args: &[SqliteValue],
34648 ) -> Result<()> {
34649 if self.interrupt_on_filter {
34650 return Err(FrankenError::Abort);
34651 }
34652 if self.cancel_on_filter {
34653 cx.cancel();
34654 }
34655 if let Some(filter_capture) = &self.filter_capture {
34656 *filter_capture.lock().expect("filter capture lock poisoned") =
34657 Some(RecordedFilterCall {
34658 idx_num,
34659 idx_str: idx_str.map(str::to_owned),
34660 args: args.to_vec(),
34661 });
34662 }
34663 self.pos = 0;
34664 self.filtered = true;
34665 Ok(())
34666 }
34667 fn next(&mut self, _cx: &Cx) -> Result<()> {
34668 self.pos += 1;
34669 Ok(())
34670 }
34671 fn eof(&self) -> bool {
34672 self.pos >= self.rows.len()
34673 }
34674 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
34675 if let Some(cancel_cx) = &self.cancel_on_column {
34676 cancel_cx.cancel();
34677 }
34678 #[allow(clippy::cast_sign_loss)]
34679 let val = self
34680 .rows
34681 .get(self.pos)
34682 .and_then(|row| row.get(col as usize))
34683 .cloned()
34684 .unwrap_or(SqliteValue::Null);
34685 ctx.set_value(val);
34686 Ok(())
34687 }
34688 fn rowid(&self) -> Result<i64> {
34689 #[allow(clippy::cast_possible_wrap)]
34690 Ok(self.pos as i64 + 1)
34691 }
34692 }
34693
34694 struct MockVtab {
34695 cursor_template: MockVtabCursor,
34696 cancel_on_begin: bool,
34697 interrupt_on_begin: bool,
34698 }
34699
34700 impl MockVtab {
34701 fn new(cursor_template: MockVtabCursor) -> Self {
34702 Self {
34703 cursor_template,
34704 cancel_on_begin: false,
34705 interrupt_on_begin: false,
34706 }
34707 }
34708
34709 fn with_begin_child_cancel(cursor_template: MockVtabCursor) -> Self {
34710 Self {
34711 cursor_template,
34712 cancel_on_begin: true,
34713 interrupt_on_begin: false,
34714 }
34715 }
34716
34717 fn with_begin_interrupt(cursor_template: MockVtabCursor) -> Self {
34718 Self {
34719 cursor_template,
34720 cancel_on_begin: false,
34721 interrupt_on_begin: true,
34722 }
34723 }
34724 }
34725
34726 impl VirtualTable for MockVtab {
34727 type Cursor = MockVtabCursor;
34728
34729 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
34730 Ok(Self::new(MockVtabCursor::new(Vec::new())))
34731 }
34732
34733 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
34734 Ok(())
34735 }
34736
34737 fn open(&self) -> Result<Self::Cursor> {
34738 Ok(self.cursor_template.clone())
34739 }
34740
34741 fn begin(&mut self, cx: &Cx) -> Result<()> {
34742 if self.interrupt_on_begin {
34743 return Err(FrankenError::Abort);
34744 }
34745 if self.cancel_on_begin {
34746 cx.cancel();
34747 }
34748 Ok(())
34749 }
34750 }
34751
34752 fn run_vtab_program(
34754 cursor_id: i32,
34755 cursor: MockVtabCursor,
34756 build: impl FnOnce(&mut ProgramBuilder),
34757 ) -> (Vec<Vec<SqliteValue>>, ExecOutcome) {
34758 let mut b = ProgramBuilder::new();
34759 build(&mut b);
34760 let prog = b.finish().expect("program should build");
34761 let mut engine = VdbeEngine::new(prog.register_count());
34762 engine.register_vtab_instance(cursor_id, Box::new(MockVtab::new(cursor)));
34763 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
34764 (
34765 engine
34766 .take_results()
34767 .into_iter()
34768 .map(|v| v.into_vec())
34769 .collect(),
34770 outcome,
34771 )
34772 }
34773
34774 fn emit_vfilter_noargs(b: &mut ProgramBuilder, cursor_id: i32, jump_label: Label) {
34775 let r_idx_num = b.alloc_reg();
34776 let r_argc = b.alloc_reg();
34777 debug_assert_eq!(r_argc, r_idx_num + 1);
34778 b.emit_op(Opcode::Integer, 0, r_idx_num, 0, P4::None, 0);
34779 b.emit_op(Opcode::Integer, 0, r_argc, 0, P4::None, 0);
34780 b.emit_jump_to_label(
34781 Opcode::VFilter,
34782 cursor_id,
34783 r_idx_num,
34784 jump_label,
34785 P4::None,
34786 0,
34787 );
34788 }
34789
34790 #[test]
34791 fn test_vopen_is_noop() {
34792 let rows = run_program(|b| {
34793 let end = b.emit_label();
34794 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34795 let r_out = b.alloc_reg();
34796 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34797 b.emit_op(Opcode::Integer, 42, r_out, 0, P4::None, 0);
34798 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34799 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34800 b.resolve_label(end);
34801 });
34802 assert_eq!(rows, vec![vec![SqliteValue::Integer(42)]]);
34803 }
34804
34805 #[test]
34806 fn test_vcreate_is_noop() {
34807 let rows = run_program(|b| {
34808 let end = b.emit_label();
34809 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34810 let r_out = b.alloc_reg();
34811 b.emit_op(
34812 Opcode::VCreate,
34813 0,
34814 0,
34815 0,
34816 P4::Str("test_module".to_owned()),
34817 0,
34818 );
34819 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34820 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34821 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34822 b.resolve_label(end);
34823 });
34824 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34825 }
34826
34827 #[test]
34828 fn test_vdestroy_is_noop() {
34829 let rows = run_program(|b| {
34830 let end = b.emit_label();
34831 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34832 let r_out = b.alloc_reg();
34833 b.emit_op(
34834 Opcode::VDestroy,
34835 0,
34836 0,
34837 0,
34838 P4::Str("test_table".to_owned()),
34839 0,
34840 );
34841 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
34842 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34843 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34844 b.resolve_label(end);
34845 });
34846 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
34847 }
34848 #[test]
34849 fn test_vcheck_stores_null_in_p2_without_clobbering_p3_argument() {
34850 let rows = run_program(|b| {
34851 let end = b.emit_label();
34852 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34853 let r_out = b.alloc_reg();
34854 let r_arg = b.alloc_reg();
34855 b.emit_op(Opcode::Integer, 99, r_out, 0, P4::None, 0);
34856 b.emit_op(Opcode::Integer, 123, r_arg, 0, P4::None, 0);
34857 b.emit_op(Opcode::VCheck, 0, r_out, r_arg, P4::None, 0);
34858 b.emit_op(Opcode::ResultRow, r_out, 2, 0, P4::None, 0);
34859 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34860 b.resolve_label(end);
34861 });
34862 assert_eq!(
34863 rows,
34864 vec![vec![SqliteValue::Null, SqliteValue::Integer(123)]]
34865 );
34866 }
34867
34868 #[test]
34869 fn test_vupdate_stores_null_in_dest() {
34870 let rows = run_program(|b| {
34874 let end = b.emit_label();
34875 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34876 let r_arg = b.alloc_reg();
34877 let r_dest = b.alloc_reg();
34878 b.emit_op(Opcode::Integer, 77, r_arg, 0, P4::None, 0);
34879 b.emit_op(Opcode::Integer, 99, r_dest, 0, P4::None, 0);
34880 #[allow(clippy::cast_possible_truncation)]
34882 b.emit_op(Opcode::VUpdate, 0, 1, r_arg, P4::None, r_dest as u16);
34883 b.emit_op(Opcode::ResultRow, r_dest, 1, 0, P4::None, 0);
34884 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34885 b.resolve_label(end);
34886 });
34887 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
34888 }
34889
34890 #[test]
34891 fn test_vinitin_copies_register() {
34892 let rows = run_program(|b| {
34893 let end = b.emit_label();
34894 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34895 let r_src = b.alloc_reg();
34896 let r_dst = b.alloc_reg();
34897 b.emit_op(Opcode::Integer, 123, r_src, 0, P4::None, 0);
34898 b.emit_op(Opcode::VInitIn, 0, r_src, r_dst, P4::None, 0);
34899 b.emit_op(Opcode::ResultRow, r_dst, 1, 0, P4::None, 0);
34900 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34901 b.resolve_label(end);
34902 });
34903 assert_eq!(rows, vec![vec![SqliteValue::Integer(123)]]);
34904 }
34905
34906 #[test]
34907 fn test_vfilter_jumps_on_empty_cursor() {
34908 let empty_cursor = MockVtabCursor::new(vec![]);
34909 let (rows, outcome) = run_vtab_program(0, empty_cursor, |b| {
34910 let end = b.emit_label();
34911 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34912 let r_out = b.alloc_reg();
34913 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34914 let after_scan = b.emit_label();
34915 emit_vfilter_noargs(b, 0, after_scan);
34916 b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
34917 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34918 b.resolve_label(after_scan);
34919 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
34920 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34921 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34922 b.resolve_label(end);
34923 });
34924 assert_eq!(outcome, ExecOutcome::Done);
34925 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34926 }
34927
34928 #[test]
34929 fn test_vfilter_jumps_when_cursor_is_missing() {
34930 let rows = run_program(|b| {
34931 let end = b.emit_label();
34932 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34933 let r_out = b.alloc_reg();
34934 let after_scan = b.emit_label();
34935 emit_vfilter_noargs(b, 99, after_scan);
34936 b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
34937 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34938 b.resolve_label(after_scan);
34939 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
34940 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
34941 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34942 b.resolve_label(end);
34943 });
34944 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
34945 }
34946
34947 #[test]
34948 fn test_vfilter_reads_idx_num_idx_str_and_args_from_registers() {
34949 let capture = Arc::new(std::sync::Mutex::new(None));
34950 let cursor = MockVtabCursor::with_filter_capture(
34951 vec![vec![SqliteValue::Integer(1)]],
34952 Arc::clone(&capture),
34953 );
34954
34955 let mut b = ProgramBuilder::new();
34956 let end = b.emit_label();
34957 let done = b.emit_label();
34958 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
34959 let r_idx_num = b.alloc_reg();
34960 let r_argc = b.alloc_reg();
34961 let r_arg0 = b.alloc_reg();
34962 let r_arg1 = b.alloc_reg();
34963 debug_assert_eq!(r_argc, r_idx_num + 1);
34964 debug_assert_eq!(r_arg0, r_idx_num + 2);
34965 debug_assert_eq!(r_arg1, r_idx_num + 3);
34966 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
34967 b.emit_op(Opcode::Integer, 7, r_idx_num, 0, P4::None, 0);
34968 b.emit_op(Opcode::Integer, 2, r_argc, 0, P4::None, 0);
34969 b.emit_op(Opcode::Integer, 42, r_arg0, 0, P4::None, 0);
34970 b.emit_op(
34971 Opcode::String8,
34972 0,
34973 r_arg1,
34974 0,
34975 P4::Str("needle".to_owned()),
34976 0,
34977 );
34978 b.emit_jump_to_label(
34979 Opcode::VFilter,
34980 0,
34981 r_idx_num,
34982 done,
34983 P4::Str("range:eq".to_owned()),
34984 0,
34985 );
34986 b.resolve_label(done);
34987 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
34988 b.resolve_label(end);
34989 let prog = b.finish().expect("program should build");
34990
34991 let mut engine = VdbeEngine::new(prog.register_count());
34992 engine.register_vtab_instance(0, Box::new(MockVtab::new(cursor)));
34993 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
34994 assert_eq!(outcome, ExecOutcome::Done);
34995 assert_eq!(
34996 capture
34997 .lock()
34998 .expect("filter capture lock poisoned")
34999 .clone(),
35000 Some(RecordedFilterCall {
35001 idx_num: 7,
35002 idx_str: Some("range:eq".to_owned()),
35003 args: vec![SqliteValue::Integer(42), SqliteValue::Text("needle".into()),],
35004 })
35005 );
35006 }
35007
35008 #[test]
35009 fn test_execute_observes_execution_cx_cancellation_immediately_after_vfilter_opcode() {
35010 let root_cx = Cx::new();
35011
35012 let mut b = ProgramBuilder::new();
35013 let end = b.emit_label();
35014 let done = b.emit_label();
35015 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35016 let r_out = b.alloc_reg();
35017 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35018 emit_vfilter_noargs(&mut b, 0, done);
35019 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35020 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35021 b.resolve_label(done);
35022 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35023 b.resolve_label(end);
35024 let prog = b.finish().expect("program should build");
35025
35026 let mut engine =
35027 VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35028 let vtab = MockVtab::new(MockVtabCursor::with_filter_child_cancel(vec![vec![
35029 SqliteValue::Integer(7),
35030 ]]));
35031 engine.register_vtab_instance(0, Box::new(vtab));
35032
35033 let err = run_async(engine.execute(&prog))
35034 .expect_err("cancellation should be observed before VFilter advances execution");
35035 assert!(matches!(err, FrankenError::Abort));
35036 assert!(engine.take_results().is_empty());
35037 }
35038
35039 #[test]
35040 fn test_vfilter_propagates_interrupt_from_cursor() {
35041 let root_cx = Cx::new();
35042
35043 let mut b = ProgramBuilder::new();
35044 let end = b.emit_label();
35045 let done = b.emit_label();
35046 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35047 let r_out = b.alloc_reg();
35048 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35049 emit_vfilter_noargs(&mut b, 0, done);
35050 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35051 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35052 b.resolve_label(done);
35053 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35054 b.resolve_label(end);
35055 let prog = b.finish().expect("program should build");
35056
35057 let mut engine =
35058 VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35059 let vtab = MockVtab::new(MockVtabCursor::with_filter_interrupt(vec![vec![
35060 SqliteValue::Integer(7),
35061 ]]));
35062 engine.register_vtab_instance(0, Box::new(vtab));
35063
35064 let err = run_async(engine.execute(&prog))
35065 .expect_err("VFilter interrupt should propagate without being wrapped");
35066 assert!(matches!(err, FrankenError::Abort));
35067 assert!(engine.take_results().is_empty());
35068 }
35069
35070 #[test]
35071 fn test_vfilter_vcolumn_vnext_scan_loop() {
35072 let cursor = MockVtabCursor::new(vec![
35073 vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
35074 vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
35075 ]);
35076 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35077 let end = b.emit_label();
35078 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35079 let r1 = b.alloc_reg();
35080 let r2 = b.alloc_reg();
35081 let done_label = b.emit_label();
35082 let loop_label = b.emit_label();
35083 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35084 emit_vfilter_noargs(b, 0, done_label);
35085 b.resolve_label(loop_label);
35086 b.emit_op(Opcode::VColumn, 0, 0, r1, P4::None, 0);
35087 b.emit_op(Opcode::VColumn, 0, 1, r2, P4::None, 0);
35088 b.emit_op(Opcode::ResultRow, r1, 2, 0, P4::None, 0);
35089 b.emit_jump_to_label(Opcode::VNext, 0, 0, loop_label, P4::None, 0);
35090 b.resolve_label(done_label);
35091 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35092 b.resolve_label(end);
35093 });
35094 assert_eq!(outcome, ExecOutcome::Done);
35095 assert_eq!(
35096 rows,
35097 vec![
35098 vec![SqliteValue::Integer(10), SqliteValue::Text("a".into())],
35099 vec![SqliteValue::Integer(20), SqliteValue::Text("b".into())],
35100 ]
35101 );
35102 }
35103
35104 #[test]
35105 fn test_vnext_no_cursor_falls_through() {
35106 let rows = run_program(|b| {
35107 let end = b.emit_label();
35108 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35109 let r_out = b.alloc_reg();
35110 let loop_label = b.emit_label();
35111 b.resolve_label(loop_label);
35112 b.emit_jump_to_label(Opcode::VNext, 99, 0, loop_label, P4::None, 0);
35113 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35114 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35115 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35116 b.resolve_label(end);
35117 });
35118 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35119 }
35120
35121 #[test]
35122 fn test_vbegin_no_instance_is_noop() {
35123 let rows = run_program(|b| {
35124 let end = b.emit_label();
35125 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35126 let r_out = b.alloc_reg();
35127 b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35128 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35129 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35130 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35131 b.resolve_label(end);
35132 });
35133 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35134 }
35135
35136 #[test]
35137 fn test_vbegin_observes_child_cx_cancellation_immediately() {
35138 let root_cx = Cx::new();
35139
35140 let mut b = ProgramBuilder::new();
35141 let end = b.emit_label();
35142 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35143 let r_out = b.alloc_reg();
35144 b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35145 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35146 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35147 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35148 b.resolve_label(end);
35149 let prog = b.finish().expect("program should build");
35150
35151 let mut engine =
35152 VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35153 let vtab = MockVtab::with_begin_child_cancel(MockVtabCursor::new(Vec::new()));
35154 engine.register_vtab_instance(0, Box::new(vtab));
35155
35156 let err = run_async(engine.execute(&prog))
35157 .expect_err("VBegin child cancellation should abort execution immediately");
35158 assert!(matches!(err, FrankenError::Abort));
35159 assert!(engine.take_results().is_empty());
35160 }
35161
35162 #[test]
35163 fn test_vbegin_propagates_interrupt_from_vtab() {
35164 let root_cx = Cx::new();
35165
35166 let mut b = ProgramBuilder::new();
35167 let end = b.emit_label();
35168 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35169 let r_out = b.alloc_reg();
35170 b.emit_op(Opcode::VBegin, 0, 0, 0, P4::None, 0);
35171 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35172 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35173 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35174 b.resolve_label(end);
35175 let prog = b.finish().expect("program should build");
35176
35177 let mut engine =
35178 VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35179 let vtab = MockVtab::with_begin_interrupt(MockVtabCursor::new(Vec::new()));
35180 engine.register_vtab_instance(0, Box::new(vtab));
35181
35182 let err = run_async(engine.execute(&prog))
35183 .expect_err("VBegin interrupt should propagate without being wrapped");
35184 assert!(matches!(err, FrankenError::Abort));
35185 assert!(engine.take_results().is_empty());
35186 }
35187
35188 #[test]
35189 fn test_vrename_no_instance_is_noop() {
35190 let rows = run_program(|b| {
35191 let end = b.emit_label();
35192 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35193 let r_out = b.alloc_reg();
35194 b.emit_op(Opcode::VRename, 0, 0, 0, P4::Str("new_name".to_owned()), 0);
35195 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35196 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35197 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35198 b.resolve_label(end);
35199 });
35200 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35201 }
35202
35203 #[test]
35204 fn test_vcolumn_no_cursor_stores_null() {
35205 let rows = run_program(|b| {
35206 let end = b.emit_label();
35207 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35208 let r_out = b.alloc_reg();
35209 b.emit_op(Opcode::Integer, 55, r_out, 0, P4::None, 0);
35210 b.emit_op(Opcode::VColumn, 99, 0, r_out, P4::None, 0);
35211 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35212 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35213 b.resolve_label(end);
35214 });
35215 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
35216 }
35217
35218 #[test]
35219 fn test_rowid_reads_from_vtab_cursor() {
35220 let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35221 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35222 let end = b.emit_label();
35223 let done = b.emit_label();
35224 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35225 let r_out = b.alloc_reg();
35226 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35227 emit_vfilter_noargs(b, 0, done);
35228 b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
35229 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35230 b.resolve_label(done);
35231 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35232 b.resolve_label(end);
35233 });
35234 assert_eq!(outcome, ExecOutcome::Done);
35235 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35236 }
35237
35238 #[test]
35239 fn test_rowid_on_vtab_cursor_at_eof_stores_null() {
35240 let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35241 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35242 let end = b.emit_label();
35243 let done = b.emit_label();
35244 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35245 let r_out = b.alloc_reg();
35246 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35247 emit_vfilter_noargs(b, 0, done);
35248 b.emit_jump_to_label(Opcode::VNext, 0, 0, done, P4::None, 0);
35249 b.emit_op(Opcode::Rowid, 0, r_out, 0, P4::None, 0);
35250 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35251 b.resolve_label(done);
35252 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35253 b.resolve_label(end);
35254 });
35255 assert_eq!(outcome, ExecOutcome::Done);
35256 assert_eq!(rows, vec![vec![SqliteValue::Null]]);
35257 }
35258
35259 #[test]
35260 fn test_nullrow_on_vtab_cursor_forces_vcolumn_and_rowid_to_null() {
35261 let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35262 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35263 let end = b.emit_label();
35264 let done = b.emit_label();
35265 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35266 let r_col = b.alloc_reg();
35267 let r_rowid = b.alloc_reg();
35268 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35269 emit_vfilter_noargs(b, 0, done);
35270 b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35271 b.emit_op(Opcode::VColumn, 0, 0, r_col, P4::None, 0);
35272 b.emit_op(Opcode::Rowid, 0, r_rowid, 0, P4::None, 0);
35273 b.emit_op(Opcode::ResultRow, r_col, 2, 0, P4::None, 0);
35274 b.resolve_label(done);
35275 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35276 b.resolve_label(end);
35277 });
35278 assert_eq!(outcome, ExecOutcome::Done);
35279 assert_eq!(rows, vec![vec![SqliteValue::Null, SqliteValue::Null]]);
35280 }
35281
35282 #[test]
35283 fn test_ifnullrow_jumps_for_vtab_nullrow() {
35284 let cursor = MockVtabCursor::new(vec![vec![SqliteValue::Integer(10)]]);
35285 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35286 let end = b.emit_label();
35287 let skip = b.emit_label();
35288 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35289 let r_out = b.alloc_reg();
35290 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35291 emit_vfilter_noargs(b, 0, skip);
35292 b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35293 b.emit_jump_to_label(Opcode::IfNullRow, 0, 0, skip, P4::None, 0);
35294 b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
35295 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35296 b.resolve_label(skip);
35297 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0);
35298 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35299 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35300 b.resolve_label(end);
35301 });
35302 assert_eq!(outcome, ExecOutcome::Done);
35303 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
35304 }
35305
35306 #[test]
35307 fn test_vnext_on_vtab_nullrow_falls_through() {
35308 let cursor = MockVtabCursor::new(vec![
35309 vec![SqliteValue::Integer(10)],
35310 vec![SqliteValue::Integer(20)],
35311 ]);
35312 let (rows, outcome) = run_vtab_program(0, cursor, |b| {
35313 let end = b.emit_label();
35314 let loop_body = b.emit_label();
35315 let after_loop = b.emit_label();
35316 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35317 let r_out = b.alloc_reg();
35318 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35319 emit_vfilter_noargs(b, 0, after_loop);
35320 b.emit_op(Opcode::NullRow, 0, 0, 0, P4::None, 0);
35321 b.emit_jump_to_label(Opcode::VNext, 0, 0, loop_body, P4::None, 0);
35322 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
35323 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35324 b.emit_jump_to_label(Opcode::Goto, 0, 0, after_loop, P4::None, 0);
35325 b.resolve_label(loop_body);
35326 b.emit_op(Opcode::Integer, 999, r_out, 0, P4::None, 0);
35327 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35328 b.resolve_label(after_loop);
35329 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35330 b.resolve_label(end);
35331 });
35332 assert_eq!(outcome, ExecOutcome::Done);
35333 assert_eq!(rows, vec![vec![SqliteValue::Integer(1)]]);
35334 }
35335
35336 #[test]
35337 fn test_execute_observes_execution_cx_cancellation_immediately_after_vcolumn_opcode() {
35338 let root_cx = Cx::new();
35339
35340 let mut b = ProgramBuilder::new();
35341 let end = b.emit_label();
35342 let done = b.emit_label();
35343 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35344 let r_out = b.alloc_reg();
35345 b.emit_op(Opcode::VOpen, 0, 0, 0, P4::None, 0);
35346 emit_vfilter_noargs(&mut b, 0, done);
35347 b.emit_op(Opcode::VColumn, 0, 0, r_out, P4::None, 0);
35348 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
35349 b.resolve_label(done);
35350 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35351 b.resolve_label(end);
35352 let prog = b.finish().expect("program should build");
35353
35354 let mut engine =
35355 VdbeEngine::new_with_execution_cx(prog.register_count(), &root_cx, PageSize::DEFAULT);
35356 let vtab = MockVtab::new(MockVtabCursor::with_column_cancel(
35357 vec![vec![SqliteValue::Integer(7)]],
35358 root_cx.clone(),
35359 ));
35360 engine.register_vtab_instance(0, Box::new(vtab));
35361
35362 let err = run_async(engine.execute(&prog))
35363 .expect_err("cancellation should be observed before VColumn publishes a value");
35364 assert!(matches!(err, FrankenError::Abort));
35365 assert!(engine.take_results().is_empty());
35366 }
35367
35368 #[test]
35371 fn test_set_snapshot_stores_time_travel_marker() {
35372 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35377
35378 let pager = MockMvccPager;
35379 let cx = Cx::new();
35380 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35381
35382 let mut db = MemDatabase::new();
35383 let root = db.create_table(1);
35384 let table = db.get_table_mut(root).unwrap();
35385 table.insert(1, vec![SqliteValue::Integer(42)]);
35386
35387 let mut b = ProgramBuilder::new();
35388 let end = b.emit_label();
35389 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35390 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
35391 b.emit_op(Opcode::SetSnapshot, 0, 0, 0, P4::TimeTravelCommitSeq(5), 0);
35393 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35394 b.resolve_label(end);
35395 let prog = b.finish().expect("program should build");
35396
35397 let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35398
35399 let commit_log = {
35401 use fsqlite_types::TxnId;
35402 let mut log = CommitLog::new(CommitSeq::new(1));
35403 for seq in 1..=5 {
35404 log.append(fsqlite_mvcc::core_types::CommitRecord {
35405 txn_id: TxnId::new(seq).unwrap(),
35406 commit_seq: CommitSeq::new(seq),
35407 pages: smallvec::smallvec![PageNumber::new(1).unwrap()],
35408 timestamp_unix_ns: 1_700_000_000_000_000_000 + seq * 1_000_000_000,
35409 });
35410 }
35411 Arc::new(Mutex::new(log))
35412 };
35413
35414 let mut engine = VdbeEngine::new(prog.register_count());
35415 engine.set_database(db);
35416 engine.set_transaction(txn);
35417 engine.set_version_store(Arc::clone(&vs));
35418 engine.set_time_travel_commit_log(Arc::clone(&commit_log));
35419 engine.set_time_travel_gc_horizon(CommitSeq::new(1));
35420
35421 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
35422 assert_eq!(outcome, ExecOutcome::Done);
35423
35424 let marker = engine.time_travel_marker(0);
35426 assert!(
35427 marker.is_some(),
35428 "time-travel marker should be set on cursor 0"
35429 );
35430 assert!(
35431 matches!(marker, Some(TimeTravelMarker::CommitSeq(seq)) if *seq == 5),
35432 "expected CommitSeq marker"
35433 );
35434 }
35435
35436 #[test]
35437 fn test_set_snapshot_upgrades_txn_cursor_to_time_travel() {
35438 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35441
35442 let pager = MockMvccPager;
35443 let cx = Cx::new();
35444 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35445
35446 let mut db = MemDatabase::new();
35447 let root = db.create_table(1);
35448 let table = db.get_table_mut(root).unwrap();
35449 table.insert(1, vec![SqliteValue::Integer(99)]);
35450
35451 let mut b = ProgramBuilder::new();
35452 let end = b.emit_label();
35453 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
35454 b.emit_op(Opcode::OpenRead, 0, root, 0, P4::Int(1), 0);
35455 b.emit_op(Opcode::SetSnapshot, 0, 0, 0, P4::TimeTravelCommitSeq(3), 0);
35456 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
35457 b.resolve_label(end);
35458 let prog = b.finish().expect("program should build");
35459
35460 let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35461
35462 let commit_log = {
35464 use fsqlite_types::TxnId;
35465 let mut log = CommitLog::new(CommitSeq::new(1));
35466 for seq in 1..=5 {
35467 log.append(fsqlite_mvcc::core_types::CommitRecord {
35468 txn_id: TxnId::new(seq).unwrap(),
35469 commit_seq: CommitSeq::new(seq),
35470 pages: smallvec::smallvec![PageNumber::new(1).unwrap()],
35471 timestamp_unix_ns: 1_700_000_000_000_000_000 + seq * 1_000_000_000,
35472 });
35473 }
35474 Arc::new(Mutex::new(log))
35475 };
35476
35477 let mut engine = VdbeEngine::new(prog.register_count());
35478 engine.set_database(db);
35479 engine.set_transaction(txn);
35480 engine.set_version_store(Arc::clone(&vs));
35481 engine.set_time_travel_commit_log(Arc::clone(&commit_log));
35482 engine.set_time_travel_gc_horizon(CommitSeq::new(1));
35483
35484 let outcome = run_async(engine.execute(&prog)).expect("execution should succeed");
35485 assert_eq!(outcome, ExecOutcome::Done);
35486
35487 let sc = engine
35489 .storage_cursors
35490 .get(&0)
35491 .expect("cursor 0 should exist");
35492 assert!(
35493 sc.cursor.is_time_travel(),
35494 "cursor should be upgraded to TimeTravel backend"
35495 );
35496 assert!(!sc.writable, "time-travel cursor should be read-only");
35498 }
35499
35500 #[test]
35501 fn test_time_travel_page_io_empty_version_store_rejects_read() {
35502 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35510
35511 let pager = MockMvccPager;
35512 let cx = Cx::new();
35513 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35514 let inner_io = SharedTxnPageIo::new(txn);
35515
35516 let empty_vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35517
35518 let tt_snapshot =
35519 TimeTravelSnapshot::new_for_commit_seq(CommitSeq::new(5), SchemaEpoch::new(1));
35520
35521 let tt_page_io = TimeTravelPageIo {
35522 inner: inner_io,
35523 version_store: empty_vs,
35524 snapshot: tt_snapshot,
35525 };
35526
35527 let result = run_async(tt_page_io.read_page(&cx, PageNumber::new(1).unwrap()));
35530
35531 assert!(
35532 result.is_err(),
35533 "reading from TimeTravelPageIo with empty VersionStore \
35534 should return an error, not silently fall through"
35535 );
35536
35537 let err_msg = format!("{}", result.unwrap_err());
35538 assert!(
35539 err_msg.contains("historical data not available")
35540 || err_msg.contains("time-travel")
35541 || err_msg.contains("version store"),
35542 "expected error about missing historical data, got: {err_msg}"
35543 );
35544 }
35545
35546 #[test]
35547 fn test_time_travel_page_io_populated_store_falls_through_for_unchanged_page() {
35548 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35553 use fsqlite_types::{PageVersion, TxnEpoch, TxnId, TxnToken};
35554
35555 let pager = MockMvccPager;
35556 let cx = Cx::new();
35557 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35558 let inner_io = SharedTxnPageIo::new(txn);
35559
35560 let vs = Arc::new(VersionStore::new(fsqlite_types::PageSize::DEFAULT));
35561
35562 let version = PageVersion {
35565 pgno: PageNumber::new(1).unwrap(),
35566 commit_seq: CommitSeq::new(3),
35567 created_by: TxnToken::new(TxnId::new(3).unwrap(), TxnEpoch::new(1)),
35568 data: PageData::from_vec(vec![0xAA; 4096]),
35569 prev: None,
35570 };
35571 vs.publish(version);
35572
35573 let tt_snapshot =
35575 TimeTravelSnapshot::new_for_commit_seq(CommitSeq::new(5), SchemaEpoch::new(1));
35576
35577 let tt_page_io = TimeTravelPageIo {
35578 inner: inner_io,
35579 version_store: vs,
35580 snapshot: tt_snapshot,
35581 };
35582
35583 let result = run_async(tt_page_io.read_page(&cx, PageNumber::new(2).unwrap()));
35589
35590 if let Err(e) = &result {
35594 let msg = format!("{e}");
35595 assert!(
35596 !msg.contains("historical data not available"),
35597 "populated VersionStore should fall through for unknown pages, \
35598 not return 'historical data not available'"
35599 );
35600 }
35601 }
35602
35603 #[test]
35604 fn test_shared_txn_page_io_zero_busy_timeout_preserves_losing_writer_state() {
35605 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35606 use fsqlite_types::Snapshot;
35607
35608 let pager = MockMvccPager;
35609 let cx = Cx::new();
35610 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35611
35612 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35613 let lock_table = Arc::new(InProcessPageLockTable::new());
35614 let commit_index = Arc::new(CommitIndex::new());
35615 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35616 let contested_page = PageNumber::ONE;
35617 let page_bytes = vec![0xAB; PageSize::DEFAULT.as_usize()];
35618
35619 let (holder_session, writer_session, writer_handle) = {
35620 let mut guard = registry
35621 .lock()
35622 .unwrap_or_else(std::sync::PoisonError::into_inner);
35623 let holder_session = guard
35624 .begin_concurrent(snapshot)
35625 .expect("holder session should register");
35626 let writer_session = guard
35627 .begin_concurrent(snapshot)
35628 .expect("writer session should register");
35629
35630 let mut holder = guard
35631 .get_mut(holder_session)
35632 .expect("holder session must be present");
35633 concurrent_write_page(
35634 &mut holder,
35635 &lock_table,
35636 holder_session,
35637 contested_page,
35638 PageData::from_vec(page_bytes.clone()),
35639 )
35640 .expect("holder should acquire the contested page lock");
35641 let writer_handle = guard
35642 .handle(writer_session)
35643 .expect("writer session handle must be present");
35644 (holder_session, writer_session, writer_handle)
35645 };
35646
35647 let mut page_io = SharedTxnPageIo::with_concurrent(
35648 txn,
35649 writer_session,
35650 writer_handle,
35651 Arc::clone(&lock_table),
35652 Arc::clone(&commit_index),
35653 0,
35654 );
35655
35656 let err = run_async(page_io.write_page(&cx, contested_page, &page_bytes))
35657 .expect_err("losing writer should time out with SQLITE_BUSY");
35658 assert!(
35659 matches!(err, FrankenError::Busy),
35660 "expected SQLITE_BUSY on timed-out handoff, got {err}"
35661 );
35662
35663 let guard = registry
35664 .lock()
35665 .unwrap_or_else(std::sync::PoisonError::into_inner);
35666 {
35667 let holder = guard
35668 .get(holder_session)
35669 .expect("holder session should remain registered");
35670 assert!(
35671 holder.write_set().contains_key(&contested_page),
35672 "winning writer must retain the contested page in its write set"
35673 );
35674 }
35675 {
35676 let writer = guard
35677 .get(writer_session)
35678 .expect("losing writer session should remain registered");
35679 assert!(
35680 writer.write_set().is_empty(),
35681 "timed-out writer must not leak page data into its write set"
35682 );
35683 assert!(
35684 writer.held_locks().is_empty(),
35685 "timed-out writer must not retain the contested page lock"
35686 );
35687 }
35688 drop(guard);
35689
35690 assert_eq!(
35691 lock_table.total_lock_count(),
35692 1,
35693 "contested page lock must remain owned only by the winning writer"
35694 );
35695 }
35696
35697 #[test]
35698 fn test_shared_txn_page_io_revalidates_fcw_after_page_lock_acquisition() {
35699 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35700 use fsqlite_types::Snapshot;
35701
35702 let pager = MemoryMockMvccPager;
35703 let cx = Cx::new();
35704 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35705
35706 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35707 let lock_table = Arc::new(InProcessPageLockTable::new());
35708 let commit_index = Arc::new(CommitIndex::new());
35709 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35710 let target_page = PageNumber::new(97).expect("test page must be non-zero");
35711 let page_bytes = vec![0x5A; PageSize::DEFAULT.as_usize()];
35712
35713 let (session_id, handle) = {
35714 let mut guard = registry
35715 .lock()
35716 .unwrap_or_else(std::sync::PoisonError::into_inner);
35717 let session_id = guard
35718 .begin_concurrent(snapshot)
35719 .expect("writer session should register");
35720 let handle = guard
35721 .handle(session_id)
35722 .expect("writer session handle must be present");
35723 (session_id, handle)
35724 };
35725
35726 let hook_commit_index = Arc::clone(&commit_index);
35727 install_concurrent_page_lock_window_hook(move || {
35728 hook_commit_index.update(target_page, CommitSeq::new(8));
35729 });
35730
35731 let mut page_io = SharedTxnPageIo::with_concurrent(
35732 txn,
35733 session_id,
35734 handle,
35735 Arc::clone(&lock_table),
35736 Arc::clone(&commit_index),
35737 0,
35738 );
35739 let error = run_async(page_io.write_page(&cx, target_page, &page_bytes))
35740 .expect_err("a commit published in the precheck/acquire window must reject the writer");
35741 assert!(
35742 matches!(error, FrankenError::BusySnapshot { .. }),
35743 "expected post-acquire BusySnapshot, got {error}"
35744 );
35745 assert_eq!(
35746 lock_table.total_lock_count(),
35747 0,
35748 "post-acquire FCW rejection must release the stale writer's page lock"
35749 );
35750
35751 let guard = registry
35752 .lock()
35753 .unwrap_or_else(std::sync::PoisonError::into_inner);
35754 let writer = guard
35755 .get(session_id)
35756 .expect("writer session should remain registered for rollback");
35757 assert!(
35758 writer.write_set().is_empty(),
35759 "post-acquire FCW rejection must not stage a stale page marker"
35760 );
35761 assert!(
35762 writer.held_locks().is_empty(),
35763 "post-acquire FCW rejection must restore the prior page-lock state"
35764 );
35765 drop(writer);
35766 drop(guard);
35767 assert!(
35768 !page_io.txn.borrow().has_pending_writes(),
35769 "post-acquire FCW rejection must not reach the pager write set"
35770 );
35771 }
35772
35773 #[test]
35774 fn test_shared_txn_page_io_owned_lock_skips_busy_snapshot_after_savepoint_rollback() {
35775 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35776 use fsqlite_types::Snapshot;
35777
35778 let pager = MemoryMockMvccPager;
35779 let cx = Cx::new();
35780 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35781
35782 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35783 let lock_table = Arc::new(InProcessPageLockTable::new());
35784 let commit_index = Arc::new(CommitIndex::new());
35785 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35786 let target_page = PageNumber::new(2).unwrap();
35787 let first_bytes = vec![0xAB; PageSize::DEFAULT.as_usize()];
35788 let second_bytes = vec![0xCD; PageSize::DEFAULT.as_usize()];
35789
35790 let (session_id, handle) = {
35791 let mut guard = registry
35792 .lock()
35793 .unwrap_or_else(std::sync::PoisonError::into_inner);
35794 let session_id = guard
35795 .begin_concurrent(snapshot)
35796 .expect("session should register");
35797 let handle = guard
35798 .handle(session_id)
35799 .expect("session handle must be present");
35800 (session_id, handle)
35801 };
35802
35803 let savepoint = {
35804 let guard = handle.lock();
35805 fsqlite_mvcc::concurrent_savepoint(&guard, "sp1").unwrap()
35806 };
35807
35808 let mut page_io = SharedTxnPageIo::with_concurrent(
35809 txn,
35810 session_id,
35811 Arc::clone(&handle),
35812 Arc::clone(&lock_table),
35813 Arc::clone(&commit_index),
35814 0,
35815 );
35816
35817 run_async(page_io.write_page(&cx, target_page, &first_bytes))
35818 .expect("initial concurrent write should succeed");
35819
35820 {
35821 let mut guard = handle.lock();
35822 fsqlite_mvcc::concurrent_rollback_to_savepoint(
35823 &mut guard,
35824 &lock_table,
35825 session_id,
35826 &savepoint,
35827 )
35828 .unwrap();
35829 assert!(
35830 guard.holds_page_lock(target_page),
35831 "rollback-to-savepoint must preserve page ownership"
35832 );
35833 assert!(
35834 !guard.tracks_write_conflict_page(target_page),
35835 "rollback-to-savepoint should clear staged tracking for the rolled-back page"
35836 );
35837 }
35838
35839 commit_index.update(target_page, CommitSeq::new(8));
35840
35841 run_async(page_io.write_page(&cx, target_page, &second_bytes))
35842 .expect("already-owned page should bypass stale-snapshot rejection");
35843
35844 let read_back = run_async(page_io.read_page_data(&cx, target_page))
35845 .expect("pager must keep marker-backed read-your-writes data");
35846 assert_eq!(
35847 read_back.as_bytes(),
35848 second_bytes.as_slice(),
35849 "rewrite after savepoint rollback should remain readable through the pager"
35850 );
35851
35852 let guard = registry
35853 .lock()
35854 .unwrap_or_else(std::sync::PoisonError::into_inner);
35855 let writer = guard
35856 .get(session_id)
35857 .expect("writer session should remain registered");
35858 assert_eq!(
35859 concurrent_page_read_status(&writer, target_page),
35860 (false, true),
35861 "rewrite after savepoint rollback should restage the owned page in the MVCC surface"
35862 );
35863 assert!(
35864 concurrent_read_page(&writer, target_page).is_none(),
35865 "marker-backed VDBE writes keep payload bytes in the pager, not MVCC"
35866 );
35867 }
35868
35869 #[test]
35870 fn test_shared_txn_page_io_waits_for_page_lock_release_and_succeeds() {
35871 use fsqlite_mvcc::concurrent_abort;
35872 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
35873 use fsqlite_types::Snapshot;
35874
35875 let pager = MockMvccPager;
35876 let cx = Cx::new();
35877 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
35878
35879 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35880 let lock_table = Arc::new(InProcessPageLockTable::new());
35881 let commit_index = Arc::new(CommitIndex::new());
35882 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35883 let contested_page = PageNumber::ONE;
35884 let page_bytes = vec![0xCD; PageSize::DEFAULT.as_usize()];
35885
35886 let (holder_session, writer_session, writer_handle) = {
35887 let mut guard = registry
35888 .lock()
35889 .unwrap_or_else(std::sync::PoisonError::into_inner);
35890 let holder_session = guard
35891 .begin_concurrent(snapshot)
35892 .expect("holder session should register");
35893 let writer_session = guard
35894 .begin_concurrent(snapshot)
35895 .expect("writer session should register");
35896
35897 let mut holder = guard
35898 .get_mut(holder_session)
35899 .expect("holder session must be present");
35900 concurrent_write_page(
35901 &mut holder,
35902 &lock_table,
35903 holder_session,
35904 contested_page,
35905 PageData::from_vec(page_bytes.clone()),
35906 )
35907 .expect("holder should acquire the contested page lock");
35908 let writer_handle = guard
35909 .handle(writer_session)
35910 .expect("writer session handle must be present");
35911 (holder_session, writer_session, writer_handle)
35912 };
35913
35914 let release_registry = Arc::clone(®istry);
35915 let release_lock_table = Arc::clone(&lock_table);
35916 let releaser = std::thread::spawn(move || {
35917 std::thread::sleep(Duration::from_millis(20));
35918 let guard = release_registry
35919 .lock()
35920 .unwrap_or_else(std::sync::PoisonError::into_inner);
35921 let mut holder = guard
35922 .get_mut(holder_session)
35923 .expect("holder session must still be present before release");
35924 concurrent_abort(&mut holder, &release_lock_table, holder_session);
35925 });
35926
35927 let mut page_io = SharedTxnPageIo::with_concurrent(
35928 txn,
35929 writer_session,
35930 writer_handle,
35931 Arc::clone(&lock_table),
35932 Arc::clone(&commit_index),
35933 250,
35934 );
35935
35936 run_async(page_io.write_page(&cx, contested_page, &page_bytes))
35937 .expect("writer should wake and acquire the page after holder release");
35938 releaser
35939 .join()
35940 .expect("holder release helper thread must complete cleanly");
35941
35942 let guard = registry
35943 .lock()
35944 .unwrap_or_else(std::sync::PoisonError::into_inner);
35945 {
35946 let writer = guard
35947 .get(writer_session)
35948 .expect("writer session should remain registered");
35949 assert!(
35950 writer.write_set().contains_key(&contested_page),
35951 "woken writer must stage the contested page in its write set"
35952 );
35953 assert!(
35954 writer.held_locks().contains(&contested_page),
35955 "woken writer must own the contested page lock after retrying"
35956 );
35957 }
35958 drop(guard);
35959
35960 assert_eq!(
35961 lock_table.total_lock_count(),
35962 1,
35963 "after wake/retry exactly one writer must hold the contested page lock"
35964 );
35965 }
35966
35967 #[test]
35968 fn test_shared_txn_page_io_short_concurrent_write_preserves_page_size_on_read() {
35969 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
35970 use fsqlite_types::Snapshot;
35971
35972 let pager = MemoryMockMvccPager;
35973 let cx = Cx::new();
35974 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
35975
35976 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
35977 let lock_table = Arc::new(InProcessPageLockTable::new());
35978 let commit_index = Arc::new(CommitIndex::new());
35979 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
35980
35981 let (session_id, handle) = {
35982 let mut guard = registry
35983 .lock()
35984 .unwrap_or_else(std::sync::PoisonError::into_inner);
35985 let session_id = guard
35986 .begin_concurrent(snapshot)
35987 .expect("session should register");
35988 let handle = guard
35989 .handle(session_id)
35990 .expect("session handle must be present");
35991 (session_id, handle)
35992 };
35993
35994 let mut page_io = SharedTxnPageIo::with_concurrent(
35995 txn,
35996 session_id,
35997 handle,
35998 Arc::clone(&lock_table),
35999 Arc::clone(&commit_index),
36000 0,
36001 );
36002 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36003 let expected = vec![0xA5; 32];
36004
36005 run_async(page_io.write_page(&cx, page_no, &expected))
36006 .expect("short concurrent write should succeed");
36007
36008 let bytes = run_async(page_io.read_page(&cx, page_no))
36009 .expect("read-your-writes should return the normalized page image");
36010 assert_eq!(
36011 bytes.len(),
36012 PageSize::DEFAULT.as_usize(),
36013 "concurrent read-your-writes must preserve the pager page-size invariant"
36014 );
36015 assert_eq!(&bytes[..expected.len()], expected.as_slice());
36016 assert!(
36017 bytes[expected.len()..].iter().all(|byte| *byte == 0),
36018 "concurrent read-your-writes should zero-fill any unwritten tail bytes"
36019 );
36020 }
36021
36022 #[test]
36023 fn test_shared_txn_page_io_short_concurrent_owned_write_preserves_page_size_on_read() {
36024 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36025 use fsqlite_types::Snapshot;
36026
36027 let pager = MemoryMockMvccPager;
36028 let cx = Cx::new();
36029 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36030
36031 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36032 let lock_table = Arc::new(InProcessPageLockTable::new());
36033 let commit_index = Arc::new(CommitIndex::new());
36034 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36035
36036 let (session_id, handle) = {
36037 let mut guard = registry
36038 .lock()
36039 .unwrap_or_else(std::sync::PoisonError::into_inner);
36040 let session_id = guard
36041 .begin_concurrent(snapshot)
36042 .expect("session should register");
36043 let handle = guard
36044 .handle(session_id)
36045 .expect("session handle must be present");
36046 (session_id, handle)
36047 };
36048
36049 let mut page_io = SharedTxnPageIo::with_concurrent(
36050 txn,
36051 session_id,
36052 handle,
36053 Arc::clone(&lock_table),
36054 Arc::clone(&commit_index),
36055 0,
36056 );
36057 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36058 let expected = vec![0x5A; 32];
36059
36060 run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(expected.clone())))
36061 .expect("short concurrent owned write should succeed");
36062
36063 let bytes = run_async(page_io.read_page(&cx, page_no))
36064 .expect("read-your-writes should return the normalized owned page image");
36065 assert_eq!(
36066 bytes.len(),
36067 PageSize::DEFAULT.as_usize(),
36068 "concurrent owned writes must preserve the pager page-size invariant"
36069 );
36070 assert_eq!(&bytes[..expected.len()], expected.as_slice());
36071 assert!(
36072 bytes[expected.len()..].iter().all(|byte| *byte == 0),
36073 "concurrent owned writes should zero-fill any unwritten tail bytes"
36074 );
36075 }
36076
36077 #[test]
36078 fn test_shared_txn_page_io_try_mutate_staged_page_data_updates_read_your_writes() {
36079 use std::path::PathBuf;
36080
36081 use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36082 use fsqlite_types::Snapshot;
36083 use fsqlite_vfs::MemoryVfs;
36084
36085 let _guard = VDBE_OBSERVABILITY_LOCK
36086 .lock()
36087 .unwrap_or_else(|e| e.into_inner());
36088 let vfs = MemoryVfs::new();
36089 let path = PathBuf::from("/shared_txn_page_io_try_mutate_staged_page_data.db");
36090 let cx = Cx::new();
36091 let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36092 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36093
36094 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36095 let lock_table = Arc::new(InProcessPageLockTable::new());
36096 let commit_index = Arc::new(CommitIndex::new());
36097 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36098
36099 let (session_id, handle) = {
36100 let mut guard = registry
36101 .lock()
36102 .unwrap_or_else(std::sync::PoisonError::into_inner);
36103 let session_id = guard
36104 .begin_concurrent(snapshot)
36105 .expect("session should register");
36106 let handle = guard
36107 .handle(session_id)
36108 .expect("session handle must be present");
36109 (session_id, handle)
36110 };
36111
36112 let mut page_io = SharedTxnPageIo::with_concurrent(
36113 txn,
36114 session_id,
36115 handle,
36116 Arc::clone(&lock_table),
36117 Arc::clone(&commit_index),
36118 0,
36119 );
36120 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36121 let mut page = vec![0x11; PageSize::MIN.as_usize()];
36122
36123 run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(page.clone())))
36124 .expect("initial concurrent owned write should succeed");
36125
36126 let mut missing_page_closure_called = false;
36127 assert!(
36128 !page_io.try_mutate_staged_page_data(
36129 PageNumber::new(3).expect("page number must be non-zero"),
36130 &mut |data| {
36131 missing_page_closure_called = true;
36132 data.as_bytes_mut()[0] = 0xEE;
36133 },
36134 ),
36135 "unstaged pages should not report a successful mutation"
36136 );
36137 assert!(
36138 !missing_page_closure_called,
36139 "unstaged-page mutation must not invoke the caller closure"
36140 );
36141
36142 assert!(
36143 page_io.try_mutate_staged_page_data(page_no, &mut |data| {
36144 let bytes = data.as_bytes_mut();
36145 bytes[0] = 0xA5;
36146 bytes[PageSize::MIN.as_usize() - 1] = 0x5A;
36147 }),
36148 "staged concurrent pages should support in-place pager mutation"
36149 );
36150 page[0] = 0xA5;
36151 page[PageSize::MIN.as_usize() - 1] = 0x5A;
36152
36153 let read_back = run_async(page_io.read_page_data(&cx, page_no))
36154 .expect("read-your-writes should use the mutated pager image");
36155 assert_eq!(read_back.as_bytes(), page.as_slice());
36156
36157 let guard = registry
36158 .lock()
36159 .unwrap_or_else(std::sync::PoisonError::into_inner);
36160 let writer = guard
36161 .get(session_id)
36162 .expect("writer session should remain registered");
36163 assert!(
36164 writer.write_set().contains_key(&page_no),
36165 "in-place mutation must preserve the concurrent write-set surface"
36166 );
36167 assert!(
36168 writer.held_locks().contains(&page_no),
36169 "in-place mutation must preserve the page lock"
36170 );
36171 }
36172
36173 #[test]
36174 fn test_shared_txn_page_io_short_owned_write_tracks_in_place_zero_extend_metrics() {
36175 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36176
36177 let _guard = VDBE_OBSERVABILITY_LOCK
36178 .lock()
36179 .unwrap_or_else(|e| e.into_inner());
36180 let prev_metrics_enabled = vdbe_metrics_enabled();
36181 reset_vdbe_metrics();
36182 set_vdbe_metrics_enabled(true);
36183
36184 let pager = MemoryMockMvccPager;
36185 let cx = Cx::new();
36186 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36187 let mut page_io = SharedTxnPageIo::new(txn);
36188 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36189 let expected = vec![0x6B; 32];
36190
36191 let before = vdbe_metrics_snapshot();
36192 run_async(page_io.write_page_data(&cx, page_no, PageData::from_vec(expected.clone())))
36193 .expect("short owned write should succeed");
36194 let after = vdbe_metrics_snapshot();
36195
36196 let bytes = run_async(page_io.read_page(&cx, page_no))
36197 .expect("write should remain readable through the pager");
36198 assert_eq!(bytes.len(), PageSize::DEFAULT.as_usize());
36199 assert_eq!(&bytes[..expected.len()], expected.as_slice());
36200 assert!(
36201 bytes[expected.len()..].iter().all(|byte| *byte == 0),
36202 "short owned writes should still zero-fill their unwritten tail"
36203 );
36204 assert_eq!(
36205 after.page_data_motion.owned_write_normalization_calls_total
36206 - before
36207 .page_data_motion
36208 .owned_write_normalization_calls_total,
36209 1
36210 );
36211 assert_eq!(
36212 after.page_data_motion.owned_in_place_zero_extends_total
36213 - before.page_data_motion.owned_in_place_zero_extends_total,
36214 1
36215 );
36216 assert_eq!(
36217 after.page_data_motion.owned_resized_copies_total
36218 - before.page_data_motion.owned_resized_copies_total,
36219 0
36220 );
36221 assert_eq!(
36222 after.page_data_motion.normalized_payload_bytes_total
36223 - before.page_data_motion.normalized_payload_bytes_total,
36224 0
36225 );
36226 assert_eq!(
36227 after.page_data_motion.normalized_zero_fill_bytes_total
36228 - before.page_data_motion.normalized_zero_fill_bytes_total,
36229 u64::try_from(PageSize::DEFAULT.as_usize() - expected.len()).unwrap()
36230 );
36231
36232 set_vdbe_metrics_enabled(prev_metrics_enabled);
36233 }
36234
36235 #[test]
36236 fn test_shared_txn_page_io_rejects_oversized_write_buffer() {
36237 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36238
36239 let pager = MemoryMockMvccPager;
36240 let cx = Cx::new();
36241 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36242 let mut page_io = SharedTxnPageIo::new(txn);
36243 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36244 let oversized = vec![0xCC; PageSize::DEFAULT.as_usize() + 1];
36245
36246 let err = run_async(page_io.write_page(&cx, page_no, &oversized))
36247 .expect_err("oversized page buffer should be rejected");
36248
36249 assert!(
36250 matches!(err, FrankenError::Internal(ref message) if message.contains("page buffer exceeds page size invariant")),
36251 "unexpected error for oversized page buffer: {err}"
36252 );
36253 }
36254
36255 #[test]
36256 fn test_shared_txn_page_io_rejects_oversized_owned_page_buffer() {
36257 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36258
36259 let pager = MemoryMockMvccPager;
36260 let cx = Cx::new();
36261 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36262 let mut page_io = SharedTxnPageIo::new(txn);
36263 let page_no = PageNumber::new(2).expect("page number must be non-zero");
36264 let oversized = PageData::from_vec(vec![0xDD; PageSize::DEFAULT.as_usize() + 1]);
36265
36266 let err = run_async(page_io.write_page_data(&cx, page_no, oversized))
36267 .expect_err("oversized owned page buffer should be rejected");
36268
36269 assert!(
36270 matches!(err, FrankenError::Internal(ref message) if message.contains("page buffer exceeds page size invariant")),
36271 "unexpected error for oversized owned page buffer: {err}"
36272 );
36273 }
36274
36275 #[test]
36276 fn test_shared_txn_page_io_busy_snapshot_restores_page_one_tracking() {
36277 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36278 use fsqlite_types::Snapshot;
36279
36280 let pager = MockMvccPager;
36281 let cx = Cx::new();
36282 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36283
36284 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36285 let lock_table = Arc::new(InProcessPageLockTable::new());
36286 let commit_index = Arc::new(CommitIndex::new());
36287 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36288 let target_page = PageNumber::new(2).unwrap();
36289 let page_bytes = vec![0xEF; PageSize::DEFAULT.as_usize()];
36290
36291 let (writer_session, writer_handle) = {
36292 let mut guard = registry
36293 .lock()
36294 .unwrap_or_else(std::sync::PoisonError::into_inner);
36295 let writer_session = guard
36296 .begin_concurrent(snapshot)
36297 .expect("writer session should register");
36298 let writer_handle = guard
36299 .handle(writer_session)
36300 .expect("writer session handle must be present");
36301 (writer_session, writer_handle)
36302 };
36303
36304 commit_index.update(target_page, CommitSeq::new(8));
36305
36306 let mut page_io = SharedTxnPageIo::with_concurrent(
36307 txn,
36308 writer_session,
36309 writer_handle,
36310 Arc::clone(&lock_table),
36311 Arc::clone(&commit_index),
36312 250,
36313 );
36314
36315 let err = run_async(page_io.write_page(&cx, target_page, &page_bytes))
36316 .expect_err("stale snapshot should reject the write");
36317 assert!(
36318 matches!(err, FrankenError::BusySnapshot { .. }),
36319 "expected SQLITE_BUSY_SNAPSHOT on stale page write, got {err}"
36320 );
36321
36322 let guard = registry
36323 .lock()
36324 .unwrap_or_else(std::sync::PoisonError::into_inner);
36325 let writer = guard
36326 .get(writer_session)
36327 .expect("writer session should remain registered");
36328 assert!(
36329 writer.write_set().is_empty(),
36330 "stale snapshot must not leak staged page bytes into the write set"
36331 );
36332 assert!(
36333 !writer.tracks_write_conflict_page(PageNumber::ONE),
36334 "failed write must restore the synthetic page-one conflict surface"
36335 );
36336 assert!(
36337 !writer.held_locks().contains(&PageNumber::ONE),
36338 "failed write must not retain the synthetic page-one lock"
36339 );
36340 assert_eq!(
36341 lock_table.total_lock_count(),
36342 0,
36343 "stale snapshot failure should leave the lock table unchanged"
36344 );
36345 }
36346
36347 #[test]
36348 fn test_shared_txn_page_io_net_zero_growth_clears_synthetic_page_one_tracking() {
36349 use fsqlite_pager::{MemoryMockMvccPager, MvccPager as _, TransactionMode};
36350 use fsqlite_types::Snapshot;
36351
36352 let pager = MemoryMockMvccPager;
36353 let cx = Cx::new();
36354 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36355
36356 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36357 let lock_table = Arc::new(InProcessPageLockTable::new());
36358 let commit_index = Arc::new(CommitIndex::new());
36359 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36360
36361 let (session_id, handle) = {
36362 let mut guard = registry
36363 .lock()
36364 .unwrap_or_else(std::sync::PoisonError::into_inner);
36365 let session_id = guard
36366 .begin_concurrent(snapshot)
36367 .expect("session should register");
36368 let handle = guard
36369 .handle(session_id)
36370 .expect("session handle must be present");
36371 (session_id, handle)
36372 };
36373
36374 let mut page_io = SharedTxnPageIo::with_concurrent(
36375 txn,
36376 session_id,
36377 Arc::clone(&handle),
36378 Arc::clone(&lock_table),
36379 Arc::clone(&commit_index),
36380 0,
36381 );
36382
36383 let page_no = run_async(page_io.allocate_page(&cx))
36384 .expect("allocate_page should succeed for concurrent txn");
36385 let page_bytes = vec![0x5A; PageSize::DEFAULT.as_usize()];
36386 run_async(page_io.write_page(&cx, page_no, &page_bytes))
36387 .expect("write_page should succeed for allocated page");
36388
36389 {
36390 let guard = handle.lock();
36391 assert!(
36392 !guard.tracks_write_conflict_page(PageNumber::ONE),
36393 "successful writes must reconcile synthetic page-one tracking when the pager does not expose page one in pending_commit_pages"
36394 );
36395 assert!(
36396 !guard.held_locks().contains(&PageNumber::ONE),
36397 "successful writes must release any synthetic page-one lock when reconciliation drops page one from the conflict surface"
36398 );
36399 }
36400
36401 run_async(page_io.free_page(&cx, page_no))
36402 .expect("free_page should succeed for net-zero growth");
36403
36404 let guard = handle.lock();
36405 assert!(
36406 !guard.tracks_write_conflict_page(PageNumber::ONE),
36407 "net-zero growth should drop the synthetic page-one conflict surface"
36408 );
36409 assert!(
36410 !guard.held_locks().contains(&PageNumber::ONE),
36411 "net-zero growth should release the synthetic page-one lock"
36412 );
36413 }
36414
36415 #[test]
36416 fn test_shared_txn_page_io_concurrent_growth_write_does_not_block_on_page_one_pretracking() {
36417 use std::path::PathBuf;
36418
36419 use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36420 use fsqlite_types::Snapshot;
36421 use fsqlite_vfs::MemoryVfs;
36422
36423 let vfs = MemoryVfs::new();
36424 let path = PathBuf::from("/leased_growth_write_skips_page_one_pretracking.db");
36425 let cx = Cx::new();
36426 let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36427
36428 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36429 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36430 let lock_table = Arc::new(InProcessPageLockTable::new());
36431 let commit_index = Arc::new(CommitIndex::new());
36432 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36433
36434 let (session_id, handle, blocker_session) = {
36435 let mut guard = registry
36436 .lock()
36437 .unwrap_or_else(std::sync::PoisonError::into_inner);
36438 let session_id = guard
36439 .begin_concurrent(snapshot)
36440 .expect("session should register");
36441 let handle = guard
36442 .handle(session_id)
36443 .expect("session handle must be present");
36444 let blocker_session = guard
36445 .begin_concurrent(snapshot)
36446 .expect("blocker session should register");
36447 (session_id, handle, blocker_session)
36448 };
36449
36450 {
36451 let guard = registry
36452 .lock()
36453 .unwrap_or_else(std::sync::PoisonError::into_inner);
36454 let mut blocker = guard
36455 .get_mut(blocker_session)
36456 .expect("blocker handle should be present");
36457 concurrent_track_write_conflict_page(
36458 &mut blocker,
36459 &lock_table,
36460 blocker_session,
36461 PageNumber::ONE,
36462 )
36463 .expect("blocker must hold synthetic page-one tracking");
36464 }
36465
36466 let mut page_io = SharedTxnPageIo::with_concurrent(
36467 txn,
36468 session_id,
36469 Arc::clone(&handle),
36470 Arc::clone(&lock_table),
36471 Arc::clone(&commit_index),
36472 0,
36473 );
36474
36475 let page_no =
36476 run_async(page_io.allocate_page(&cx)).expect("allocate_page should not need page one");
36477 run_async(page_io.write_page(&cx, page_no, &vec![0x5A; PageSize::MIN.as_usize()]))
36478 .expect("leased growth write should not block on unrelated page-one tracking");
36479
36480 let guard = handle.lock();
36481 assert!(
36482 guard.tracks_write_conflict_page(page_no),
36483 "the actual high page must still enter the write-conflict surface"
36484 );
36485 assert!(
36486 !guard.tracks_write_conflict_page(PageNumber::ONE),
36487 "tier-1 leased growth should not synthesize page-one conflict tracking before commit planning"
36488 );
36489 }
36490
36491 #[test]
36492 fn test_shared_txn_page_io_high_page_write_skips_conservative_page_one_probe() {
36493 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36494 use fsqlite_types::Snapshot;
36495
36496 let pager = MockMvccPager;
36497 let cx = Cx::new();
36498 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36499
36500 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36501 let lock_table = Arc::new(InProcessPageLockTable::new());
36502 let commit_index = Arc::new(CommitIndex::new());
36503 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36504 let target_page = PageNumber::new(2).unwrap();
36505
36506 let (session_id, handle, blocker_session) = {
36507 let mut guard = registry
36508 .lock()
36509 .unwrap_or_else(std::sync::PoisonError::into_inner);
36510 let session_id = guard
36511 .begin_concurrent(snapshot)
36512 .expect("session should register");
36513 let handle = guard
36514 .handle(session_id)
36515 .expect("session handle must be present");
36516 let blocker_session = guard
36517 .begin_concurrent(snapshot)
36518 .expect("blocker session should register");
36519 (session_id, handle, blocker_session)
36520 };
36521
36522 {
36523 let guard = registry
36524 .lock()
36525 .unwrap_or_else(std::sync::PoisonError::into_inner);
36526 let mut blocker = guard
36527 .get_mut(blocker_session)
36528 .expect("blocker handle should be present");
36529 concurrent_track_write_conflict_page(
36530 &mut blocker,
36531 &lock_table,
36532 blocker_session,
36533 PageNumber::ONE,
36534 )
36535 .expect("blocker must hold page-one tracking");
36536 }
36537
36538 let mut page_io = SharedTxnPageIo::with_concurrent(
36539 txn,
36540 session_id,
36541 Arc::clone(&handle),
36542 Arc::clone(&lock_table),
36543 Arc::clone(&commit_index),
36544 0,
36545 );
36546
36547 run_async(page_io.write_page(&cx, target_page, &vec![0x7B; PageSize::DEFAULT.as_usize()]))
36548 .expect("ordinary high-page concurrent writes must not probe page-one tracking");
36549
36550 let guard = handle.lock();
36551 assert!(
36552 guard.tracks_write_conflict_page(target_page),
36553 "the high page must still enter the write-conflict surface"
36554 );
36555 assert!(
36556 !guard.tracks_write_conflict_page(PageNumber::ONE),
36557 "high-page writes must not synthesize page-one conflict tracking"
36558 );
36559 }
36560
36561 #[test]
36562 fn test_shared_txn_page_io_free_does_not_late_acquire_existing_freelist_trunk_pages() {
36563 use std::path::PathBuf;
36564
36565 use fsqlite_pager::{MvccPager as _, SimplePager, TransactionMode};
36566 use fsqlite_types::Snapshot;
36567 use fsqlite_vfs::MemoryVfs;
36568
36569 let vfs = MemoryVfs::new();
36570 let path = PathBuf::from("/late_pending_commit_freelist_trunk.db");
36571 let cx = Cx::new();
36572 let pager = run_async(SimplePager::open_with_cx(&cx, vfs, &path, PageSize::MIN)).unwrap();
36573 let ps = PageSize::MIN.as_usize();
36574
36575 let (p2, p3) = {
36576 let mut seed = run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36577 let p2 = run_async(seed.allocate_page(&cx)).unwrap();
36578 let p3 = run_async(seed.allocate_page(&cx)).unwrap();
36579 run_async(seed.write_page(&cx, p2, &vec![0x11; ps])).unwrap();
36580 run_async(seed.write_page(&cx, p3, &vec![0x22; ps])).unwrap();
36581 run_async(seed.commit(&cx)).unwrap();
36582 (p2, p3)
36583 };
36584
36585 {
36586 let mut establish_committed_freelist =
36587 run_async(pager.begin(&cx, TransactionMode::Immediate)).unwrap();
36588 run_async(establish_committed_freelist.free_page(&cx, p2)).unwrap();
36589 run_async(establish_committed_freelist.commit(&cx)).unwrap();
36590 }
36591
36592 {
36593 let mut preview = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36594 run_async(preview.free_page(&cx, p3)).unwrap();
36595 let predicted = preview.pending_commit_pages().unwrap();
36596 assert!(
36597 predicted
36598 .iter()
36599 .any(|page| *page != PageNumber::ONE && page.get() <= p3.get()),
36600 "freeing a durable page should expose a real freelist page in the commit surface"
36601 );
36602 run_async(preview.rollback(&cx)).unwrap();
36603 }
36604
36605 let txn = run_async(pager.begin(&cx, TransactionMode::Concurrent)).unwrap();
36606 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36607 let lock_table = Arc::new(InProcessPageLockTable::new());
36608 let commit_index = Arc::new(CommitIndex::new());
36609 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36610
36611 let (session_id, handle, blocker_session) = {
36612 let mut guard = registry
36613 .lock()
36614 .unwrap_or_else(std::sync::PoisonError::into_inner);
36615 let session_id = guard
36616 .begin_concurrent(snapshot)
36617 .expect("session should register");
36618 let handle = guard
36619 .handle(session_id)
36620 .expect("session handle must be present");
36621 let blocker_session = guard
36622 .begin_concurrent(snapshot)
36623 .expect("blocker session should register");
36624 (session_id, handle, blocker_session)
36625 };
36626
36627 {
36628 let guard = registry
36629 .lock()
36630 .unwrap_or_else(std::sync::PoisonError::into_inner);
36631 let mut blocker = guard
36632 .get_mut(blocker_session)
36633 .expect("blocker handle should be present");
36634 for page in txn.pending_commit_pages().unwrap() {
36635 if page == PageNumber::ONE {
36636 continue;
36637 }
36638 concurrent_track_write_conflict_page(
36639 &mut blocker,
36640 &lock_table,
36641 blocker_session,
36642 page,
36643 )
36644 .expect("blocker must hold predicted freelist commit page locks");
36645 }
36646 }
36647
36648 let predicted_commit_pages = txn.pending_commit_pages().unwrap();
36649
36650 let mut page_io = SharedTxnPageIo::with_concurrent(
36651 txn,
36652 session_id,
36653 Arc::clone(&handle),
36654 Arc::clone(&lock_table),
36655 Arc::clone(&commit_index),
36656 0,
36657 );
36658
36659 run_async(page_io.free_page(&cx, p3))
36660 .expect("free_page must not fail just because commit-time trunk rewrites will later need an existing freelist page");
36661
36662 let guard = handle.lock();
36663 for page in predicted_commit_pages {
36664 assert!(
36665 !guard.tracks_write_conflict_page(page),
36666 "the per-op path must not late-acquire freelist commit-surface pages after mutating the pager state"
36667 );
36668 assert!(
36669 !guard.held_locks().contains(&page),
36670 "the per-op path must not steal freelist commit-surface page locks before commit planning"
36671 );
36672 }
36673 }
36674
36675 #[test]
36676 fn test_shared_txn_page_io_clears_preexisting_synthetic_page_one_tracking_when_unneeded() {
36677 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36678
36679 let cx = Cx::new();
36683 let pager = MockMvccPager;
36684 let txn = run_async(pager.begin(&cx, TransactionMode::Immediate))
36685 .expect("transaction should start");
36686
36687 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36688 let lock_table = Arc::new(InProcessPageLockTable::new());
36689 let commit_index = Arc::new(CommitIndex::new());
36690 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36691
36692 let (session_id, handle) = {
36693 let mut guard = registry
36694 .lock()
36695 .unwrap_or_else(std::sync::PoisonError::into_inner);
36696 let session_id = guard
36697 .begin_concurrent(snapshot)
36698 .expect("session should register");
36699 let handle = guard
36700 .handle(session_id)
36701 .expect("session handle must be present");
36702 (session_id, handle)
36703 };
36704
36705 {
36706 let mut guard = handle.lock();
36707 concurrent_track_write_conflict_page(
36708 &mut guard,
36709 &lock_table,
36710 session_id,
36711 PageNumber::ONE,
36712 )
36713 .expect("synthetic page-one tracking should be installed");
36714 }
36715
36716 let mut page_io = SharedTxnPageIo::with_concurrent(
36717 txn,
36718 session_id,
36719 Arc::clone(&handle),
36720 Arc::clone(&lock_table),
36721 Arc::clone(&commit_index),
36722 0,
36723 );
36724
36725 {
36726 let guard = handle.lock();
36727 assert!(
36728 guard.tracks_write_conflict_page(PageNumber::ONE),
36729 "test precondition should start with synthetic page-one tracking already present"
36730 );
36731 }
36732
36733 run_async(page_io.free_page(
36734 &cx,
36735 PageNumber::new(2).expect("page number must be non-zero"),
36736 ))
36737 .expect("free_page should succeed while pending commit pages stay empty");
36738
36739 let guard = handle.lock();
36740 assert!(
36741 !guard.tracks_write_conflict_page(PageNumber::ONE),
36742 "reconciliation must clear stale synthetic page-one tracking even when the current op did not introduce it"
36743 );
36744 assert!(
36745 !guard.held_locks().contains(&PageNumber::ONE),
36746 "reconciliation must release the stale synthetic page-one lock"
36747 );
36748 }
36749
36750 #[test]
36751 fn test_shared_txn_page_io_wait_is_cancellation_responsive() {
36752 use fsqlite_pager::{MockMvccPager, MvccPager as _, TransactionMode};
36753 use fsqlite_types::Snapshot;
36754
36755 let pager = MockMvccPager;
36756 let root_cx = Cx::new();
36757 let write_cx = root_cx.create_child();
36758 let txn = run_async(pager.begin(&root_cx, TransactionMode::Immediate)).unwrap();
36759
36760 let registry = Arc::new(Mutex::new(ConcurrentRegistry::new()));
36761 let lock_table = Arc::new(InProcessPageLockTable::new());
36762 let commit_index = Arc::new(CommitIndex::new());
36763 let snapshot = Snapshot::new(CommitSeq::new(7), SchemaEpoch::new(1));
36764 let contested_page = PageNumber::ONE;
36765 let page_bytes = vec![0xAA; PageSize::DEFAULT.as_usize()];
36766
36767 let (holder_session, writer_session, writer_handle) = {
36768 let mut guard = registry
36769 .lock()
36770 .unwrap_or_else(std::sync::PoisonError::into_inner);
36771 let holder_session = guard
36772 .begin_concurrent(snapshot)
36773 .expect("holder session should register");
36774 let writer_session = guard
36775 .begin_concurrent(snapshot)
36776 .expect("writer session should register");
36777
36778 let mut holder = guard
36779 .get_mut(holder_session)
36780 .expect("holder session must be present");
36781 concurrent_write_page(
36782 &mut holder,
36783 &lock_table,
36784 holder_session,
36785 contested_page,
36786 PageData::from_vec(page_bytes.clone()),
36787 )
36788 .expect("holder should acquire the contested page lock");
36789 let writer_handle = guard
36790 .handle(writer_session)
36791 .expect("writer session handle must be present");
36792 (holder_session, writer_session, writer_handle)
36793 };
36794
36795 let cancel_cx = write_cx.clone();
36796 let cancel_helper = std::thread::spawn(move || {
36797 std::thread::sleep(Duration::from_millis(20));
36798 cancel_cx.cancel();
36799 });
36800
36801 let mut page_io = SharedTxnPageIo::with_concurrent(
36802 txn,
36803 writer_session,
36804 writer_handle,
36805 Arc::clone(&lock_table),
36806 Arc::clone(&commit_index),
36807 500,
36808 );
36809
36810 let started = std::time::Instant::now();
36811 let err = run_async(page_io.write_page(&write_cx, contested_page, &page_bytes))
36812 .expect_err("cancelled waiter should abort before busy timeout");
36813 cancel_helper
36814 .join()
36815 .expect("cancel helper thread must complete cleanly");
36816
36817 assert!(matches!(err, FrankenError::Abort));
36818 assert!(
36819 started.elapsed() < Duration::from_millis(250),
36820 "cancelled wait should return promptly instead of sleeping until busy_timeout"
36821 );
36822
36823 let guard = registry
36824 .lock()
36825 .unwrap_or_else(std::sync::PoisonError::into_inner);
36826 let holder = guard
36827 .get(holder_session)
36828 .expect("holder session should remain registered");
36829 assert!(
36830 holder.held_locks().contains(&contested_page),
36831 "cancellation must not disturb the winning writer's held lock"
36832 );
36833 let writer = guard
36834 .get(writer_session)
36835 .expect("writer session should remain registered");
36836 assert!(
36837 writer.write_set().is_empty(),
36838 "cancelled write must not stage page bytes"
36839 );
36840 assert!(
36841 writer.held_locks().is_empty(),
36842 "cancelled write must not retain the contested page lock"
36843 );
36844 }
36845
36846 #[test]
36849 fn test_subtype_set_get_clr_roundtrip() {
36850 let rows = run_program(|b| {
36854 let end = b.emit_label();
36855 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36856
36857 let r_subtype = b.alloc_reg(); let r_target = b.alloc_reg(); let r_out1 = b.alloc_reg(); let r_out2 = b.alloc_reg(); let r_out3 = b.alloc_reg(); b.emit_op(
36865 Opcode::String8,
36866 0,
36867 r_target,
36868 0,
36869 P4::Str(r#"{"a":1}"#.to_owned()),
36870 0,
36871 );
36872
36873 b.emit_op(Opcode::GetSubtype, r_target, r_out1, 0, P4::None, 0);
36875
36876 b.emit_op(Opcode::Integer, 74, r_subtype, 0, P4::None, 0);
36878 b.emit_op(Opcode::SetSubtype, r_subtype, r_target, 0, P4::None, 0);
36879
36880 b.emit_op(Opcode::GetSubtype, r_target, r_out2, 0, P4::None, 0);
36882
36883 b.emit_op(Opcode::ClrSubtype, r_target, 0, 0, P4::None, 0);
36885 b.emit_op(Opcode::GetSubtype, r_target, r_out3, 0, P4::None, 0);
36886
36887 b.emit_op(Opcode::ResultRow, r_out1, 3, 0, P4::None, 0);
36888 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36889 b.resolve_label(end);
36890 });
36891 assert_eq!(rows.len(), 1);
36892 assert_eq!(rows[0][0], SqliteValue::Integer(0)); assert_eq!(rows[0][1], SqliteValue::Integer(74)); assert_eq!(rows[0][2], SqliteValue::Integer(0)); }
36896
36897 #[test]
36898 fn test_subtype_set_zero_clears() {
36899 let rows = run_program(|b| {
36901 let end = b.emit_label();
36902 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36903
36904 let r_st = b.alloc_reg();
36905 let r_target = b.alloc_reg();
36906 let r_out = b.alloc_reg();
36907
36908 b.emit_op(Opcode::Integer, 42, r_target, 0, P4::None, 0);
36909 b.emit_op(Opcode::Integer, 74, r_st, 0, P4::None, 0);
36911 b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36912 b.emit_op(Opcode::Integer, 0, r_st, 0, P4::None, 0);
36914 b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36915 b.emit_op(Opcode::GetSubtype, r_target, r_out, 0, P4::None, 0);
36916
36917 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
36918 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36919 b.resolve_label(end);
36920 });
36921 assert_eq!(rows[0][0], SqliteValue::Integer(0));
36922 }
36923
36924 #[test]
36925 fn test_subtype_is_cleared_when_register_value_is_overwritten() {
36926 let rows = run_program(|b| {
36927 let end = b.emit_label();
36928 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
36929
36930 let r_st = b.alloc_reg();
36931 let r_target = b.alloc_reg();
36932 let r_out = b.alloc_reg();
36933
36934 b.emit_op(
36935 Opcode::String8,
36936 0,
36937 r_target,
36938 0,
36939 P4::Str(r#"{"a":1}"#.to_owned()),
36940 0,
36941 );
36942 b.emit_op(Opcode::Integer, 74, r_st, 0, P4::None, 0);
36943 b.emit_op(Opcode::SetSubtype, r_st, r_target, 0, P4::None, 0);
36944
36945 b.emit_op(Opcode::Integer, 42, r_target, 0, P4::None, 0);
36948 b.emit_op(Opcode::GetSubtype, r_target, r_out, 0, P4::None, 0);
36949
36950 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
36951 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36952 b.resolve_label(end);
36953 });
36954
36955 assert_eq!(rows, vec![vec![SqliteValue::Integer(0)]]);
36956 }
36957
36958 #[test]
36959 fn test_take_reg_clears_subtype_metadata() {
36960 let mut engine = VdbeEngine::new(4);
36961 engine.set_reg(1, SqliteValue::Text("payload".into()));
36962 engine.set_register_subtype(1, 74);
36963
36964 assert_eq!(engine.take_reg(1), SqliteValue::Text("payload".into()));
36965 assert_eq!(engine.get_reg(1), &SqliteValue::Null);
36966 assert!(
36967 engine.register_subtype(1).is_none(),
36968 "moving a register value out must clear any stale subtype metadata"
36969 );
36970 }
36971
36972 #[test]
36973 fn test_execute_reuse_clears_subtype_metadata() {
36974 let mut first_builder = ProgramBuilder::new();
36975 first_builder.emit_op(
36976 Opcode::String8,
36977 0,
36978 1,
36979 0,
36980 P4::Str(r#"{"a":1}"#.to_owned()),
36981 0,
36982 );
36983 first_builder.emit_op(Opcode::Integer, 74, 2, 0, P4::None, 0);
36984 first_builder.emit_op(Opcode::SetSubtype, 2, 1, 0, P4::None, 0);
36985 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36986 let first_program = first_builder.finish().expect("first program should build");
36987
36988 let mut second_builder = ProgramBuilder::new();
36989 second_builder.emit_op(Opcode::GetSubtype, 1, 3, 0, P4::None, 0);
36990 second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
36991 second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
36992 let second_program = second_builder
36993 .finish()
36994 .expect("second program should build");
36995
36996 let mut engine = VdbeEngine::new(
36997 first_program
36998 .register_count()
36999 .max(second_program.register_count()),
37000 );
37001 assert_eq!(
37002 run_async(engine.execute(&first_program)).expect("first execution"),
37003 ExecOutcome::Done
37004 );
37005 assert_eq!(
37006 run_async(engine.execute(&second_program)).expect("second execution"),
37007 ExecOutcome::Done
37008 );
37009
37010 assert_eq!(
37011 engine
37012 .results()
37013 .iter()
37014 .map(|row| row.clone().into_vec())
37015 .collect::<Vec<_>>(),
37016 vec![vec![SqliteValue::Integer(0)]]
37017 );
37018 }
37019
37020 #[test]
37021 fn test_simple_statement_does_not_allocate_cold_state_sidecar() {
37022 let mut builder = ProgramBuilder::new();
37023 builder.emit_op(Opcode::Integer, 42, 1, 0, P4::None, 0);
37024 builder.emit_op(Opcode::ResultRow, 1, 1, 0, P4::None, 0);
37025 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37026 let program = builder.finish().expect("program should build");
37027
37028 let mut engine = VdbeEngine::new(program.register_count());
37029 assert_eq!(
37030 run_async(engine.execute(&program)).expect("execution should succeed"),
37031 ExecOutcome::Done
37032 );
37033
37034 assert!(
37035 !engine.has_cold_state_allocated(),
37036 "simple non-feature statements should not allocate the cold sidecar"
37037 );
37038 assert!(engine.statement_cold_state.is_empty());
37039 }
37040
37041 #[test]
37042 fn test_aggregate_statement_allocates_cold_state_sidecar() {
37043 let mut builder = ProgramBuilder::new();
37044 builder.emit_op(Opcode::Integer, 7, 1, 0, P4::None, 0);
37045 builder.emit_op(Opcode::AggStep, 0, 1, 2, P4::FuncName("sum".to_owned()), 1);
37046 builder.emit_op(Opcode::AggFinal, 2, 1, 0, P4::FuncName("sum".to_owned()), 0);
37047 builder.emit_op(Opcode::ResultRow, 2, 1, 0, P4::None, 0);
37048 builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37049 let program = builder.finish().expect("program should build");
37050
37051 let mut engine = VdbeEngine::new(program.register_count());
37052 let mut registry = FunctionRegistry::new();
37053 register_builtins(&mut registry);
37054 engine.set_function_registry(Arc::new(registry));
37055
37056 assert_eq!(
37057 run_async(engine.execute(&program)).expect("aggregate execution should succeed"),
37058 ExecOutcome::Done
37059 );
37060 assert!(engine.has_cold_state_allocated());
37061 assert!(
37062 engine
37063 .statement_cold_state
37064 .contains(StatementColdState::AGGREGATES)
37065 );
37066 assert_eq!(
37067 engine
37068 .results()
37069 .iter()
37070 .map(|row| row.clone().into_vec())
37071 .collect::<Vec<_>>(),
37072 vec![vec![SqliteValue::Integer(7)]]
37073 );
37074 }
37075
37076 #[test]
37077 fn test_execute_reuse_drops_cold_state_sidecar() {
37078 let mut aggregate_builder = ProgramBuilder::new();
37079 aggregate_builder.emit_op(Opcode::Integer, 3, 1, 0, P4::None, 0);
37080 aggregate_builder.emit_op(Opcode::AggStep, 0, 1, 2, P4::FuncName("sum".to_owned()), 1);
37081 aggregate_builder.emit_op(Opcode::AggFinal, 2, 1, 0, P4::FuncName("sum".to_owned()), 0);
37082 aggregate_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37083 let aggregate_program = aggregate_builder
37084 .finish()
37085 .expect("aggregate program should build");
37086
37087 let mut simple_builder = ProgramBuilder::new();
37088 simple_builder.emit_op(Opcode::Integer, 1, 1, 0, P4::None, 0);
37089 simple_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37090 let simple_program = simple_builder
37091 .finish()
37092 .expect("simple program should build");
37093
37094 let mut engine = VdbeEngine::new(
37095 aggregate_program
37096 .register_count()
37097 .max(simple_program.register_count()),
37098 );
37099 let mut registry = FunctionRegistry::new();
37100 register_builtins(&mut registry);
37101 engine.set_function_registry(Arc::new(registry));
37102
37103 assert_eq!(
37104 run_async(engine.execute(&aggregate_program))
37105 .expect("aggregate execution should succeed"),
37106 ExecOutcome::Done
37107 );
37108 assert!(engine.has_cold_state_allocated());
37109
37110 assert_eq!(
37111 run_async(engine.execute(&simple_program)).expect("simple execution should succeed"),
37112 ExecOutcome::Done
37113 );
37114 assert!(
37115 !engine.has_cold_state_allocated(),
37116 "reuse should drop the cold sidecar before the next simple statement"
37117 );
37118 assert!(engine.statement_cold_state.is_empty());
37119 }
37120
37121 #[test]
37122 fn test_vdbe_engine_inline_size_stays_bounded() {
37123 assert!(
37124 std::mem::size_of::<VdbeEngine>() <= VDBE_ENGINE_INLINE_SIZE_BUDGET_BYTES,
37125 "VdbeEngine inline size regressed to {} bytes",
37126 std::mem::size_of::<VdbeEngine>()
37127 );
37128 }
37129
37130 #[test]
37133 fn test_bloom_filter_add_and_test() {
37134 let rows = run_program(|b| {
37137 let end = b.emit_label();
37138 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37139
37140 let r_filter = b.alloc_reg();
37141 let r_key = b.alloc_reg();
37142 let r_out = b.alloc_reg();
37143
37144 b.emit_op(Opcode::String8, 0, r_key, 0, P4::Str("hello".to_owned()), 0);
37146 b.emit_op(Opcode::FilterAdd, r_filter, 0, r_key, P4::None, 0);
37147
37148 let not_found = b.emit_label();
37150 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); b.emit_jump_to_label(Opcode::Filter, r_filter, r_key, not_found, P4::None, 0);
37152 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37154 b.resolve_label(not_found);
37155
37156 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37157 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37158 b.resolve_label(end);
37159 });
37160 assert_eq!(rows[0][0], SqliteValue::Integer(1)); }
37162
37163 #[test]
37164 fn test_bloom_filter_miss_jumps() {
37165 let rows = run_program(|b| {
37168 let end = b.emit_label();
37169 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37170
37171 let r_filter = b.alloc_reg();
37172 let r_key1 = b.alloc_reg();
37173 let r_key2 = b.alloc_reg();
37174 let r_out = b.alloc_reg();
37175
37176 b.emit_op(
37178 Opcode::String8,
37179 0,
37180 r_key1,
37181 0,
37182 P4::Str("hello".to_owned()),
37183 0,
37184 );
37185 b.emit_op(Opcode::FilterAdd, r_filter, 0, r_key1, P4::None, 0);
37186
37187 b.emit_op(
37189 Opcode::String8,
37190 0,
37191 r_key2,
37192 0,
37193 P4::Str("world".to_owned()),
37194 0,
37195 );
37196 let not_found = b.emit_label();
37197 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); b.emit_jump_to_label(Opcode::Filter, r_filter, r_key2, not_found, P4::None, 0);
37199 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37201 b.resolve_label(not_found);
37202
37203 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37204 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37205 b.resolve_label(end);
37206 });
37207 assert!(matches!(rows[0][0], SqliteValue::Integer(0 | 1)));
37209 }
37210
37211 #[test]
37212 fn test_bloom_filter_no_filter_falls_through() {
37213 let rows = run_program(|b| {
37216 let end = b.emit_label();
37217 b.emit_jump_to_label(Opcode::Init, 0, 0, end, P4::None, 0);
37218
37219 let r_filter = b.alloc_reg();
37220 let r_key = b.alloc_reg();
37221 let r_out = b.alloc_reg();
37222
37223 b.emit_op(Opcode::Integer, 42, r_key, 0, P4::None, 0);
37224 let not_found = b.emit_label();
37225 b.emit_op(Opcode::Integer, 0, r_out, 0, P4::None, 0); b.emit_jump_to_label(Opcode::Filter, r_filter, r_key, not_found, P4::None, 0);
37227 b.emit_op(Opcode::Integer, 1, r_out, 0, P4::None, 0);
37229 b.resolve_label(not_found);
37230
37231 b.emit_op(Opcode::ResultRow, r_out, 1, 0, P4::None, 0);
37232 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37233 b.resolve_label(end);
37234 });
37235 assert_eq!(rows[0][0], SqliteValue::Integer(1)); }
37237
37238 #[test]
37239 fn test_execute_reuse_clears_bloom_filters() {
37240 let added_value = SqliteValue::Text("hello".into());
37241 let missing_value = SqliteValue::Text("world".into());
37242 let bloom_bits = (BLOOM_FILTER_WORDS * 64) as u64;
37243 assert_ne!(
37244 bloom_hash(&added_value) % bloom_bits,
37245 bloom_hash(&missing_value) % bloom_bits,
37246 "test fixture must exercise a genuinely missing bloom-filter bit"
37247 );
37248
37249 let mut first_builder = ProgramBuilder::new();
37250 first_builder.emit_op(Opcode::String8, 0, 2, 0, P4::Str("hello".to_owned()), 0);
37251 first_builder.emit_op(Opcode::FilterAdd, 1, 0, 2, P4::None, 0);
37252 first_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37253 let first_program = first_builder.finish().expect("first program should build");
37254
37255 let mut second_builder = ProgramBuilder::new();
37256 let not_present = second_builder.emit_label();
37257 let end = second_builder.emit_label();
37258 second_builder.emit_op(Opcode::String8, 0, 2, 0, P4::Str("world".to_owned()), 0);
37259 second_builder.emit_jump_to_label(Opcode::Filter, 1, 0, not_present, P4::None, 2);
37260 second_builder.emit_op(Opcode::Integer, 1, 3, 0, P4::None, 0);
37261 second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
37262 second_builder.emit_jump_to_label(Opcode::Goto, 0, 0, end, P4::None, 0);
37263 second_builder.resolve_label(not_present);
37264 second_builder.emit_op(Opcode::Integer, 0, 3, 0, P4::None, 0);
37265 second_builder.emit_op(Opcode::ResultRow, 3, 1, 0, P4::None, 0);
37266 second_builder.resolve_label(end);
37267 second_builder.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37268 let second_program = second_builder
37269 .finish()
37270 .expect("second program should build");
37271
37272 let mut engine = VdbeEngine::new(
37273 first_program
37274 .register_count()
37275 .max(second_program.register_count()),
37276 );
37277 assert_eq!(
37278 run_async(engine.execute(&first_program)).expect("first execution"),
37279 ExecOutcome::Done
37280 );
37281 assert_eq!(
37282 run_async(engine.execute(&second_program)).expect("second execution"),
37283 ExecOutcome::Done
37284 );
37285
37286 assert_eq!(
37287 engine
37288 .results()
37289 .iter()
37290 .map(|row| row.clone().into_vec())
37291 .collect::<Vec<_>>(),
37292 vec![vec![SqliteValue::Integer(1)]],
37293 "second execution should observe no inherited bloom filter"
37294 );
37295 }
37296
37297 fn opcode_fusion_build_unfused_program(literal: i32) -> crate::VdbeProgram {
37306 let mut b = ProgramBuilder::new();
37307 let r = b.alloc_reg();
37308 b.emit_op(Opcode::Integer, literal, r, 0, P4::None, 0);
37309 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37310 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37311 b.finish().expect("unfused program should build")
37312 }
37313
37314 fn opcode_fusion_build_fused_program(literal: i32) -> (crate::VdbeProgram, usize) {
37315 let mut b = ProgramBuilder::new();
37316 let r = b.alloc_reg();
37317 b.emit_op(Opcode::Integer, literal, r, 0, P4::None, 0);
37318 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37319 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37320 let fused_count = b.apply_fuse_literal_result_row();
37321 (b.finish().expect("fused program should build"), fused_count)
37322 }
37323
37324 #[test]
37325 fn opcode_fusion_literal_result_row_peephole_rewrites_pair() {
37326 let (program, fused_count) = opcode_fusion_build_fused_program(42);
37327 assert_eq!(fused_count, 1, "exactly one fusion site should match");
37328 let ops = program.ops();
37329 assert_eq!(
37330 ops[0].opcode,
37331 Opcode::FusedLiteralResultRow,
37332 "first op should be the fused opcode"
37333 );
37334 assert_eq!(ops[0].p1, 42);
37335 assert_eq!(
37336 ops[1].opcode,
37337 Opcode::Noop,
37338 "second op should be Noop to preserve program length"
37339 );
37340 assert_eq!(ops[2].opcode, Opcode::Halt);
37341 }
37342
37343 #[test]
37344 fn opcode_fusion_literal_result_row_byte_equivalent_to_unfused() {
37345 let literal = 42;
37346
37347 let unfused = opcode_fusion_build_unfused_program(literal);
37348 let mut eng_unfused = VdbeEngine::new(unfused.register_count());
37349 assert_eq!(
37350 run_async(eng_unfused.execute(&unfused)).expect("unfused exec"),
37351 ExecOutcome::Done
37352 );
37353 let unfused_rows: Vec<Vec<SqliteValue>> = eng_unfused
37354 .take_results()
37355 .into_iter()
37356 .map(|row| row.into_vec())
37357 .collect();
37358
37359 let (fused, fused_count) = opcode_fusion_build_fused_program(literal);
37360 assert_eq!(fused_count, 1);
37361 let mut eng_fused = VdbeEngine::new(fused.register_count());
37362 assert_eq!(
37363 run_async(eng_fused.execute(&fused)).expect("fused exec"),
37364 ExecOutcome::Done
37365 );
37366 let fused_rows: Vec<Vec<SqliteValue>> = eng_fused
37367 .take_results()
37368 .into_iter()
37369 .map(|row| row.into_vec())
37370 .collect();
37371
37372 assert_eq!(
37373 fused_rows, unfused_rows,
37374 "fused program must emit byte-equivalent rows"
37375 );
37376 assert_eq!(
37377 fused_rows,
37378 vec![vec![SqliteValue::Integer(i64::from(literal))]]
37379 );
37380 }
37381
37382 #[test]
37383 fn opcode_fusion_peephole_skips_multi_column_result_row() {
37384 let mut b = ProgramBuilder::new();
37388 let r = b.alloc_reg();
37389 let _r2 = b.alloc_reg();
37390 b.emit_op(Opcode::Integer, 7, r, 0, P4::None, 0);
37391 b.emit_op(Opcode::ResultRow, r, 2, 0, P4::None, 0);
37392 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37393
37394 let count = b.apply_fuse_literal_result_row();
37395 assert_eq!(count, 0, "multi-column ResultRow must not fuse");
37396
37397 let program = b.finish().expect("program should build");
37398 assert_eq!(program.ops()[0].opcode, Opcode::Integer);
37399 assert_eq!(program.ops()[1].opcode, Opcode::ResultRow);
37400 }
37401
37402 #[test]
37403 fn opcode_fusion_peephole_skips_register_mismatch() {
37404 let mut b = ProgramBuilder::new();
37406 let r_a = b.alloc_reg();
37407 let r_b = b.alloc_reg();
37408 b.emit_op(Opcode::Integer, 7, r_a, 0, P4::None, 0);
37409 b.emit_op(Opcode::Integer, 9, r_b, 0, P4::None, 0);
37410 b.emit_op(Opcode::ResultRow, r_b, 1, 0, P4::None, 0);
37411 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37412
37413 let count = b.apply_fuse_literal_result_row();
37414 assert_eq!(count, 1);
37417
37418 let program = b.finish().expect("program should build");
37419 assert_eq!(program.ops()[0].opcode, Opcode::Integer);
37420 assert_eq!(program.ops()[1].opcode, Opcode::FusedLiteralResultRow);
37421 assert_eq!(program.ops()[2].opcode, Opcode::Noop);
37422 }
37423
37424 #[test]
37425 fn opcode_fusion_peephole_skips_jump_target_on_result_row() {
37426 let mut b = ProgramBuilder::new();
37430 let r = b.alloc_reg();
37431 b.emit_op(Opcode::Goto, 0, 2, 0, P4::None, 0);
37433 b.emit_op(Opcode::Integer, 5, r, 0, P4::None, 0);
37435 b.emit_op(Opcode::ResultRow, r, 1, 0, P4::None, 0);
37437 b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
37439
37440 let count = b.apply_fuse_literal_result_row();
37441 assert_eq!(count, 0, "ResultRow that is a jump target must not fuse");
37442 }
37443}