#![allow(unused_variables)] use crate::error::SQLITE_CONSTRAINT_UNIQUE; use crate::function::AlterTableFunc; use crate::mvcc::database::CheckpointStateMachine; use crate::numeric::{NullableInteger, Numeric}; use crate::schema::Table; use crate::state_machine::StateMachine; use crate::storage::btree::{ integrity_check, CursorTrait, IntegrityCheckError, IntegrityCheckState, PageCategory, }; use crate::storage::database::DatabaseFile; use crate::storage::page_cache::PageCache; use crate::storage::pager::{AtomicDbState, CreateBTreeFlags, DbState}; use crate::storage::sqlite3_ondisk::{read_varint_fast, DatabaseHeader, PageSize}; use crate::translate::collate::CollationSeq; use crate::types::{ compare_immutable, compare_records_generic, Extendable, IOCompletions, ImmutableRecord, SeekResult, Text, }; use crate::util::{ normalize_ident, rewrite_column_references_if_needed, rewrite_fk_parent_cols_if_self_ref, rewrite_fk_parent_table_if_needed, rewrite_inline_col_fk_target_if_needed, }; use crate::vdbe::insn::InsertFlags; use crate::vdbe::{registers_to_ref_values, EndStatement, TxnCleanup}; use crate::vector::{vector32_sparse, vector_concat, vector_distance_jaccard, vector_slice}; use crate::{ error::{ LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, }, ext::ExtValue, function::{AggFunc, ExtFunc, MathFunc, MathFuncArity, ScalarFunc, VectorFunc}, functions::{ datetime::{ exec_date, exec_datetime_full, exec_julianday, exec_strftime, exec_time, exec_unixepoch, }, printf::exec_printf, }, translate::emitter::TransactionMode, }; use crate::{get_cursor, CheckpointMode, Connection, DatabaseStorage, MvCursor}; use std::any::Any; use std::env::temp_dir; use std::ops::DerefMut; use std::{ borrow::BorrowMut, sync::{atomic::Ordering, Arc, Mutex}, }; use turso_macros::match_ignore_ascii_case; use crate::pseudo::PseudoCursor; use crate::{ schema::{affinity, Affinity}, storage::btree::{BTreeCursor, BTreeKey}, }; use crate::{ storage::wal::CheckpointResult, types::{ AggContext, Cursor, ExternalAggState, IOResult, SeekKey, SeekOp, SumAggState, Value, ValueType, }, util::{cast_real_to_integer, checked_cast_text_to_numeric, parse_schema_rows}, vdbe::{ builder::CursorType, insn::{IdxInsertFlags, Insn}, }, vector::{vector32, vector64, vector_distance_cos, vector_distance_l2, vector_extract}, }; use crate::{info, turso_assert, OpenFlags, Row, TransactionState, ValueRef}; use super::{ insn::{Cookie, RegisterOrLiteral}, CommitState, }; use parking_lot::RwLock; use turso_parser::ast::{self, ForeignKeyClause, Name, SortOrder}; use turso_parser::parser::Parser; use super::{ likeop::{construct_like_escape_arg, exec_glob, exec_like_with_escape}, sorter::Sorter, }; use regex::{Regex, RegexBuilder}; use std::collections::HashMap; #[cfg(feature = "json")] use crate::{ function::JsonFunc, json, json::convert_dbtype_to_raw_jsonb, json::get_json, json::is_json_valid, json::json_array, json::json_array_length, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_error_position, json::json_extract, json::json_from_raw_bytes_agg, json::json_insert, json::json_object, json::json_patch, json::json_quote, json::json_remove, json::json_replace, json::json_set, json::json_type, json::jsonb, json::jsonb_array, json::jsonb_extract, json::jsonb_insert, json::jsonb_object, json::jsonb_patch, json::jsonb_remove, json::jsonb_replace, json::jsonb_set, }; use super::{make_record, Program, ProgramState, Register}; #[cfg(feature = "fs")] use crate::resolve_ext_path; use crate::{bail_constraint_error, must_be_btree_cursor, MvStore, Pager, Result}; /// Macro to destructure an Insn enum variant, only to be used when it /// is *impossible* to be another variant. macro_rules! load_insn { ($variant:ident { $($field:tt $(: $binding:pat)?),* $(,)? }, $insn:expr) => { #[cfg(debug_assertions)] let Insn::$variant { $($field $(: $binding)?),* } = $insn else { panic!("Expected Insn::{}, got {:?}", stringify!($variant), $insn); }; #[cfg(not(debug_assertions))] let Insn::$variant { $($field $(: $binding)?),*} = $insn else { // this will optimize away the branch unsafe { std::hint::unreachable_unchecked() }; }; }; } macro_rules! return_if_io { ($expr:expr) => { match $expr { Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(err) => return Err(err), } }; } pub type InsnFunction = fn( &Program, &mut ProgramState, &Insn, &Arc, Option<&Arc>, ) -> Result; /// Compare two values using the specified collation for text values. /// Non-text values are compared using their natural ordering. fn compare_with_collation( lhs: &Value, rhs: &Value, collation: Option, ) -> std::cmp::Ordering { match (lhs, rhs) { (Value::Text(lhs_text), Value::Text(rhs_text)) => { if let Some(coll) = collation { coll.compare_strings(lhs_text.as_str(), rhs_text.as_str()) } else { lhs.cmp(rhs) } } _ => lhs.cmp(rhs), } } pub enum InsnFunctionStepResult { Done, IO(IOCompletions), Row, Step, } impl From> for InsnFunctionStepResult { fn from(value: IOResult) -> Self { match value { IOResult::Done(_) => InsnFunctionStepResult::Done, IOResult::IO(io) => InsnFunctionStepResult::IO(io), } } } pub fn op_init( _program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Init { target_pc }, insn); assert!(target_pc.is_offset()); state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_add( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Add { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_add(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_subtract( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Subtract { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_subtract(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_multiply( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Multiply { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_multiply(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_divide( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Divide { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_divide(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_index( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(DropIndex { index, db: _ }, insn); program .connection .with_schema_mut(|schema| schema.remove_index(index)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_remainder( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Remainder { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_remainder(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_and( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(BitAnd { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_bit_and(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_or( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(BitOr { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_bit_or(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_not( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(BitNot { reg, dest }, insn); state.registers[*dest] = Register::Value(state.registers[*reg].get_value().exec_bit_not()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug)] pub enum OpCheckpointState { StartCheckpoint, FinishCheckpoint { result: Option }, CompleteResult { result: Result }, } pub fn op_checkpoint( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { match op_checkpoint_inner(program, state, insn, pager, mv_store) { Ok(result) => Ok(result), Err(err) => { state.op_checkpoint_state = OpCheckpointState::StartCheckpoint; Err(err) } } } pub fn op_checkpoint_inner( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Checkpoint { database: _, checkpoint_mode, dest, }, insn ); if !program.connection.auto_commit.load(Ordering::SeqCst) { // TODO: sqlite returns "Runtime error: database table is locked (6)" when a table is in use // when a checkpoint is attempted. We don't have table locks, so return TableLocked for any // attempt to checkpoint in an interactive transaction. This does not end the transaction, // however. return Err(LimboError::TableLocked); } if let Some(mv_store) = mv_store { if !matches!(checkpoint_mode, CheckpointMode::Truncate { .. }) { return Err(LimboError::InvalidArgument( "Only TRUNCATE checkpoint mode is supported for MVCC".to_string(), )); } let mut ckpt_sm = StateMachine::new(CheckpointStateMachine::new( pager.clone(), mv_store.clone(), program.connection.clone(), true, )); loop { let result = ckpt_sm.step(&())?; match result { IOResult::IO(io) => { pager.io.step()?; } IOResult::Done(result) => { state.op_checkpoint_state = OpCheckpointState::CompleteResult { result: Ok(result) }; break; } } } } loop { match &mut state.op_checkpoint_state { OpCheckpointState::StartCheckpoint => { let step_result = program .connection .pager .load() .wal_checkpoint_start(*checkpoint_mode); match step_result { Ok(IOResult::Done(result)) => { state.op_checkpoint_state = OpCheckpointState::FinishCheckpoint { result: Some(result), }; continue; } Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(err) => { state.op_checkpoint_state = OpCheckpointState::CompleteResult { result: Err(err) }; continue; } } } OpCheckpointState::FinishCheckpoint { result } => { let step_result = program .connection .pager .load() .wal_checkpoint_finish(result.as_mut().unwrap()); match step_result { Ok(IOResult::Done(())) => { state.op_checkpoint_state = OpCheckpointState::CompleteResult { result: Ok(result.take().unwrap()), }; continue; } Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(err) => { state.op_checkpoint_state = OpCheckpointState::CompleteResult { result: Err(err) }; continue; } } } OpCheckpointState::CompleteResult { result } => { match result { Ok(CheckpointResult { num_attempted, num_backfilled, .. }) => { // https://sqlite.org/pragma.html#pragma_wal_checkpoint // 1st col: 1 (checkpoint SQLITE_BUSY) or 0 (not busy). state.registers[*dest] = Register::Value(Value::Integer(0)); // 2nd col: # modified pages written to wal file state.registers[*dest + 1] = Register::Value(Value::Integer(*num_attempted as i64)); // 3rd col: # pages moved to db after checkpoint state.registers[*dest + 2] = Register::Value(Value::Integer(*num_backfilled as i64)); } Err(_err) => state.registers[*dest] = Register::Value(Value::Integer(1)), } state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { match insn { Insn::Null { dest, dest_end } | Insn::BeginSubrtn { dest, dest_end } => { if let Some(dest_end) = dest_end { for i in *dest..=*dest_end { state.registers[i] = Register::Value(Value::Null); } } else { state.registers[*dest] = Register::Value(Value::Null); } } _ => unreachable!("unexpected Insn {:?}", insn), } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_null_row( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(NullRow { cursor_id }, insn); { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "NullRow"); let cursor = cursor.as_btree_mut(); cursor.set_null_flag(true); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_compare( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Compare { start_reg_a, start_reg_b, count, key_info, }, insn ); let start_reg_a = *start_reg_a; let start_reg_b = *start_reg_b; let count = *count; if start_reg_a + count > start_reg_b { return Err(LimboError::InternalError( "Compare registers overlap".to_string(), )); } let mut cmp = std::cmp::Ordering::Equal; for (i, key_col) in key_info.iter().enumerate().take(count) { // TODO (https://github.com/tursodatabase/turso/issues/2304): this logic is almost the same as compare_immutable() // but that one works on RefValue and this works on Value. There are tons of cases like this where we could reuse // functionality if we had a trait that both RefValue and Value implement. let a = state.registers[start_reg_a + i].get_value(); let b = state.registers[start_reg_b + i].get_value(); let column_order = key_col.sort_order; let collation = key_col.collation; cmp = match (a, b) { (Value::Text(left), Value::Text(right)) => { collation.compare_strings(left.as_str(), right.as_str()) } _ => a.partial_cmp(b).unwrap(), }; if !cmp.is_eq() { cmp = match column_order { SortOrder::Asc => cmp, SortOrder::Desc => cmp.reverse(), }; break; } } state.last_compare = Some(cmp); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_jump( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Jump { target_pc_lt, target_pc_eq, target_pc_gt, }, insn ); assert!(target_pc_lt.is_offset()); assert!(target_pc_eq.is_offset()); assert!(target_pc_gt.is_offset()); let cmp = state.last_compare.take(); if cmp.is_none() { return Err(LimboError::InternalError( "Jump without compare".to_string(), )); } let target_pc = match cmp.unwrap() { std::cmp::Ordering::Less => *target_pc_lt, std::cmp::Ordering::Equal => *target_pc_eq, std::cmp::Ordering::Greater => *target_pc_gt, }; state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_move( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Move { source_reg, dest_reg, count, }, insn ); let source_reg = *source_reg; let dest_reg = *dest_reg; let count = *count; for i in 0..count { state.registers[dest_reg + i] = std::mem::replace( &mut state.registers[source_reg + i], Register::Value(Value::Null), ); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_if_pos( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IfPos { reg, target_pc, decrement_by, }, insn ); assert!(target_pc.is_offset()); let reg = *reg; let target_pc = *target_pc; match state.registers[reg].get_value() { Value::Integer(n) if *n > 0 => { state.pc = target_pc.as_offset_int(); state.registers[reg] = Register::Value(Value::Integer(*n - *decrement_by as i64)); } Value::Integer(_) => { state.pc += 1; } _ => { return Err(LimboError::InternalError( "IfPos: the value in the register is not an integer".into(), )); } } Ok(InsnFunctionStepResult::Step) } pub fn op_not_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(NotNull { reg, target_pc }, insn); assert!(target_pc.is_offset()); let reg = *reg; let target_pc = *target_pc; match &state.registers[reg].get_value() { Value::Null => { state.pc += 1; } _ => { state.pc = target_pc.as_offset_int(); } } Ok(InsnFunctionStepResult::Step) } #[derive(Debug, Clone, Copy, PartialEq)] enum ComparisonOp { Eq, Ne, Lt, Le, Gt, Ge, } impl ComparisonOp { fn compare(&self, lhs: &Value, rhs: &Value, collation: &CollationSeq) -> bool { match (lhs, rhs) { (Value::Text(lhs_text), Value::Text(rhs_text)) => { let order = collation.compare_strings(lhs_text.as_str(), rhs_text.as_str()); match self { ComparisonOp::Eq => order.is_eq(), ComparisonOp::Ne => order.is_ne(), ComparisonOp::Lt => order.is_lt(), ComparisonOp::Le => order.is_le(), ComparisonOp::Gt => order.is_gt(), ComparisonOp::Ge => order.is_ge(), } } (_, _) => match self { ComparisonOp::Eq => *lhs == *rhs, ComparisonOp::Ne => *lhs != *rhs, ComparisonOp::Lt => *lhs < *rhs, ComparisonOp::Le => *lhs <= *rhs, ComparisonOp::Gt => *lhs > *rhs, ComparisonOp::Ge => *lhs >= *rhs, }, } } fn compare_integers(&self, lhs: &Value, rhs: &Value) -> bool { match self { ComparisonOp::Eq => lhs == rhs, ComparisonOp::Ne => lhs != rhs, ComparisonOp::Lt => lhs < rhs, ComparisonOp::Le => lhs <= rhs, ComparisonOp::Gt => lhs > rhs, ComparisonOp::Ge => lhs >= rhs, } } fn handle_nulls(&self, lhs: &Value, rhs: &Value, null_eq: bool, jump_if_null: bool) -> bool { match self { ComparisonOp::Eq => { let both_null = lhs == rhs; (null_eq && both_null) || (!null_eq && jump_if_null) } ComparisonOp::Ne => { let at_least_one_null = lhs != rhs; (null_eq && at_least_one_null) || (!null_eq && jump_if_null) } ComparisonOp::Lt | ComparisonOp::Le | ComparisonOp::Gt | ComparisonOp::Ge => { jump_if_null } } } } pub fn op_comparison( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { let (lhs, rhs, target_pc, flags, collation, op) = match insn { Insn::Eq { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Eq, ), Insn::Ne { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Ne, ), Insn::Lt { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Lt, ), Insn::Le { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Le, ), Insn::Gt { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Gt, ), Insn::Ge { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Ge, ), _ => unreachable!("unexpected Insn {:?}", insn), }; assert!(target_pc.is_offset()); let nulleq = flags.has_nulleq(); let jump_if_null = flags.has_jump_if_null(); let affinity = flags.get_affinity(); let lhs_value = state.registers[lhs].get_value(); let rhs_value = state.registers[rhs].get_value(); // Fast path for integers if matches!(lhs_value, Value::Integer(_)) && matches!(rhs_value, Value::Integer(_)) { if op.compare_integers(lhs_value, rhs_value) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } // Handle NULL values if matches!(lhs_value, Value::Null) || matches!(rhs_value, Value::Null) { if op.handle_nulls(lhs_value, rhs_value, nulleq, jump_if_null) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } let mut lhs_temp_reg = None; let mut rhs_temp_reg = None; let mut lhs_converted = false; let mut rhs_converted = false; // Apply affinity conversions match affinity { Affinity::Numeric | Affinity::Integer => { let lhs_is_text = matches!(state.registers[lhs].get_value(), Value::Text(_)); let rhs_is_text = matches!(state.registers[rhs].get_value(), Value::Text(_)); if lhs_is_text || rhs_is_text { if lhs_is_text { lhs_temp_reg = Some(state.registers[lhs].clone()); lhs_converted = apply_numeric_affinity(lhs_temp_reg.as_mut().unwrap(), false); } if rhs_is_text { rhs_temp_reg = Some(state.registers[rhs].clone()); rhs_converted = apply_numeric_affinity(rhs_temp_reg.as_mut().unwrap(), false); } } } Affinity::Text => { let lhs_is_text = matches!(state.registers[lhs].get_value(), Value::Text(_)); let rhs_is_text = matches!(state.registers[rhs].get_value(), Value::Text(_)); if lhs_is_text || rhs_is_text { if is_numeric_value(&state.registers[lhs]) { lhs_temp_reg = Some(state.registers[lhs].clone()); lhs_converted = stringify_register(lhs_temp_reg.as_mut().unwrap()); } if is_numeric_value(&state.registers[rhs]) { rhs_temp_reg = Some(state.registers[rhs].clone()); rhs_converted = stringify_register(rhs_temp_reg.as_mut().unwrap()); } } } Affinity::Real => { if matches!(state.registers[lhs].get_value(), Value::Text(_)) { lhs_temp_reg = Some(state.registers[lhs].clone()); lhs_converted = apply_numeric_affinity(lhs_temp_reg.as_mut().unwrap(), false); } if matches!(state.registers[rhs].get_value(), Value::Text(_)) { rhs_temp_reg = Some(state.registers[rhs].clone()); rhs_converted = apply_numeric_affinity(rhs_temp_reg.as_mut().unwrap(), false); } if let Value::Integer(i) = (lhs_temp_reg.as_ref().unwrap_or(&state.registers[lhs])).get_value() { lhs_temp_reg = Some(Register::Value(Value::Float(*i as f64))); lhs_converted = true; } if let Value::Integer(i) = rhs_temp_reg .as_ref() .unwrap_or(&state.registers[rhs]) .get_value() { rhs_temp_reg = Some(Register::Value(Value::Float(*i as f64))); rhs_converted = true; } } Affinity::Blob => {} // Do nothing for blob affinity. } let should_jump = op.compare( lhs_temp_reg .as_ref() .unwrap_or(&state.registers[lhs]) .get_value(), rhs_temp_reg .as_ref() .unwrap_or(&state.registers[rhs]) .get_value(), &collation, ); if lhs_converted { state.registers[lhs] = lhs_temp_reg.unwrap(); } if rhs_converted { state.registers[rhs] = rhs_temp_reg.unwrap(); } if should_jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_if( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( If { reg, target_pc, jump_if_null, }, insn ); assert!(target_pc.is_offset()); if state.registers[*reg] .get_value() .exec_if(*jump_if_null, false) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_if_not( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IfNot { reg, target_pc, jump_if_null, }, insn ); assert!(target_pc.is_offset()); if state.registers[*reg] .get_value() .exec_if(*jump_if_null, true) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_open_read( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( OpenRead { cursor_id, root_page, db, }, insn ); let pager = program.get_pager_from_database_index(db); if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.open_read(&program.connection)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); if program.connection.get_mv_tx_id().is_none() { assert!(*root_page >= 0, ""); } let cursors = &mut state.cursors; let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::BTreeIndex(index_arc) => index_arc.columns.len(), CursorType::MaterializedView(table_rc, _) => table_rc.columns.len(), _ => unreachable!("This should not have happened"), }; let maybe_promote_to_mvcc_cursor = |btree_cursor: Box| -> Result> { if let Some(tx_id) = program.connection.get_mv_tx_id() { let mv_store = mv_store.unwrap().clone(); Ok(Box::new(MvCursor::new( mv_store, tx_id, *root_page, pager.clone(), btree_cursor, )?)) } else { Ok(btree_cursor) } }; match cursor_type { CursorType::MaterializedView(_, view_mutex) => { // This is a materialized view with storage // Create btree cursor for reading the persistent data let btree_cursor = Box::new(BTreeCursor::new_table( pager.clone(), *root_page, num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor)?; // Get the view name and look up or create its transaction state let view_name = view_mutex.lock().unwrap().name().to_string(); let tx_state = program .connection .view_transaction_states .get_or_create(&view_name); // Create materialized view cursor with this view's transaction state let mv_cursor = crate::incremental::cursor::MaterializedViewCursor::new( cursor, view_mutex.clone(), pager.clone(), tx_state, )?; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_materialized_view(mv_cursor)); } CursorType::BTreeTable(_) => { // Regular table let btree_cursor = Box::new(BTreeCursor::new_table( pager.clone(), *root_page, num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor)?; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(index) => { let btree_cursor = Box::new(BTreeCursor::new_index( pager.clone(), *root_page, index.as_ref(), num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor)?; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } CursorType::Pseudo(_) => { panic!("OpenRead on pseudo cursor"); } CursorType::Sorter => { panic!("OpenRead on sorter cursor"); } CursorType::IndexMethod(..) => { unreachable!("IndexMethod handled above") } CursorType::VirtualTable(_) => { panic!("OpenRead on virtual table cursor, use Insn:VOpen instead"); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vopen( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(VOpen { cursor_id }, insn); let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); let CursorType::VirtualTable(virtual_table) = cursor_type else { panic!("VOpen on non-virtual table cursor"); }; let cursor = virtual_table.open(program.connection.clone())?; state .cursors .get_mut(*cursor_id) .unwrap_or_else(|| panic!("cursor id {} out of bounds", *cursor_id)) .replace(Cursor::Virtual(cursor)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vcreate( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VCreate { module_name, table_name, args_reg, }, insn ); let module_name = state.registers[*module_name].get_value().to_string(); let table_name = state.registers[*table_name].get_value().to_string(); let args = if let Some(args_reg) = args_reg { if let Register::Record(rec) = &state.registers[*args_reg] { rec.get_values().iter().map(|v| v.to_ffi()).collect() } else { return Err(LimboError::InternalError( "VCreate: args_reg is not a record".to_string(), )); } } else { vec![] }; let conn = program.connection.clone(); let table = crate::VirtualTable::table(Some(&table_name), &module_name, args, &conn.syms.read())?; { conn.syms.write().vtabs.insert(table_name, table.clone()); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vfilter( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VFilter { cursor_id, pc_if_empty, arg_count, args_reg, idx_str, idx_num, }, insn ); let has_rows = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_virtual_mut(); let mut args = Vec::with_capacity(*arg_count); for i in 0..*arg_count { args.push(state.registers[args_reg + i].get_value().clone()); } let idx_str = if let Some(idx_str) = idx_str { Some(state.registers[*idx_str].get_value().to_string()) } else { None }; cursor.filter(*idx_num as i32, idx_str, *arg_count, args)? }; // Increment filter_operations metric for virtual table filter state.metrics.filter_operations = state.metrics.filter_operations.saturating_add(1); if !has_rows { state.pc = pc_if_empty.as_offset_int(); } else { // VFilter positions to the first row if any exist, which counts as a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_vcolumn( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VColumn { cursor_id, column, dest, }, insn ); let value = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); cursor.column(*column)? }; state.registers[*dest] = Register::Value(value); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vupdate( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VUpdate { cursor_id, arg_count, start_reg, conflict_action, .. }, insn ); let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); let CursorType::VirtualTable(virtual_table) = cursor_type else { panic!("VUpdate on non-virtual table cursor"); }; if virtual_table.readonly() { return Err(LimboError::ReadOnly); } if *arg_count < 2 { return Err(LimboError::InternalError( "VUpdate: arg_count must be at least 2 (rowid and insert_rowid)".to_string(), )); } let mut argv = Vec::with_capacity(*arg_count); for i in 0..*arg_count { if let Some(value) = state.registers.get(*start_reg + i) { argv.push(value.get_value().clone()); } else { return Err(LimboError::InternalError(format!( "VUpdate: register out of bounds at {}", *start_reg + i ))); } } let result = virtual_table.update(&argv); match result { Ok(Some(new_rowid)) => { if *conflict_action == 5 { // ResolveType::Replace program.connection.update_last_rowid(new_rowid); } state.pc += 1; } Ok(None) => { // no-op or successful update without rowid return state.pc += 1; } Err(e) => { // virtual table update failed return Err(LimboError::ExtensionError(format!( "Virtual table update failed: {e}" ))); } } Ok(InsnFunctionStepResult::Step) } pub fn op_vnext( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VNext { cursor_id, pc_if_next, }, insn ); let has_more = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); cursor.next()? }; if has_more { // Increment metrics for row read from virtual table (including materialized views) state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_vdestroy( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(VDestroy { db, table_name }, insn); let conn = program.connection.clone(); { let Some(vtab) = conn.syms.write().vtabs.remove(table_name) else { return Err(crate::LimboError::InternalError( "Could not find Virtual Table to Destroy".to_string(), )); }; vtab.destroy()?; } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vbegin( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(VBegin { cursor_id }, insn); let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); let vtab_id = cursor .vtab_id() .expect("VBegin on non ext-virtual table cursor"); let mut states = program.connection.vtab_txn_states.write(); if states.insert(vtab_id) { // Only begin a new transaction if one is not already active for this virtual table module let vtabs = &program.connection.syms.read().vtabs; let vtab = vtabs .iter() .find(|p| p.1.id().eq(&vtab_id)) .expect("Could not find virtual table for VBegin"); vtab.1.begin()?; } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vrename( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( VRename { cursor_id, new_name_reg }, insn ); let name = state.registers[*new_name_reg].get_value().to_string(); let conn = program.connection.clone(); let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); let vtabs = &program.connection.syms.read().vtabs; let vtab = vtabs .iter() .find(|p| { p.1.id().eq(&cursor .vtab_id() .expect("non ext-virtual table used in VRollback")) }) .expect("Could not find virtual table for VRollback"); vtab.1.rename(&name)?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_open_pseudo( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( OpenPseudo { cursor_id, content_reg: _, num_fields: _, }, insn ); { let cursors = &mut state.cursors; let cursor = PseudoCursor::default(); cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_pseudo(cursor)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_rewind( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Rewind { cursor_id, pc_if_empty, }, insn ); assert!(pc_if_empty.is_offset()); let is_empty = { let cursor = state.get_cursor(*cursor_id); match cursor { Cursor::BTree(btree_cursor) => { return_if_io!(btree_cursor.rewind()); btree_cursor.is_empty() } Cursor::MaterializedView(mv_cursor) => { return_if_io!(mv_cursor.rewind()); !mv_cursor.is_valid()? } _ => panic!("Rewind on non-btree/materialized-view cursor"), } }; if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { // Rewind positions to the first row, which is effectively a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_last( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Last { cursor_id, pc_if_empty, }, insn ); assert!(pc_if_empty.is_offset()); let is_empty = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Last"); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.last()); cursor.is_empty() }; if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { // Last positions to the last row, which is effectively a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } #[derive(Debug, Clone, Copy)] pub enum OpColumnState { Start, Rowid { index_cursor_id: usize, table_cursor_id: usize, }, Seek { rowid: i64, table_cursor_id: usize, }, GetColumn, } pub fn op_column( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Column { cursor_id, column, dest, default, }, insn ); 'outer: loop { match state.op_column_state { OpColumnState::Start => { if let Some((index_cursor_id, table_cursor_id)) = state.deferred_seeks[*cursor_id].take() { state.op_column_state = OpColumnState::Rowid { index_cursor_id, table_cursor_id, }; } else { state.op_column_state = OpColumnState::GetColumn; } } OpColumnState::Rowid { index_cursor_id, table_cursor_id, } => { let Some(rowid) = ({ let index_cursor = state.get_cursor(index_cursor_id); match index_cursor { Cursor::BTree(cursor) => return_if_io!(cursor.rowid()), Cursor::IndexMethod(cursor) => return_if_io!(cursor.query_rowid()), _ => panic!("unexpected cursor type"), } }) else { state.registers[*dest] = Register::Value(Value::Null); break 'outer; }; state.op_column_state = OpColumnState::Seek { rowid, table_cursor_id, }; } OpColumnState::Seek { rowid, table_cursor_id, } => { { let table_cursor = state.get_cursor(table_cursor_id); // MaterializedView cursors shouldn't go through deferred seek logic // but if we somehow get here, handle it appropriately match table_cursor { Cursor::MaterializedView(mv_cursor) => { // Seek to the rowid in the materialized view return_if_io!(mv_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); } _ => { // Regular btree cursor let table_cursor = table_cursor.as_btree_mut(); return_if_io!(table_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); } } } state.op_column_state = OpColumnState::GetColumn; } OpColumnState::GetColumn => { // First check if this is a MaterializedViewCursor { let cursor = state.get_cursor(*cursor_id); if let Cursor::MaterializedView(mv_cursor) = cursor { // Handle materialized view column access let value = return_if_io!(mv_cursor.column(*column)); state.registers[*dest] = Register::Value(value); break 'outer; } // Fall back to normal handling } let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); match cursor_type { CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) | CursorType::MaterializedView(_, _) => { 'ifnull: { let cursor_ref = must_be_btree_cursor!( *cursor_id, program.cursor_ref, state, "Column" ); let cursor = cursor_ref.as_btree_mut(); if cursor.get_null_flag() { state.registers[*dest] = Register::Value(Value::Null); break 'outer; } let record_result = return_if_io!(cursor.record()); let Some(payload) = record_result.as_ref().map(|r| r.get_payload()) else { break 'ifnull; }; let mut record_cursor = cursor.record_cursor_mut(); if record_cursor.offsets.is_empty() { let (header_size, header_len_bytes) = read_varint_fast(payload)?; let header_size = header_size as usize; debug_assert!(header_size <= payload.len() && header_size <= 98307, "header_size: {header_size}, header_len_bytes: {header_len_bytes}, payload.len(): {}", payload.len()); record_cursor.header_size = header_size; record_cursor.header_offset = header_len_bytes; record_cursor.offsets.push(header_size); } let target_column = *column; let mut parse_pos = record_cursor.header_offset; let mut data_offset = record_cursor .offsets .last() .copied() .expect("header_offset must be set"); // Parse the header for serial types incrementally until we have the target column while record_cursor.serial_types.len() <= target_column && parse_pos < record_cursor.header_size { let (serial_type, varint_len) = read_varint_fast(&payload[parse_pos..])?; record_cursor.serial_types.push(serial_type); parse_pos += varint_len; let data_size = match serial_type { // NULL 0 => 0, // I8 1 => 1, // I16 2 => 2, // I24 3 => 3, // I32 4 => 4, // I48 5 => 6, // I64 6 => 8, // F64 7 => 8, // CONST_INT0 8 => 0, // CONST_INT1 9 => 0, // BLOB n if n >= 12 && n & 1 == 0 => (n - 12) >> 1, // TEXT n if n >= 13 && n & 1 == 1 => (n - 13) >> 1, // Reserved 10 | 11 => { return Err(LimboError::Corrupt(format!( "Reserved serial type: {serial_type}" ))) } _ => unreachable!("Invalid serial type: {serial_type}"), } as usize; data_offset += data_size; record_cursor.offsets.push(data_offset); } debug_assert!( parse_pos <= record_cursor.header_size, "parse_pos: {parse_pos}, header_size: {}", record_cursor.header_size ); record_cursor.header_offset = parse_pos; if target_column >= record_cursor.serial_types.len() { break 'ifnull; } let start_offset = record_cursor.offsets[target_column]; let end_offset = record_cursor.offsets[target_column + 1]; // SAFETY: We know that the payload is valid until the next row is processed. let buf = unsafe { std::mem::transmute::<&[u8], &'static [u8]>( &payload[start_offset..end_offset], ) }; let serial_type = record_cursor.serial_types[target_column]; drop(record_result); drop(record_cursor); match serial_type { // NULL 0 => { state.registers[*dest] = Register::Value(Value::Null); } // I8 1 => { state.registers[*dest] = Register::Value(Value::Integer(buf[0] as i8 as i64)); } // I16 2 => { state.registers[*dest] = Register::Value(Value::Integer(i16::from_be_bytes([ buf[0], buf[1], ]) as i64)); } // I24 3 => { let sign_extension = (buf[0] > 0x7F) as u8 * 0xFF; let value = Value::Integer(i32::from_be_bytes([ sign_extension, buf[0], buf[1], buf[2], ]) as i64); state.registers[*dest] = Register::Value(value); } // I32 4 => { let value = Value::Integer(i32::from_be_bytes([ buf[0], buf[1], buf[2], buf[3], ]) as i64); state.registers[*dest] = Register::Value(value); } // I48 5 => { let sign_extension = (buf[0] > 0x7F) as u8 * 0xFF; let value = Value::Integer(i64::from_be_bytes([ sign_extension, sign_extension, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], ])); state.registers[*dest] = Register::Value(value); } // I64 6 => { let value = Value::Integer(i64::from_be_bytes( buf[..8].try_into().unwrap(), )); state.registers[*dest] = Register::Value(value); } // F64 7 => { let value = Value::Float(f64::from_be_bytes( buf[..8].try_into().unwrap(), )); state.registers[*dest] = Register::Value(value); } // CONST_INT0 8 => { state.registers[*dest] = Register::Value(Value::Integer(0)); } // CONST_INT1 9 => { state.registers[*dest] = Register::Value(Value::Integer(1)); } // BLOB n if n >= 12 && n & 1 == 0 => { // Try to reuse the registers when allocation is not needed. match state.registers[*dest] { Register::Value(Value::Blob(ref mut existing_blob)) => { existing_blob.do_extend(&buf); } _ => { state.registers[*dest] = Register::Value(Value::Blob(buf.to_vec())); } } } // TEXT n if n >= 13 && n & 1 == 1 => { // Try to reuse the registers when allocation is not needed. match state.registers[*dest] { Register::Value(Value::Text(ref mut existing_text)) => { // SAFETY: We know the text is valid UTF-8 because we only accept valid UTF-8 and the serial type is TEXT. let text = unsafe { std::str::from_utf8_unchecked(buf) }; existing_text.do_extend(&text); } _ => { // SAFETY: We know the text is valid UTF-8 because we only accept valid UTF-8 and the serial type is TEXT. let text = unsafe { std::str::from_utf8_unchecked(buf) }; state.registers[*dest] = Register::Value(Value::Text(Text::new(text))); } } } _ => panic!("Invalid serial type: {serial_type}"), } break 'outer; }; // DEFAULT handling. Try to reuse the registers when allocation is not needed. let Some(ref default) = default else { state.registers[*dest] = Register::Value(Value::Null); break; }; match (default, &mut state.registers[*dest]) { ( Value::Text(new_text), Register::Value(Value::Text(existing_text)), ) => { existing_text.do_extend(new_text); } ( Value::Blob(new_blob), Register::Value(Value::Blob(existing_blob)), ) => { existing_blob.do_extend(new_blob); } _ => { state.registers[*dest] = Register::Value(default.clone()); } } break; } CursorType::Sorter => { let record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); cursor.record().cloned() }; if let Some(record) = record { state.registers[*dest] = Register::Value(match record.get_value_opt(*column) { Some(val) => val.to_owned(), None => default.clone().unwrap_or(Value::Null), }); } else { state.registers[*dest] = Register::Value(Value::Null); } } CursorType::Pseudo(_) => { let value = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_pseudo_mut(); cursor.get_value(*column)? }; state.registers[*dest] = Register::Value(value); } CursorType::IndexMethod(..) => { let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); let value = return_if_io!(cursor.query_column(*column)); state.registers[*dest] = Register::Value(value); } CursorType::VirtualTable(_) => { panic!("Insn:Column on virtual table cursor, use Insn:VColumn instead"); } } break; } } } state.op_column_state = OpColumnState::Start; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_type_check( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( TypeCheck { start_reg, count, check_generated, table_reference, }, insn ); assert!(table_reference.is_strict); state.registers[*start_reg..*start_reg + *count] .iter_mut() .zip(table_reference.columns.iter()) .try_for_each(|(reg, col)| { // INT PRIMARY KEY is not row_id_alias so we throw error if this col is NULL if !col.is_rowid_alias() && col.primary_key() && matches!(reg.get_value(), Value::Null) { bail_constraint_error!( "NOT NULL constraint failed: {}.{} ({})", &table_reference.name, col.name.as_deref().unwrap_or(""), SQLITE_CONSTRAINT ) } else if col.is_rowid_alias() && matches!(reg.get_value(), Value::Null) { // Handle INTEGER PRIMARY KEY for null as usual (Rowid will be auto-assigned) return Ok(()); } let col_affinity = col.affinity(); let ty_str = &col.ty_str; let ty_bytes = ty_str.as_bytes(); let applied = apply_affinity_char(reg, col_affinity); let value_type = reg.get_value().value_type(); match_ignore_ascii_case!(match ty_bytes { b"INTEGER" | b"INT" if value_type == ValueType::Integer => {} b"REAL" if value_type == ValueType::Float => {} b"BLOB" if value_type == ValueType::Blob => {} b"TEXT" if value_type == ValueType::Text => {} b"ANY" => {} _ => bail_constraint_error!( "cannot store {} value in {} column {}.{} ({})", value_type, ty_str, &table_reference.name, col.name.as_deref().unwrap_or(""), SQLITE_CONSTRAINT ), }); Ok(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_make_record( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( MakeRecord { start_reg, count, dest_reg, affinity_str, .. }, insn ); if let Some(affinity_str) = affinity_str { if affinity_str.len() != *count { return Err(LimboError::InternalError(format!( "MakeRecord: the length of affinity string ({}) does not match the count ({})", affinity_str.len(), *count ))); } for (i, affinity_ch) in affinity_str.chars().enumerate().take(*count) { let reg_index = *start_reg + i; let affinity = Affinity::from_char(affinity_ch); apply_affinity_char(&mut state.registers[reg_index], affinity); } } let record = make_record(&state.registers, start_reg, count); state.registers[*dest_reg] = Register::Record(record); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_mem_max( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(MemMax { dest_reg, src_reg }, insn); let dest_val = state.registers[*dest_reg].get_value(); let src_val = state.registers[*src_reg].get_value(); let dest_int = extract_int_value(dest_val); let src_int = extract_int_value(src_val); if dest_int < src_int { state.registers[*dest_reg] = Register::Value(Value::Integer(src_int)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_result_row( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ResultRow { start_reg, count }, insn); let row = Row { values: &state.registers[*start_reg] as *const Register, count: *count, }; state.result_row = Some(row); state.pc += 1; Ok(InsnFunctionStepResult::Row) } pub fn op_next( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Next { cursor_id, pc_if_next, }, insn ); assert!(pc_if_next.is_offset()); let is_empty = { let cursor = state.get_cursor(*cursor_id); match cursor { Cursor::BTree(btree_cursor) => { btree_cursor.set_null_flag(false); return_if_io!(btree_cursor.next()); btree_cursor.is_empty() } Cursor::MaterializedView(mv_cursor) => { let has_more = return_if_io!(mv_cursor.next()); !has_more } Cursor::IndexMethod(_) => { let cursor = cursor.as_index_method_mut(); let has_more = return_if_io!(cursor.query_next()); !has_more } _ => panic!("Next on non-btree/materialized-view cursor"), } }; if !is_empty { // Increment metrics for row read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.metrics.btree_next = state.metrics.btree_next.saturating_add(1); // Track if this is a full table scan or index scan if let Some((_, cursor_type)) = program.cursor_ref.get(*cursor_id) { if cursor_type.is_index() { state.metrics.index_steps = state.metrics.index_steps.saturating_add(1); } else if matches!(cursor_type, CursorType::BTreeTable(_)) { state.metrics.fullscan_steps = state.metrics.fullscan_steps.saturating_add(1); } } state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_prev( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Prev { cursor_id, pc_if_prev, }, insn ); assert!(pc_if_prev.is_offset()); let is_empty = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Prev"); let cursor = cursor.as_btree_mut(); cursor.set_null_flag(false); return_if_io!(cursor.prev()); cursor.is_empty() }; if !is_empty { // Increment metrics for row read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.metrics.btree_prev = state.metrics.btree_prev.saturating_add(1); // Track if this is a full table scan or index scan if let Some((_, cursor_type)) = program.cursor_ref.get(*cursor_id) { if cursor_type.is_index() { state.metrics.index_steps = state.metrics.index_steps.saturating_add(1); } else if matches!(cursor_type, CursorType::BTreeTable(_)) { state.metrics.fullscan_steps = state.metrics.fullscan_steps.saturating_add(1); } } state.pc = pc_if_prev.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn halt( program: &Program, state: &mut ProgramState, pager: &Arc, mv_store: Option<&Arc>, err_code: usize, description: &str, ) -> Result { if err_code > 0 { // Any non-FK constraint violation causes the statement subtransaction to roll back. state.end_statement(&program.connection, pager, EndStatement::RollbackSavepoint)?; vtab_rollback_all(&program.connection, state)?; } match err_code { 0 => {} SQLITE_CONSTRAINT_PRIMARYKEY => { return Err(LimboError::Constraint(format!( "UNIQUE constraint failed: {description} (19)" ))); } SQLITE_CONSTRAINT_NOTNULL => { return Err(LimboError::Constraint(format!( "NOT NULL constraint failed: {description} (19)" ))); } SQLITE_CONSTRAINT_UNIQUE => { return Err(LimboError::Constraint(format!( "UNIQUE constraint failed: {description} (19)" ))); } _ => { return Err(LimboError::Constraint(format!( "undocumented halt error code {description}" ))); } } let auto_commit = program.connection.auto_commit.load(Ordering::SeqCst); tracing::trace!("halt(auto_commit={})", auto_commit); // Check for immediate foreign key violations. // Any immediate violation causes the statement subtransaction to roll back. if program.connection.foreign_keys_enabled() && state .fk_immediate_violations_during_stmt .load(Ordering::Acquire) > 0 { state.end_statement(&program.connection, pager, EndStatement::RollbackSavepoint)?; return Err(LimboError::Constraint( "foreign key constraint failed".to_string(), )); } if auto_commit { // In autocommit mode, a statement that leaves deferred violations must fail here, // and it also ends the transaction. if program.connection.foreign_keys_enabled() { let deferred_violations = program .connection .fk_deferred_violations .swap(0, Ordering::AcqRel); if deferred_violations > 0 { vtab_rollback_all(&program.connection, state)?; pager.rollback_tx(&program.connection); program.connection.set_tx_state(TransactionState::None); program.connection.auto_commit.store(true, Ordering::SeqCst); return Err(LimboError::Constraint( "foreign key constraint failed".to_string(), )); } } state.end_statement(&program.connection, pager, EndStatement::ReleaseSavepoint)?; vtab_commit_all(&program.connection, state)?; program .commit_txn(pager.clone(), state, mv_store, false) .map(Into::into) } else { // Even if deferred violations are present, the statement subtransaction completes successfully when // it is part of an interactive transaction. state.end_statement(&program.connection, pager, EndStatement::ReleaseSavepoint)?; Ok(InsnFunctionStepResult::Done) } } /// Call xCommit on all virtual tables that participated in the current transaction. fn vtab_commit_all(conn: &Connection, state: &mut ProgramState) -> crate::Result<()> { let mut set = conn.vtab_txn_states.write(); if set.is_empty() { return Ok(()); } let reg = &conn.syms.read().vtabs; for id in set.drain() { let vtab = reg .iter() .find(|(_, vtab)| vtab.id() == id) .expect("vtab must exist"); vtab.1.commit()?; } Ok(()) } /// Rollback all virtual tables that are part of the current transaction. fn vtab_rollback_all(conn: &Connection, state: &mut ProgramState) -> crate::Result<()> { let mut set = conn.vtab_txn_states.write(); if set.is_empty() { return Ok(()); } let reg = &conn.syms.read().vtabs; for id in set.drain() { let vtab = reg .iter() .find(|(_, vtab)| vtab.id() == id) .expect("vtab must exist"); vtab.1.rollback()?; } Ok(()) } pub fn op_halt( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Halt { err_code, description, }, insn ); halt(program, state, pager, mv_store, *err_code, description) } pub fn op_halt_if_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( HaltIfNull { target_reg, err_code, description, }, insn ); if state.registers[*target_reg].get_value() == &Value::Null { halt(program, state, pager, mv_store, *err_code, description) } else { state.pc += 1; Ok(InsnFunctionStepResult::Step) } } #[derive(Debug, Clone, Copy)] pub enum OpTransactionState { Start, CheckSchemaCookie, BeginStatement, } pub fn op_transaction( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { match op_transaction_inner(program, state, insn, pager, mv_store) { Ok(result) => Ok(result), Err(err) => { state.op_transaction_state = OpTransactionState::Start; Err(err) } } } pub fn op_transaction_inner( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Transaction { db, tx_mode, schema_cookie, }, insn ); let pager = program.get_pager_from_database_index(db); loop { match state.op_transaction_state { OpTransactionState::Start => { let conn = program.connection.clone(); let write = matches!(tx_mode, TransactionMode::Write); if write && conn.db.open_flags.get().contains(OpenFlags::ReadOnly) { return Err(LimboError::ReadOnly); } // 1. We try to upgrade current version let current_state = conn.get_tx_state(); let (new_transaction_state, updated) = if conn.is_nested_stmt() { (current_state, false) } else { match (current_state, write) { // pending state means that we tried beginning a tx and the method returned IO. // instead of ending the read tx, just update the state to pending. (TransactionState::PendingUpgrade, write) => { turso_assert!( write, "pending upgrade should only be set for write transactions" ); ( TransactionState::Write { schema_did_change: false, }, true, ) } (TransactionState::Write { schema_did_change }, true) => { (TransactionState::Write { schema_did_change }, false) } (TransactionState::Write { schema_did_change }, false) => { (TransactionState::Write { schema_did_change }, false) } (TransactionState::Read, true) => ( TransactionState::Write { schema_did_change: false, }, true, ), (TransactionState::Read, false) => (TransactionState::Read, false), (TransactionState::None, true) => ( TransactionState::Write { schema_did_change: false, }, true, ), (TransactionState::None, false) => (TransactionState::Read, true), } }; // 2. Start transaction if needed if let Some(mv_store) = &mv_store { // In MVCC we don't have write exclusivity, therefore we just need to start a transaction if needed. // Programs can run Transaction twice, first with read flag and then with write flag. So a single txid is enough // for both. let current_mv_tx = program.connection.get_mv_tx(); let has_existing_mv_tx = current_mv_tx.is_some(); let conn_has_executed_begin_deferred = !has_existing_mv_tx && !program.connection.auto_commit.load(Ordering::SeqCst); if conn_has_executed_begin_deferred && *tx_mode == TransactionMode::Concurrent { return Err(LimboError::TxError( "Cannot start CONCURRENT transaction after BEGIN DEFERRED".to_string(), )); } if !has_existing_mv_tx { let tx_id = match tx_mode { TransactionMode::None | TransactionMode::Read | TransactionMode::Concurrent => mv_store.begin_tx(pager.clone())?, TransactionMode::Write => { mv_store.begin_exclusive_tx(pager.clone(), None)? } }; *program.connection.mv_tx.write() = Some((tx_id, *tx_mode)); } else if updated { // TODO: fix tx_mode in Insn::Transaction, now each statement overrides it even if there's already a CONCURRENT Tx in progress, for example let (tx_id, mv_tx_mode) = current_mv_tx.unwrap(); let actual_tx_mode = if mv_tx_mode == TransactionMode::Concurrent { TransactionMode::Concurrent } else { *tx_mode }; if matches!(new_transaction_state, TransactionState::Write { .. }) && matches!(actual_tx_mode, TransactionMode::Write) { mv_store.begin_exclusive_tx(pager.clone(), Some(tx_id))?; } } } else { if matches!(tx_mode, TransactionMode::Concurrent) { return Err(LimboError::TxError( "Concurrent transaction mode is only supported when MVCC is enabled" .to_string(), )); } if updated && matches!(current_state, TransactionState::None) { turso_assert!( !conn.is_nested_stmt(), "nested stmt should not begin a new read transaction" ); pager.begin_read_tx()?; state.auto_txn_cleanup = TxnCleanup::RollbackTxn; } if updated && matches!(new_transaction_state, TransactionState::Write { .. }) { turso_assert!( !conn.is_nested_stmt(), "nested stmt should not begin a new write transaction" ); let begin_w_tx_res = pager.begin_write_tx(); if let Err(LimboError::Busy) = begin_w_tx_res { // We failed to upgrade to write transaction so put the transaction into its original state. // That is, if the transaction had not started, end the read transaction so that next time we // start a new one. if matches!(current_state, TransactionState::None) { pager.end_read_tx(); conn.set_tx_state(TransactionState::None); state.auto_txn_cleanup = TxnCleanup::None; } assert_eq!(conn.get_tx_state(), current_state); return Err(LimboError::Busy); } if let IOResult::IO(io) = begin_w_tx_res? { // set the transaction state to pending so we don't have to // end the read transaction. program .connection .set_tx_state(TransactionState::PendingUpgrade); return Ok(InsnFunctionStepResult::IO(io)); } } } // 3. Transaction state should be updated before checking for Schema cookie so that the tx is ended properly on error if updated { conn.set_tx_state(new_transaction_state); } state.op_transaction_state = OpTransactionState::CheckSchemaCookie; continue; } // 4. Check whether schema has changed if we are actually going to access the database. // Can only read header if page 1 has been allocated already // begin_write_tx that happens, but not begin_read_tx OpTransactionState::CheckSchemaCookie => { let res = get_schema_cookie(&pager, mv_store, program); match res { Ok(IOResult::Done(header_schema_cookie)) => { if header_schema_cookie != *schema_cookie { tracing::debug!( "schema changed, force reprepare: {} != {}", header_schema_cookie, *schema_cookie ); return Err(LimboError::SchemaUpdated); } } Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), // This means we are starting a read_tx and we do not have a page 1 yet, so we just continue execution Err(LimboError::Page1NotAlloc) => {} Err(err) => { return Err(err); } } state.op_transaction_state = OpTransactionState::BeginStatement; } OpTransactionState::BeginStatement => { if program.needs_stmt_subtransactions && mv_store.is_none() { let write = matches!(tx_mode, TransactionMode::Write); let res = state.begin_statement(&program.connection, &pager, write)?; if let IOResult::IO(io) = res { return Ok(InsnFunctionStepResult::IO(io)); } } state.pc += 1; state.op_transaction_state = OpTransactionState::Start; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_auto_commit( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( AutoCommit { auto_commit, rollback }, insn ); let conn = program.connection.clone(); let fk_on = conn.foreign_keys_enabled(); let had_autocommit = conn.auto_commit.load(Ordering::SeqCst); // true, not in tx // Drive any multi-step commit/rollback that’s already in progress. if matches!(state.commit_state, CommitState::Committing) { let res = program .commit_txn(pager.clone(), state, mv_store, *rollback) .map(Into::into); // Only clear after a final, successful non-rollback COMMIT. if fk_on && !*rollback && matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) { conn.clear_deferred_foreign_key_violations(); } return res; } // The logic in this opcode can be a bit confusing, so to make things a bit clearer lets be // very explicit about the currently existing and requested state. let requested_autocommit = *auto_commit; let requested_rollback = *rollback; let changed = requested_autocommit != had_autocommit; // what the requested operation is let is_begin_req = had_autocommit && !requested_autocommit && !requested_rollback; let is_commit_req = !had_autocommit && requested_autocommit && !requested_rollback; let is_rollback_req = !had_autocommit && requested_autocommit && requested_rollback; if changed { if requested_rollback { // ROLLBACK transition if let Some(mv_store) = mv_store { if let Some(tx_id) = conn.get_mv_tx_id() { mv_store.rollback_tx(tx_id, pager.clone(), &conn); } } else { pager.rollback_tx(&conn); } conn.set_tx_state(TransactionState::None); conn.auto_commit.store(true, Ordering::SeqCst); } else { // BEGIN (true->false) or COMMIT (false->true) if is_commit_req { // Pre-check deferred FKs; leave tx open and do NOT clear violations check_deferred_fk_on_commit(&conn)?; } conn.auto_commit .store(requested_autocommit, Ordering::SeqCst); } } else { // No autocommit flip let mvcc_tx_active = conn.get_mv_tx().is_some(); if !mvcc_tx_active { if !requested_autocommit { return Err(LimboError::TxError( "cannot start a transaction within a transaction".to_string(), )); } else if requested_rollback { return Err(LimboError::TxError( "cannot rollback - no transaction is active".to_string(), )); } else { return Err(LimboError::TxError( "cannot commit - no transaction is active".to_string(), )); } } else if is_begin_req { return Err(LimboError::TxError( "cannot use BEGIN after BEGIN CONCURRENT".to_string(), )); } } let res = program .commit_txn(pager.clone(), state, mv_store, requested_rollback) .map(Into::into); // Clear deferred FK counters only after FINAL success of COMMIT/ROLLBACK. if fk_on && matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) && (is_rollback_req || is_commit_req) { conn.clear_deferred_foreign_key_violations(); } res } fn check_deferred_fk_on_commit(conn: &Connection) -> Result<()> { if !conn.foreign_keys_enabled() { return Ok(()); } if conn.get_deferred_foreign_key_violations() > 0 { return Err(LimboError::Constraint( "FOREIGN KEY constraint failed".into(), )); } Ok(()) } pub fn op_goto( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Goto { target_pc }, insn); assert!(target_pc.is_offset()); state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_gosub( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Gosub { target_pc, return_reg, }, insn ); assert!(target_pc.is_offset()); state.registers[*return_reg] = Register::Value(Value::Integer((state.pc + 1) as i64)); state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_return( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Return { return_reg, can_fallthrough, }, insn ); if let Value::Integer(pc) = state.registers[*return_reg].get_value() { let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("Return register is negative: {pc}")); state.pc = pc; } else { if !*can_fallthrough { return Err(LimboError::InternalError( "Return register is not an integer".to_string(), )); } state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_integer( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Integer { value, dest }, insn); state.registers[*dest] = Register::Value(Value::Integer(*value)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_real( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Real { value, dest }, insn); state.registers[*dest] = Register::Value(Value::Float(*value)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_real_affinity( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(RealAffinity { register }, insn); if let Value::Integer(i) = &state.registers[*register].get_value() { state.registers[*register] = Register::Value(Value::Float(*i as f64)); }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_string8( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(String8 { value, dest }, insn); state.registers[*dest] = Register::Value(Value::build_text(value)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_blob( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Blob { value, dest }, insn); state.registers[*dest] = Register::Value(Value::Blob(value.clone())); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_row_data( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(RowData { cursor_id, dest }, insn); let record = { let cursor_ref = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "RowData"); let cursor = cursor_ref.as_btree_mut(); let record_option = return_if_io!(cursor.record()); let record = record_option.ok_or_else(|| { LimboError::InternalError("RowData: cursor has no record".to_string()) })?; record.clone() }; let reg = &mut state.registers[*dest]; *reg = Register::Record(record); state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug, Clone, Copy)] pub enum OpRowIdState { Start, Record { index_cursor_id: usize, table_cursor_id: usize, }, Seek { rowid: i64, table_cursor_id: usize, }, GetRowid, } pub fn op_row_id( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(RowId { cursor_id, dest }, insn); loop { match state.op_row_id_state { OpRowIdState::Start => { if let Some((index_cursor_id, table_cursor_id)) = state.deferred_seeks[*cursor_id].take() { state.op_row_id_state = OpRowIdState::Record { index_cursor_id, table_cursor_id, }; } else { state.op_row_id_state = OpRowIdState::GetRowid; } } OpRowIdState::Record { index_cursor_id, table_cursor_id, } => { let rowid = { let index_cursor = state.get_cursor(index_cursor_id); match index_cursor { Cursor::BTree(index_cursor) => { let record = return_if_io!(index_cursor.record()); let record = record.as_ref().unwrap(); let mut record_cursor_ref = index_cursor.record_cursor_mut(); let record_cursor = record_cursor_ref.deref_mut(); let rowid = record.last_value(record_cursor).unwrap(); match rowid { Ok(ValueRef::Integer(rowid)) => rowid, _ => unreachable!(), } } Cursor::IndexMethod(index_cursor) => { return_if_io!(index_cursor.query_rowid()).unwrap() } _ => panic!("unexpected cursor type"), } }; state.op_row_id_state = OpRowIdState::Seek { rowid, table_cursor_id, } } OpRowIdState::Seek { rowid, table_cursor_id, } => { { let table_cursor = state.get_cursor(table_cursor_id); let table_cursor = table_cursor.as_btree_mut(); return_if_io!( table_cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true }) ); } state.op_row_id_state = OpRowIdState::GetRowid; } OpRowIdState::GetRowid => { let cursors = &mut state.cursors; if let Some(Cursor::BTree(btree_cursor)) = cursors.get_mut(*cursor_id).unwrap() { if let Some(ref rowid) = return_if_io!(btree_cursor.rowid()) { state.registers[*dest] = Register::Value(Value::Integer(*rowid)); } else { state.registers[*dest] = Register::Value(Value::Null); } } else if let Some(Cursor::Virtual(virtual_cursor)) = cursors.get_mut(*cursor_id).unwrap() { let rowid = virtual_cursor.rowid(); if rowid != 0 { state.registers[*dest] = Register::Value(Value::Integer(rowid)); } else { state.registers[*dest] = Register::Value(Value::Null); } } else if let Some(Cursor::MaterializedView(mv_cursor)) = cursors.get_mut(*cursor_id).unwrap() { if let Some(rowid) = return_if_io!(mv_cursor.rowid()) { state.registers[*dest] = Register::Value(Value::Integer(rowid)); } else { state.registers[*dest] = Register::Value(Value::Null); } } else if let Some(Cursor::IndexMethod(cursor)) = cursors.get_mut(*cursor_id).unwrap() { if let Some(rowid) = return_if_io!(cursor.query_rowid()) { state.registers[*dest] = Register::Value(Value::Integer(rowid)); } else { state.registers[*dest] = Register::Value(Value::Null); } } else { return Err(LimboError::InternalError( "RowId: cursor is not a table, virtual, or materialized view cursor" .to_string(), )); } break; } } } state.op_row_id_state = OpRowIdState::Start; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_idx_row_id( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(IdxRowId { cursor_id, dest }, insn); let cursors = &mut state.cursors; let cursor = cursors.get_mut(*cursor_id).unwrap().as_mut().unwrap(); let rowid = match cursor { Cursor::BTree(cursor) => return_if_io!(cursor.rowid()), Cursor::IndexMethod(cursor) => return_if_io!(cursor.query_rowid()), _ => panic!("unexpected cursor type"), }; state.registers[*dest] = match rowid { Some(rowid) => Register::Value(Value::Integer(rowid)), None => Register::Value(Value::Null), }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_seek_rowid( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SeekRowid { cursor_id, src_reg, target_pc, }, insn ); assert!(target_pc.is_offset()); let (pc, did_seek) = { let cursor = get_cursor!(state, *cursor_id); // Handle MaterializedView cursor let (pc, did_seek) = match cursor { Cursor::MaterializedView(mv_cursor) => { let rowid = match state.registers[*src_reg].get_value() { Value::Integer(rowid) => Some(*rowid), Value::Null => None, _ => None, }; match rowid { Some(rowid) => { let seek_result = return_if_io!(mv_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); let pc = if !matches!(seek_result, SeekResult::Found) { target_pc.as_offset_int() } else { state.pc + 1 }; (pc, true) } None => (target_pc.as_offset_int(), false), } } Cursor::BTree(btree_cursor) => { let rowid = match state.registers[*src_reg].get_value() { Value::Integer(rowid) => Some(*rowid), Value::Null => None, // For non-integer values try to apply affinity and convert them to integer. other => { let mut temp_reg = Register::Value(other.clone()); let converted = apply_affinity_char(&mut temp_reg, Affinity::Numeric); if converted { match temp_reg.get_value() { Value::Integer(i) => Some(*i), Value::Float(f) => Some(*f as i64), _ => None, } } else { None } } }; match rowid { Some(rowid) => { let seek_result = return_if_io!(btree_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); let pc = if !matches!(seek_result, SeekResult::Found) { target_pc.as_offset_int() } else { state.pc + 1 }; (pc, true) } None => (target_pc.as_offset_int(), false), } } _ => panic!("SeekRowid on non-btree/materialized-view cursor"), }; (pc, did_seek) }; // Increment btree_seeks metric for SeekRowid operation after cursor is dropped if did_seek { state.metrics.btree_seeks = state.metrics.btree_seeks.saturating_add(1); } state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_deferred_seek( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( DeferredSeek { index_cursor_id, table_cursor_id, }, insn ); state.deferred_seeks[*table_cursor_id] = Some((*index_cursor_id, *table_cursor_id)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Separate enum for seek key to avoid lifetime issues /// with using [SeekKey] - OpSeekState always owns the key, /// unless it's [OpSeekKey::IndexKeyFromRegister] in which case the record /// is owned by the program state's registers and we store the register number. #[derive(Debug)] pub enum OpSeekKey { TableRowId(i64), IndexKeyOwned(ImmutableRecord), IndexKeyFromRegister(usize), } #[derive(Debug)] pub enum OpSeekState { /// Initial state Start, /// Position cursor with seek operation with (rowid, op) search parameters Seek { key: OpSeekKey, op: SeekOp }, /// Advance cursor (with [BTreeCursor::next]/[BTreeCursor::prev] methods) which was /// positioned after [OpSeekState::Seek] state if [BTreeCursor::seek] returned [SeekResult::TryAdvance] Advance { op: SeekOp }, /// Move cursor to the last BTree row if DB knows that comparison result will be fixed (due to type ordering, e.g. NUMBER always <= TEXT) MoveLast, } pub fn op_seek( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { let (cursor_id, is_index, record_source, target_pc) = match insn { Insn::SeekGE { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekLE { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekGT { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekLT { cursor_id, is_index, start_reg, num_regs, target_pc, .. } => ( cursor_id, *is_index, RecordSource::Unpacked { start_reg: *start_reg, num_regs: *num_regs, }, target_pc, ), _ => unreachable!("unexpected Insn {:?}", insn), }; assert!( target_pc.is_offset(), "op_seek: target_pc should be an offset, is: {target_pc:?}" ); let eq_only = match insn { Insn::SeekGE { eq_only, .. } | Insn::SeekLE { eq_only, .. } => *eq_only, _ => false, }; let op = match insn { Insn::SeekGE { eq_only, .. } => SeekOp::GE { eq_only: *eq_only }, Insn::SeekGT { .. } => SeekOp::GT, Insn::SeekLE { eq_only, .. } => SeekOp::LE { eq_only: *eq_only }, Insn::SeekLT { .. } => SeekOp::LT, _ => unreachable!("unexpected Insn {:?}", insn), }; match seek_internal( program, state, pager, mv_store, record_source, *cursor_id, is_index, op, ) { Ok(SeekInternalResult::Found) => { state.pc += 1; Ok(InsnFunctionStepResult::Step) } Ok(SeekInternalResult::NotFound) => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } Ok(SeekInternalResult::IO(io)) => Ok(InsnFunctionStepResult::IO(io)), Err(e) => Err(e), } } #[derive(Debug)] pub enum SeekInternalResult { Found, NotFound, IO(IOCompletions), } #[derive(Clone, Copy)] pub enum RecordSource { Unpacked { start_reg: usize, num_regs: usize }, Packed { record_reg: usize }, } /// Internal function used by many VDBE instructions that need to perform a seek operation. /// /// Explanation for some of the arguments: /// - `record_source`: whether the seek key record is already a record (packed) or it will be constructed from registers (unpacked) /// - `cursor_id`: the cursor id /// - `is_index`: true if the cursor is an index, false if it is a table /// - `op`: the [SeekOp] to perform #[allow(clippy::too_many_arguments)] pub fn seek_internal( program: &Program, state: &mut ProgramState, pager: &Arc, mv_store: Option<&Arc>, record_source: RecordSource, cursor_id: usize, is_index: bool, op: SeekOp, ) -> Result { /// wrapper so we can use the ? operator and handle errors correctly in this outer function fn inner( program: &Program, state: &mut ProgramState, pager: &Arc, mv_store: Option<&Arc>, record_source: RecordSource, cursor_id: usize, is_index: bool, op: SeekOp, ) -> Result { loop { match &state.seek_state { OpSeekState::Start => { if is_index { // FIXME: text-to-numeric conversion should also happen here when applicable (when index column is numeric) // See below for the table-btree implementation of this match record_source { RecordSource::Unpacked { start_reg, num_regs, } => { let record_from_regs = make_record(&state.registers, &start_reg, &num_regs); state.seek_state = OpSeekState::Seek { key: OpSeekKey::IndexKeyOwned(record_from_regs), op, }; } RecordSource::Packed { record_reg } => { state.seek_state = OpSeekState::Seek { key: OpSeekKey::IndexKeyFromRegister(record_reg), op, }; } }; continue; } let RecordSource::Unpacked { start_reg, num_regs, } = record_source else { unreachable!("op_seek: record_source should be Unpacked for table-btree"); }; assert_eq!(num_regs, 1, "op_seek: num_regs should be 1 for table-btree"); let original_value = state.registers[start_reg].get_value().clone(); let mut temp_value = original_value.clone(); let conversion_successful = if matches!(temp_value, Value::Text(_)) { let mut temp_reg = Register::Value(temp_value); let converted = apply_numeric_affinity(&mut temp_reg, false); temp_value = temp_reg.get_value().clone(); converted } else { true // Non-text values don't need conversion }; let int_key = extract_int_value(&temp_value); let lost_precision = !conversion_successful || !matches!(temp_value, Value::Integer(_)); let actual_op = if lost_precision { match &temp_value { Value::Float(f) => { let int_key_as_float = int_key as f64; let c = if int_key_as_float > *f { 1 } else if int_key_as_float < *f { -1 } else { 0 }; match c.cmp(&0) { std::cmp::Ordering::Less => match op { SeekOp::LT => SeekOp::LE { eq_only: false }, // (x < 5.1) -> (x <= 5) SeekOp::GE { .. } => SeekOp::GT, // (x >= 5.1) -> (x > 5) other => other, }, std::cmp::Ordering::Greater => match op { SeekOp::GT => SeekOp::GE { eq_only: false }, // (x > 4.9) -> (x >= 5) SeekOp::LE { .. } => SeekOp::LT, // (x <= 4.9) -> (x < 5) other => other, }, std::cmp::Ordering::Equal => op, } } Value::Text(_) | Value::Blob(_) => { match op { SeekOp::GT | SeekOp::GE { .. } => { // No integers are > or >= non-numeric text return Ok(SeekInternalResult::NotFound); } SeekOp::LT | SeekOp::LE { .. } => { // All integers are < or <= non-numeric text // Move to last position and then use the normal seek logic state.seek_state = OpSeekState::MoveLast; continue; } } } _ => op, } } else { op }; let rowid = if matches!(original_value, Value::Null) { match actual_op { SeekOp::GE { .. } | SeekOp::GT => { return Ok(SeekInternalResult::NotFound); } SeekOp::LE { .. } | SeekOp::LT => { // No integers are < NULL return Ok(SeekInternalResult::NotFound); } } } else { int_key }; state.seek_state = OpSeekState::Seek { key: OpSeekKey::TableRowId(rowid), op: actual_op, }; continue; } OpSeekState::Seek { key, op } => { let seek_result = { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); let seek_key = match key { OpSeekKey::TableRowId(rowid) => SeekKey::TableRowId(*rowid), OpSeekKey::IndexKeyOwned(record) => SeekKey::IndexKey(record), OpSeekKey::IndexKeyFromRegister(record_reg) => match &state.registers[*record_reg] { Register::Record(ref record) => SeekKey::IndexKey(record), _ => unreachable!("op_seek: record_reg should be a Record register when OpSeekKey::IndexKeyFromRegister is used"), } }; match cursor.seek(seek_key, *op)? { IOResult::Done(seek_result) => seek_result, IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } }; // Increment btree_seeks metric after seek operation and cursor is dropped state.metrics.btree_seeks = state.metrics.btree_seeks.saturating_add(1); let found = match seek_result { SeekResult::Found => true, SeekResult::NotFound => false, SeekResult::TryAdvance => { state.seek_state = OpSeekState::Advance { op: *op }; continue; } }; return Ok(if found { SeekInternalResult::Found } else { SeekInternalResult::NotFound }); } OpSeekState::Advance { op } => { let found = { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); // Seek operation has anchor number which equals to the closed boundary of the range // (e.g. for >= x - anchor is x, for > x - anchor is x + 1) // // Before Advance state, cursor was positioned to the leaf page which should hold the anchor. // Sometimes this leaf page can have no matching rows, and in this case // we need to move cursor in the direction of Seek to find record which matches the seek filter // // Consider following scenario: Seek [> 666] // interior page dividers: I1: [ .. 667 .. ] // / \ // leaf pages: P1[661,665] P2[anything here is GT 666] // After the initial Seek, cursor will be positioned after the end of leaf page P1 [661, 665] // because this is potential position for insertion of value 666. // But as P1 has no row matching Seek criteria - we need to move it to the right // (and as we at the page boundary, we will move cursor to the next neighbor leaf, which guaranteed to have // row keys greater than divider, which is greater or equal than anchor) // this same logic applies for indexes, but the next/prev record is expected to be found in the parent page's // divider cell. turso_assert!( !cursor.get_skip_advance(), "skip_advance should not be true in the middle of a seek operation" ); let result = match op { // deliberately call get_next_record() instead of next() to avoid skip_advance triggering unwantedly SeekOp::GT | SeekOp::GE { .. } => cursor.next()?, SeekOp::LT | SeekOp::LE { .. } => cursor.prev()?, }; match result { IOResult::Done(found) => { cursor.set_has_record(found); cursor.invalidate_record(); found } IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } }; return Ok(if found { SeekInternalResult::Found } else { SeekInternalResult::NotFound }); } OpSeekState::MoveLast => { let cursor = state.get_cursor(cursor_id); let cursor = cursor.as_btree_mut(); match cursor.last()? { IOResult::Done(()) => {} IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } // the MoveLast variant is only used for SeekOp::LT and SeekOp::LE when the seek condition is always true, // so we have always found what we were looking for. return Ok(SeekInternalResult::Found); } } } } let result = inner( program, state, pager, mv_store, record_source, cursor_id, is_index, op, ); if !matches!(result, Ok(SeekInternalResult::IO(..))) { state.seek_state = OpSeekState::Start; } result } /// Returns the tie-breaker ordering for SQLite index comparison opcodes. /// /// When comparing index keys that omit the PRIMARY KEY/ROWID, SQLite uses a /// tie-breaker value (`default_rc` in the C code) to determine the result when /// the non-primary-key portions of the keys are equal. /// /// This function extracts the appropriate tie-breaker based on the comparison opcode: /// /// ## Tie-breaker Logic /// /// - **`IdxLE` and `IdxGT`**: Return `Ordering::Less` (equivalent to `default_rc = -1`) /// - When keys are equal, these operations should favor the "less than" result /// - `IdxLE`: "less than or equal" - equality should be treated as "less" /// - `IdxGT`: "greater than" - equality should be treated as "less" (so condition fails) /// /// - **`IdxGE` and `IdxLT`**: Return `Ordering::Equal` (equivalent to `default_rc = 0`) /// - When keys are equal, these operations should treat it as true equality /// - `IdxGE`: "greater than or equal" - equality should be treated as "equal" /// - `IdxLT`: "less than" - equality should be treated as "equal" (so condition fails) /// /// ## SQLite Implementation Details /// /// In SQLite's C implementation, this corresponds to: /// ```c /// if( pOp->opcodeopcode==OP_IdxLE || pOp->opcode==OP_IdxGT ); /// r.default_rc = -1; // Ordering::Less /// }else{ /// assert( pOp->opcode==OP_IdxGE || pOp->opcode==OP_IdxLT ); /// r.default_rc = 0; // Ordering::Equal /// } /// ``` #[inline(always)] fn get_tie_breaker_from_idx_comp_op(insn: &Insn) -> std::cmp::Ordering { match insn { Insn::IdxLE { .. } | Insn::IdxGT { .. } => std::cmp::Ordering::Less, Insn::IdxGE { .. } | Insn::IdxLT { .. } => std::cmp::Ordering::Equal, _ => panic!("Invalid instruction for index comparison"), } } #[allow(clippy::let_and_return)] pub fn op_idx_ge( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxGE { cursor_id, start_reg, num_regs, target_pc, }, insn ); assert!(target_pc.is_offset()); let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { // Create the comparison record from registers let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic( &idx_record, // The serialized record from the index &values, // The record built from registers cursor.get_index_info(), // Sort order flags 0, tie_breaker, )?; if ord.is_ge() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_seek_end( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(SeekEnd { cursor_id }, *insn); { let cursor = state.get_cursor(cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.seek_end()); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_le( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxLE { cursor_id, start_reg, num_regs, target_pc, }, insn ); assert!(target_pc.is_offset()); let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic( &idx_record, &values, cursor.get_index_info(), 0, tie_breaker, )?; if ord.is_le() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_gt( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxGT { cursor_id, start_reg, num_regs, target_pc, }, insn ); assert!(target_pc.is_offset()); let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic( &idx_record, &values, cursor.get_index_info(), 0, tie_breaker, )?; if ord.is_gt() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_lt( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxLT { cursor_id, start_reg, num_regs, target_pc, }, insn ); assert!(target_pc.is_offset()); let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic( &idx_record, &values, cursor.get_index_info(), 0, tie_breaker, )?; if ord.is_lt() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_decr_jump_zero( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(DecrJumpZero { reg, target_pc }, insn); assert!(target_pc.is_offset()); match state.registers[*reg].get_value() { Value::Integer(n) => { let n = n - 1; state.registers[*reg] = Register::Value(Value::Integer(n)); if n == 0 { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } } _ => unreachable!("DecrJumpZero on non-integer register"), } Ok(InsnFunctionStepResult::Step) } fn apply_kbn_step(acc: &mut Value, r: f64, state: &mut SumAggState) { let s = acc.as_float(); let t = s + r; let correction = if s.abs() > r.abs() { (s - t) + r } else { (r - t) + s }; state.r_err += correction; *acc = Value::Float(t); } // Add a (possibly large) integer to the running sum. fn apply_kbn_step_int(acc: &mut Value, i: i64, state: &mut SumAggState) { const THRESHOLD: i64 = 4503599627370496; // 2^52 if i <= -THRESHOLD || i >= THRESHOLD { let i_sm = i % 16384; let i_big = i - i_sm; apply_kbn_step(acc, i_big as f64, state); apply_kbn_step(acc, i_sm as f64, state); } else { apply_kbn_step(acc, i as f64, state); } } pub fn op_agg_step( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( AggStep { acc_reg, col, delimiter, func, }, insn ); if let Register::Value(Value::Null) = state.registers[*acc_reg] { state.registers[*acc_reg] = match func { AggFunc::Avg => { Register::Aggregate(AggContext::Avg(Value::Float(0.0), Value::Integer(0))) } AggFunc::Sum => { Register::Aggregate(AggContext::Sum(Value::Null, SumAggState::default())) } AggFunc::Total => { // The result of total() is always a floating point value. // No overflow error is ever raised if any prior input was a floating point value. // Total() never throws an integer overflow. Register::Aggregate(AggContext::Sum(Value::Float(0.0), SumAggState::default())) } AggFunc::Count | AggFunc::Count0 => { Register::Aggregate(AggContext::Count(Value::Integer(0))) } AggFunc::Max => Register::Aggregate(AggContext::Max(None)), AggFunc::Min => Register::Aggregate(AggContext::Min(None)), AggFunc::GroupConcat | AggFunc::StringAgg => { Register::Aggregate(AggContext::GroupConcat(Value::build_text(""))) } #[cfg(feature = "json")] AggFunc::JsonGroupArray | AggFunc::JsonbGroupArray => { Register::Aggregate(AggContext::GroupConcat(Value::Blob(vec![]))) } #[cfg(feature = "json")] AggFunc::JsonGroupObject | AggFunc::JsonbGroupObject => { Register::Aggregate(AggContext::GroupConcat(Value::Blob(vec![]))) } AggFunc::External(func) => match func.as_ref() { ExtFunc::Aggregate { init, step, finalize, argc, } => Register::Aggregate(AggContext::External(ExternalAggState { state: unsafe { (init)() }, argc: *argc, step_fn: *step, finalize_fn: *finalize, })), _ => unreachable!("scalar function called in aggregate context"), }, }; } match func { AggFunc::Avg => { let col = state.registers[*col].clone(); // > The avg() function returns the average value of all non-NULL X within a group // https://sqlite.org/lang_aggfunc.html#avg if !col.is_null() { let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { panic!( "Unexpected value {:?} in AggStep at register {}", state.registers[*acc_reg], *acc_reg ); }; let AggContext::Avg(acc, count) = agg.borrow_mut() else { unreachable!(); }; *acc = acc.exec_add(col.get_value()); *count += 1; } } AggFunc::Sum | AggFunc::Total => { let col = state.registers[*col].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { panic!( "Unexpected value {:?} at register {:?} in AggStep", state.registers[*acc_reg], *acc_reg ); }; let AggContext::Sum(acc, sum_state) = agg.borrow_mut() else { unreachable!(); }; match col { Register::Value(value) => { match value { Value::Null => { // Ignore NULLs } Value::Integer(i) => match acc { Value::Null => { *acc = Value::Integer(i); } Value::Integer(acc_i) => { match acc_i.checked_add(i) { Some(sum) => *acc = Value::Integer(sum), None => { if matches!(func, AggFunc::Total) { // Total() never throw an integer overflow -> switch to float with KBN summation let acc_f = *acc_i as f64; *acc = Value::Float(acc_f); sum_state.approx = true; sum_state.ovrfl = true; apply_kbn_step_int(acc, i, sum_state); } else { return Err(LimboError::IntegerOverflow); } } } } Value::Float(_) => { apply_kbn_step_int(acc, i, sum_state); } _ => unreachable!(), }, Value::Float(f) => match acc { Value::Null => { *acc = Value::Float(f); sum_state.approx = true; } Value::Integer(i) => { let i_f = *i as f64; *acc = Value::Float(i_f); sum_state.approx = true; apply_kbn_step(acc, f, sum_state); } Value::Float(_) => { sum_state.approx = true; apply_kbn_step(acc, f, sum_state); } _ => unreachable!(), }, Value::Text(t) => { let s = t.as_str(); let (_, parsed_number) = try_for_float(s); handle_text_sum(acc, sum_state, parsed_number); } Value::Blob(b) => { if let Ok(s) = std::str::from_utf8(&b) { let (_, parsed_number) = try_for_float(s); handle_text_sum(acc, sum_state, parsed_number); } else { handle_text_sum(acc, sum_state, ParsedNumber::None); } } } } _ => unreachable!(), } } AggFunc::Count | AggFunc::Count0 => { let skip = (matches!(func, AggFunc::Count) && matches!(state.registers[*col].get_value(), Value::Null)); if matches!(&state.registers[*acc_reg], Register::Value(Value::Null)) { state.registers[*acc_reg] = Register::Aggregate(AggContext::Count(Value::Integer(0))); } let Register::Aggregate(agg) = &mut state.registers[*acc_reg] else { panic!( "Unexpected value {:?} in AggStep at register {}", state.registers[*acc_reg], *acc_reg ); }; let AggContext::Count(count) = agg else { unreachable!(); }; if !skip { *count += 1; }; } AggFunc::Max => { let col = state.registers[*col].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { panic!( "Unexpected value {:?} in AggStep at register {}", state.registers[*acc_reg], *acc_reg ); }; let AggContext::Max(acc) = agg.borrow_mut() else { unreachable!(); }; let new_value = col.get_value(); if *new_value != Value::Null && acc.as_ref().is_none_or(|acc| { use std::cmp::Ordering; compare_with_collation(new_value, acc, state.current_collation) == Ordering::Greater }) { *acc = Some(new_value.clone()); } } AggFunc::Min => { let col = state.registers[*col].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { panic!( "Unexpected value {:?} in AggStep", state.registers[*acc_reg] ); }; let AggContext::Min(acc) = agg.borrow_mut() else { unreachable!(); }; let new_value = col.get_value(); if *new_value != Value::Null && acc.as_ref().is_none_or(|acc| { use std::cmp::Ordering; compare_with_collation(new_value, acc, state.current_collation) == Ordering::Less }) { *acc = Some(new_value.clone()); } } AggFunc::GroupConcat | AggFunc::StringAgg => { let col = state.registers[*col].get_value().clone(); let delimiter = state.registers[*delimiter].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { unreachable!(); }; let AggContext::GroupConcat(acc) = agg.borrow_mut() else { unreachable!(); }; if acc.to_string().is_empty() { *acc = col; } else { match col { Value::Null => {} _ => { match delimiter { Register::Value(value) => { *acc += value; } _ => unreachable!(), } *acc += col; } } } } #[cfg(feature = "json")] AggFunc::JsonGroupObject | AggFunc::JsonbGroupObject => { let key = state.registers[*col].clone(); let value = state.registers[*delimiter].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { unreachable!(); }; let AggContext::GroupConcat(acc) = agg.borrow_mut() else { unreachable!(); }; let mut key_vec = convert_dbtype_to_raw_jsonb(key.get_value())?; let mut val_vec = convert_dbtype_to_raw_jsonb(value.get_value())?; match acc { Value::Blob(vec) => { if vec.is_empty() { // bits for obj header vec.push(12); vec.append(&mut key_vec); vec.append(&mut val_vec); } else { vec.append(&mut key_vec); vec.append(&mut val_vec); } } _ => unreachable!(), }; } #[cfg(feature = "json")] AggFunc::JsonGroupArray | AggFunc::JsonbGroupArray => { let col = state.registers[*col].clone(); let Register::Aggregate(agg) = state.registers[*acc_reg].borrow_mut() else { unreachable!(); }; let AggContext::GroupConcat(acc) = agg.borrow_mut() else { unreachable!(); }; let mut data = convert_dbtype_to_raw_jsonb(col.get_value())?; match acc { Value::Blob(vec) => { if vec.is_empty() { vec.push(11); vec.append(&mut data) } else { vec.append(&mut data); } } _ => unreachable!(), }; } AggFunc::External(_) => { let (step_fn, state_ptr, argc) = { let Register::Aggregate(agg) = &state.registers[*acc_reg] else { unreachable!(); }; let AggContext::External(agg_state) = agg else { unreachable!(); }; (agg_state.step_fn, agg_state.state, agg_state.argc) }; if argc == 0 { unsafe { step_fn(state_ptr, 0, std::ptr::null()) }; } else { let register_slice = &state.registers[*col..*col + argc]; let mut ext_values: Vec = Vec::with_capacity(argc); for ov in register_slice.iter() { ext_values.push(ov.get_value().to_ffi()); } let argv_ptr = ext_values.as_ptr(); unsafe { step_fn(state_ptr, argc as i32, argv_ptr) }; for ext_value in ext_values { unsafe { ext_value.__free_internal_type() }; } } } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_agg_final( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { let (acc_reg, dest_reg, func) = match insn { Insn::AggFinal { register, func } => (*register, *register, func), Insn::AggValue { acc_reg, dest_reg, func, } => (*acc_reg, *dest_reg, func), _ => unreachable!("unexpected Insn {:?}", insn), }; match &state.registers[acc_reg] { Register::Aggregate(agg) => match func { AggFunc::Avg => { let AggContext::Avg(acc, count) = agg else { unreachable!(); }; let acc = if count.as_int() == Some(0) { Value::Null } else { acc.clone() / count.clone() }; state.registers[dest_reg] = Register::Value(acc); } AggFunc::Sum => { let AggContext::Sum(acc, sum_state) = agg else { unreachable!(); }; let value = match acc { Value::Null => match sum_state.approx { true => Value::Float(0.0), false => Value::Null, }, Value::Integer(i) if !sum_state.approx && !sum_state.ovrfl => { Value::Integer(*i) } _ => Value::Float(acc.as_float() + sum_state.r_err), }; state.registers[dest_reg] = Register::Value(value); } AggFunc::Total => { let AggContext::Sum(acc, _) = agg else { unreachable!(); }; let value = match acc { Value::Null => Value::Float(0.0), Value::Integer(i) => Value::Float(*i as f64), Value::Float(f) => Value::Float(*f), _ => unreachable!(), }; state.registers[dest_reg] = Register::Value(value); } AggFunc::Count | AggFunc::Count0 => { let AggContext::Count(count) = agg else { unreachable!(); }; state.registers[dest_reg] = Register::Value(count.clone()); } AggFunc::Max => { let AggContext::Max(acc) = agg else { unreachable!(); }; match acc { Some(value) => state.registers[dest_reg] = Register::Value(value.clone()), None => state.registers[dest_reg] = Register::Value(Value::Null), } } AggFunc::Min => { let AggContext::Min(acc) = agg else { unreachable!(); }; match acc { Some(value) => state.registers[dest_reg] = Register::Value(value.clone()), None => state.registers[dest_reg] = Register::Value(Value::Null), } } AggFunc::GroupConcat | AggFunc::StringAgg => { let AggContext::GroupConcat(acc) = agg else { unreachable!(); }; state.registers[dest_reg] = Register::Value(acc.clone()); } #[cfg(feature = "json")] AggFunc::JsonGroupObject => { let AggContext::GroupConcat(acc) = agg else { unreachable!(); }; let data = acc.to_blob().expect("Should be blob"); state.registers[dest_reg] = Register::Value(json_from_raw_bytes_agg(data, false)?); } #[cfg(feature = "json")] AggFunc::JsonbGroupObject => { let AggContext::GroupConcat(acc) = agg else { unreachable!(); }; let data = acc.to_blob().expect("Should be blob"); state.registers[dest_reg] = Register::Value(json_from_raw_bytes_agg(data, true)?); } #[cfg(feature = "json")] AggFunc::JsonGroupArray => { let AggContext::GroupConcat(acc) = agg else { unreachable!(); }; let data = acc.to_blob().expect("Should be blob"); state.registers[dest_reg] = Register::Value(json_from_raw_bytes_agg(data, false)?); } #[cfg(feature = "json")] AggFunc::JsonbGroupArray => { let AggContext::GroupConcat(acc) = agg else { unreachable!(); }; let data = acc.to_blob().expect("Should be blob"); state.registers[dest_reg] = Register::Value(json_from_raw_bytes_agg(data, true)?); } AggFunc::External(_) => { let AggContext::External(agg_state) = agg else { unreachable!(); }; let value = agg.compute_external()?; state.registers[dest_reg] = Register::Value(value) } }, Register::Value(Value::Null) => { // when the set is empty match func { AggFunc::Total => { state.registers[dest_reg] = Register::Value(Value::Float(0.0)); } AggFunc::Count | AggFunc::Count0 => { state.registers[dest_reg] = Register::Value(Value::Integer(0)); } _ => {} } } other => { panic!("Unexpected value {other:?} in AggFinal"); } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_open( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterOpen { cursor_id, columns: _, order, collations, }, insn ); // be careful here - we must not use any async operations after pager.with_header because this op-code has no proper state-machine let page_size = match pager.with_header(|header| header.page_size) { Ok(IOResult::Done(page_size)) => page_size, Err(_) => PageSize::default(), Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; let page_size = page_size.get() as usize; let cache_size = program.connection.get_cache_size(); // Set the buffer size threshold to be roughly the same as the limit configured for the page-cache. let max_buffer_size_bytes = if cache_size < 0 { (cache_size.abs() * 1024) as usize } else { (cache_size as usize) * page_size }; let cursor = Sorter::new( order, collations .iter() .map(|collation| collation.unwrap_or_default()) .collect(), max_buffer_size_bytes, page_size, pager.io.clone(), ); let cursors = &mut state.cursors; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_sorter(cursor)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_data( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterData { cursor_id, dest_reg, pseudo_cursor, }, insn ); let record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); cursor.record().cloned() }; let record = match record { Some(record) => record, None => { state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; state.registers[*dest_reg] = Register::Record(record.clone()); { let pseudo_cursor = state.get_cursor(*pseudo_cursor); pseudo_cursor.as_pseudo_mut().insert(record); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_insert( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterInsert { cursor_id, record_reg, }, insn ); { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_sorter_mut(); let record = match &state.registers[*record_reg] { Register::Record(record) => record, _ => unreachable!("SorterInsert on non-record register"), }; return_if_io!(cursor.insert(record)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_sort( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterSort { cursor_id, pc_if_empty, }, insn ); let (is_empty, did_sort) = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); let is_empty = cursor.is_empty(); if !is_empty { return_if_io!(cursor.sort()); } (is_empty, !is_empty) }; // Increment metrics for sort operation after cursor is dropped if did_sort { state.metrics.sort_operations = state.metrics.sort_operations.saturating_add(1); } if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_next( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterNext { cursor_id, pc_if_next, }, insn ); assert!(pc_if_next.is_offset()); let has_more = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); return_if_io!(cursor.next()); cursor.has_more() }; if has_more { state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_compare( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SorterCompare { cursor_id, sorted_record_reg, num_regs, pc_when_nonequal, }, insn ); let previous_sorter_values = { let Register::Record(record) = &state.registers[*sorted_record_reg] else { return Err(LimboError::InternalError( "Sorted record must be a record".to_string(), )); }; &record.get_values()[..*num_regs] }; // Inlined `state.get_cursor` to prevent borrowing conflit with `state.registers` let cursor = state .cursors .get_mut(*cursor_id) .unwrap_or_else(|| panic!("cursor id {cursor_id} out of bounds")) .as_mut() .unwrap_or_else(|| panic!("cursor id {cursor_id} is None")); let cursor = cursor.as_sorter_mut(); let Some(current_sorter_record) = cursor.record() else { return Err(LimboError::InternalError( "Sorter must have a record".to_string(), )); }; let current_sorter_values = ¤t_sorter_record.get_values()[..*num_regs]; // If the current sorter record has a NULL in any of the significant fields, the comparison is not equal. let is_equal = current_sorter_values .iter() .all(|v| !matches!(v, ValueRef::Null)) && compare_immutable( previous_sorter_values, current_sorter_values, &cursor.index_key_info, ) .is_eq(); if is_equal { state.pc += 1; } else { state.pc = pc_when_nonequal.as_offset_int(); } Ok(InsnFunctionStepResult::Step) } pub fn op_function( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Function { constant_mask, func, start_reg, dest, }, insn ); let arg_count = func.arg_count; match &func.func { #[cfg(feature = "json")] crate::function::Func::Json(json_func) => match json_func { JsonFunc::Json => { let json_value = &state.registers[*start_reg]; let json_str = get_json(json_value.get_value(), None); match json_str { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::Jsonb => { let json_value = &state.registers[*start_reg]; let json_blob = jsonb(json_value.get_value(), &state.json_cache); match json_blob { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonArray | JsonFunc::JsonObject | JsonFunc::JsonbArray | JsonFunc::JsonbObject => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let json_func = match json_func { JsonFunc::JsonArray => json_array, JsonFunc::JsonObject => json_object, JsonFunc::JsonbArray => jsonb_array, JsonFunc::JsonbObject => jsonb_object, _ => unreachable!(), }; let json_result = json_func(reg_values); match json_result { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonExtract => { let result = match arg_count { 0 => Ok(Value::Null), _ => { let val = &state.registers[*start_reg]; let reg_values = &state.registers[*start_reg + 1..*start_reg + arg_count]; json_extract(val.get_value(), reg_values, &state.json_cache) } }; match result { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonbExtract => { let result = match arg_count { 0 => Ok(Value::Null), _ => { let val = &state.registers[*start_reg]; let reg_values = &state.registers[*start_reg + 1..*start_reg + arg_count]; jsonb_extract(val.get_value(), reg_values, &state.json_cache) } }; match result { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { assert_eq!(arg_count, 2); let json = &state.registers[*start_reg]; let path = &state.registers[*start_reg + 1]; let json_func = match json_func { JsonFunc::JsonArrowExtract => json_arrow_extract, JsonFunc::JsonArrowShiftExtract => json_arrow_shift_extract, _ => unreachable!(), }; let json_str = json_func(json.get_value(), path.get_value(), &state.json_cache); match json_str { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonArrayLength | JsonFunc::JsonType => { let json_value = &state.registers[*start_reg]; let path_value = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; let func_result = match json_func { JsonFunc::JsonArrayLength => json_array_length( json_value.get_value(), path_value.map(|x| x.get_value()), &state.json_cache, ), JsonFunc::JsonType => { json_type(json_value.get_value(), path_value.map(|x| x.get_value())) } _ => unreachable!(), }; match func_result { Ok(result) => state.registers[*dest] = Register::Value(result), Err(e) => return Err(e), } } JsonFunc::JsonErrorPosition => { let json_value = &state.registers[*start_reg]; match json_error_position(json_value.get_value()) { Ok(pos) => state.registers[*dest] = Register::Value(pos), Err(e) => return Err(e), } } JsonFunc::JsonValid => { let json_value = &state.registers[*start_reg]; state.registers[*dest] = Register::Value(is_json_valid(json_value.get_value())); } JsonFunc::JsonPatch => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let target = &state.registers[*start_reg]; let patch = &state.registers[*start_reg + 1]; state.registers[*dest] = Register::Value(json_patch( target.get_value(), patch.get_value(), &state.json_cache, )?); } JsonFunc::JsonbPatch => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let target = &state.registers[*start_reg]; let patch = &state.registers[*start_reg + 1]; state.registers[*dest] = Register::Value(jsonb_patch( target.get_value(), patch.get_value(), &state.json_cache, )?); } JsonFunc::JsonRemove => { if let Ok(json) = json_remove( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonbRemove => { if let Ok(json) = jsonb_remove( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonReplace => { if let Ok(json) = json_replace( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonbReplace => { if let Ok(json) = jsonb_replace( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonInsert => { if let Ok(json) = json_insert( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonbInsert => { if let Ok(json) = jsonb_insert( &state.registers[*start_reg..*start_reg + arg_count], &state.json_cache, ) { state.registers[*dest] = Register::Value(json); } else { state.registers[*dest] = Register::Value(Value::Null); } } JsonFunc::JsonPretty => { let json_value = &state.registers[*start_reg]; let indent = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; // Blob should be converted to Ascii in a lossy way // However, Rust strings uses utf-8 // so the behavior at the moment is slightly different // To the way blobs are parsed here in SQLite. let indent = match indent { Some(value) => match value.get_value() { Value::Text(text) => text.as_str(), Value::Integer(val) => &val.to_string(), Value::Float(val) => &val.to_string(), Value::Blob(val) => &String::from_utf8_lossy(val), _ => " ", }, // If the second argument is omitted or is NULL, then indentation is four spaces per level None => " ", }; let json_str = get_json(json_value.get_value(), Some(indent))?; state.registers[*dest] = Register::Value(json_str); } JsonFunc::JsonSet => { if arg_count % 2 == 0 { bail_constraint_error!("json_set() needs an odd number of arguments") } let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let json_result = json_set(reg_values, &state.json_cache); match json_result { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonbSet => { if arg_count % 2 == 0 { bail_constraint_error!("json_set() needs an odd number of arguments") } let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let json_result = jsonb_set(reg_values, &state.json_cache); match json_result { Ok(json) => state.registers[*dest] = Register::Value(json), Err(e) => return Err(e), } } JsonFunc::JsonQuote => { let json_value = &state.registers[*start_reg]; match json_quote(json_value.get_value()) { Ok(result) => state.registers[*dest] = Register::Value(result), Err(e) => return Err(e), } } }, crate::function::Func::Scalar(scalar_func) => match scalar_func { ScalarFunc::Cast => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let reg_value_argument = state.registers[*start_reg].clone(); let Value::Text(reg_value_type) = state.registers[*start_reg + 1].get_value().clone() else { unreachable!("Cast with non-text type"); }; let result = reg_value_argument .get_value() .exec_cast(reg_value_type.as_str()); state.registers[*dest] = Register::Value(result); } ScalarFunc::Changes => { let res = &program.connection.last_change; let changes = res.load(Ordering::SeqCst); state.registers[*dest] = Register::Value(Value::Integer(changes)); } ScalarFunc::Char => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest] = Register::Value(exec_char(reg_values)); } ScalarFunc::Coalesce => {} ScalarFunc::Concat => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let result = exec_concat_strings(reg_values); state.registers[*dest] = Register::Value(result); } ScalarFunc::ConcatWs => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let result = exec_concat_ws(reg_values); state.registers[*dest] = Register::Value(result); } ScalarFunc::Glob => { let pattern = &state.registers[*start_reg]; let text = &state.registers[*start_reg + 1]; let result = match (pattern.get_value(), text.get_value()) { (Value::Null, _) | (_, Value::Null) => Value::Null, (Value::Text(pattern), Value::Text(text)) => { let cache = if *constant_mask > 0 { Some(&mut state.regex_cache.glob) } else { None }; Value::Integer(exec_glob(cache, pattern.as_str(), text.as_str()) as i64) } // Convert any other value types to text for GLOB comparison (pattern_val, text_val) => { let pattern_str = pattern_val.to_string(); let text_str = text_val.to_string(); let cache = if *constant_mask > 0 { Some(&mut state.regex_cache.glob) } else { None }; Value::Integer(exec_glob(cache, &pattern_str, &text_str) as i64) } }; state.registers[*dest] = Register::Value(result); } ScalarFunc::IfNull => {} ScalarFunc::Iif => {} ScalarFunc::Instr => { let reg_value = &state.registers[*start_reg]; let pattern_value = &state.registers[*start_reg + 1]; let result = reg_value.get_value().exec_instr(pattern_value.get_value()); state.registers[*dest] = Register::Value(result); } ScalarFunc::LastInsertRowid => { state.registers[*dest] = Register::Value(Value::Integer(program.connection.last_insert_rowid())); } ScalarFunc::Like => { let pattern = &state.registers[*start_reg]; let match_expression = &state.registers[*start_reg + 1]; let pattern = match pattern.get_value() { Value::Text(_) => pattern.get_value(), _ => &pattern.get_value().exec_cast("TEXT"), }; let match_expression = match match_expression.get_value() { Value::Text(_) => match_expression.get_value(), _ => &match_expression.get_value().exec_cast("TEXT"), }; let result = match (pattern, match_expression) { (Value::Text(pattern), Value::Text(match_expression)) if arg_count == 3 => { let escape = construct_like_escape_arg(state.registers[*start_reg + 2].get_value())?; Value::Integer(exec_like_with_escape( pattern.as_str(), match_expression.as_str(), escape, ) as i64) } (Value::Text(pattern), Value::Text(match_expression)) => { let cache = if *constant_mask > 0 { Some(&mut state.regex_cache.like) } else { None }; Value::Integer(Value::exec_like( cache, pattern.as_str(), match_expression.as_str(), ) as i64) } (Value::Null, _) | (_, Value::Null) => Value::Null, _ => { unreachable!("Like failed"); } }; state.registers[*dest] = Register::Value(result); } ScalarFunc::Abs | ScalarFunc::Lower | ScalarFunc::Upper | ScalarFunc::Length | ScalarFunc::OctetLength | ScalarFunc::Typeof | ScalarFunc::Unicode | ScalarFunc::Quote | ScalarFunc::RandomBlob | ScalarFunc::Sign | ScalarFunc::Soundex | ScalarFunc::ZeroBlob => { let reg_value = state.registers[*start_reg].borrow_mut().get_value(); let result = match scalar_func { ScalarFunc::Sign => reg_value.exec_sign(), ScalarFunc::Abs => Some(reg_value.exec_abs()?), ScalarFunc::Lower => reg_value.exec_lower(), ScalarFunc::Upper => reg_value.exec_upper(), ScalarFunc::Length => Some(reg_value.exec_length()), ScalarFunc::OctetLength => Some(reg_value.exec_octet_length()), ScalarFunc::Typeof => Some(reg_value.exec_typeof()), ScalarFunc::Unicode => Some(reg_value.exec_unicode()), ScalarFunc::Quote => Some(reg_value.exec_quote()), ScalarFunc::RandomBlob => { Some(reg_value.exec_randomblob(|dest| pager.io.fill_bytes(dest))) } ScalarFunc::ZeroBlob => Some(reg_value.exec_zeroblob()), ScalarFunc::Soundex => Some(reg_value.exec_soundex()), _ => unreachable!(), }; state.registers[*dest] = Register::Value(result.unwrap_or(Value::Null)); } ScalarFunc::Hex => { let reg_value = state.registers[*start_reg].borrow_mut(); let result = reg_value.get_value().exec_hex(); state.registers[*dest] = Register::Value(result); } ScalarFunc::Unhex => { let reg_value = &state.registers[*start_reg]; let ignored_chars = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_unhex(ignored_chars.map(|x| x.get_value())); state.registers[*dest] = Register::Value(result); } ScalarFunc::Random => { state.registers[*dest] = Register::Value(Value::exec_random(|| pager.io.generate_random_number())); } ScalarFunc::Trim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_trim(pattern_value.map(|x| x.get_value())); state.registers[*dest] = Register::Value(result); } ScalarFunc::LTrim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_ltrim(pattern_value.map(|x| x.get_value())); state.registers[*dest] = Register::Value(result); } ScalarFunc::RTrim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_rtrim(pattern_value.map(|x| x.get_value())); state.registers[*dest] = Register::Value(result); } ScalarFunc::Round => { let reg_value = &state.registers[*start_reg]; assert!(arg_count == 1 || arg_count == 2); let precision_value = if arg_count > 1 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_round(precision_value.map(|x| x.get_value())); state.registers[*dest] = Register::Value(result); } ScalarFunc::Min => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest] = Register::Value(Value::exec_min(reg_values.iter().map(|v| v.get_value()))); } ScalarFunc::Max => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest] = Register::Value(Value::exec_max(reg_values.iter().map(|v| v.get_value()))); } ScalarFunc::Nullif => { let first_value = &state.registers[*start_reg]; let second_value = &state.registers[*start_reg + 1]; state.registers[*dest] = Register::Value(Value::exec_nullif( first_value.get_value(), second_value.get_value(), )); } ScalarFunc::Substr | ScalarFunc::Substring => { let str_value = &state.registers[*start_reg]; let start_value = &state.registers[*start_reg + 1]; let length_value = if func.arg_count == 3 { Some(&state.registers[*start_reg + 2]) } else { None }; let result = Value::exec_substring( str_value.get_value(), start_value.get_value(), length_value.map(|x| x.get_value()), ); state.registers[*dest] = Register::Value(result); } ScalarFunc::Date => { let result = exec_date(&state.registers[*start_reg..*start_reg + arg_count]); state.registers[*dest] = Register::Value(result); } ScalarFunc::Time => { let values = &state.registers[*start_reg..*start_reg + arg_count]; let result = exec_time(values); state.registers[*dest] = Register::Value(result); } ScalarFunc::TimeDiff => { if arg_count != 2 { state.registers[*dest] = Register::Value(Value::Null); } else { let start = state.registers[*start_reg].get_value().clone(); let end = state.registers[*start_reg + 1].get_value().clone(); let result = crate::functions::datetime::exec_timediff(&[ Register::Value(start), Register::Value(end), ]); state.registers[*dest] = Register::Value(result); } } ScalarFunc::TotalChanges => { let res = &program.connection.total_changes; let total_changes = res.load(Ordering::SeqCst); state.registers[*dest] = Register::Value(Value::Integer(total_changes)); } ScalarFunc::DateTime => { let result = exec_datetime_full(&state.registers[*start_reg..*start_reg + arg_count]); state.registers[*dest] = Register::Value(result); } ScalarFunc::JulianDay => { let result = exec_julianday(&state.registers[*start_reg..*start_reg + arg_count]); state.registers[*dest] = Register::Value(result); } ScalarFunc::UnixEpoch => { if *start_reg == 0 { let result = exec_unixepoch(&Value::build_text("now"))?; state.registers[*dest] = Register::Value(result); } else { let datetime_value = &state.registers[*start_reg]; let unixepoch = exec_unixepoch(datetime_value.get_value()); match unixepoch { Ok(time) => state.registers[*dest] = Register::Value(time), Err(e) => { return Err(LimboError::ParseError(format!( "Error encountered while parsing datetime value: {e}" ))); } } } } ScalarFunc::TursoVersion => { if !program.connection.is_db_initialized() { state.registers[*dest] = Register::Value(Value::build_text(info::build::PKG_VERSION)); } else { let version_integer = return_if_io!(pager.with_header(|header| header.version_number)).get() as i64; let version = execute_turso_version(version_integer); state.registers[*dest] = Register::Value(Value::build_text(version)); } } ScalarFunc::SqliteVersion => { let version = execute_sqlite_version(); state.registers[*dest] = Register::Value(Value::build_text(version)); } ScalarFunc::SqliteSourceId => { let src_id = format!( "{} {}", info::build::BUILT_TIME_SQLITE, info::build::GIT_COMMIT_HASH.unwrap_or("unknown") ); state.registers[*dest] = Register::Value(Value::build_text(src_id)); } ScalarFunc::Replace => { assert_eq!(arg_count, 3); let source = &state.registers[*start_reg]; let pattern = &state.registers[*start_reg + 1]; let replacement = &state.registers[*start_reg + 2]; state.registers[*dest] = Register::Value(Value::exec_replace( source.get_value(), pattern.get_value(), replacement.get_value(), )); } #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] ScalarFunc::LoadExtension => { if !program.connection.db.can_load_extensions() { crate::bail_parse_error!("runtime extension loading is disabled"); } let extension = &state.registers[*start_reg]; let ext = resolve_ext_path(&extension.get_value().to_string())?; program.connection.load_extension(ext)?; } ScalarFunc::StrfTime => { let result = exec_strftime(&state.registers[*start_reg..*start_reg + arg_count]); state.registers[*dest] = Register::Value(result); } ScalarFunc::Printf => { let result = exec_printf(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } ScalarFunc::TableColumnsJsonArray => { assert_eq!(arg_count, 1); #[cfg(not(feature = "json"))] { return Err(LimboError::InvalidArgument( "table_columns_json_array: turso must be compiled with JSON support" .to_string(), )); } #[cfg(feature = "json")] { use crate::types::TextSubtype; let table = state.registers[*start_reg].get_value(); let Value::Text(table) = table else { return Err(LimboError::InvalidArgument( "table_columns_json_array: function argument must be of type TEXT" .to_string(), )); }; let table = { let schema = program.connection.schema.read(); match schema.get_table(table.as_str()) { Some(table) => table, None => { return Err(LimboError::InvalidArgument(format!( "table_columns_json_array: table {table} doesn't exists" ))) } } }; let mut json = json::jsonb::Jsonb::make_empty_array(table.columns().len() * 10); for column in table.columns() { let name = column.name.as_ref().unwrap(); let name_json = json::convert_ref_dbtype_to_jsonb( ValueRef::Text(name.as_bytes(), TextSubtype::Text), json::Conv::ToString, )?; json.append_jsonb_to_end(name_json.data()); } json.finalize_unsafe(json::jsonb::ElementType::ARRAY)?; state.registers[*dest] = Register::Value(json::json_string_to_db_type( json, json::jsonb::ElementType::ARRAY, json::OutputVariant::String, )?); } } ScalarFunc::BinRecordJsonObject => { assert_eq!(arg_count, 2); #[cfg(not(feature = "json"))] { return Err(LimboError::InvalidArgument( "bin_record_json_object: turso must be compiled with JSON support" .to_string(), )); } #[cfg(feature = "json")] 'outer: { use crate::types::RecordCursor; use std::str::FromStr; let columns_str = state.registers[*start_reg].get_value(); let bin_record = state.registers[*start_reg + 1].get_value(); let Value::Text(columns_str) = columns_str else { return Err(LimboError::InvalidArgument( "bin_record_json_object: function arguments must be of type TEXT and BLOB correspondingly".to_string() )); }; if let Value::Null = bin_record { state.registers[*dest] = Register::Value(Value::Null); break 'outer; } let Value::Blob(bin_record) = bin_record else { return Err(LimboError::InvalidArgument( "bin_record_json_object: function arguments must be of type TEXT and BLOB correspondingly".to_string() )); }; let mut columns_json_array = json::jsonb::Jsonb::from_str(columns_str.as_str())?; let columns_len = columns_json_array.array_len()?; let mut record = ImmutableRecord::new(bin_record.len()); record.start_serialization(bin_record); let mut record_cursor = RecordCursor::new(); let mut json = json::jsonb::Jsonb::make_empty_obj(columns_len); for i in 0..columns_len { let mut op = json::jsonb::SearchOperation::new(0); let path = json::path::JsonPath { elements: vec![ json::path::PathElement::Root(), json::path::PathElement::ArrayLocator(Some(i as i32)), ], }; columns_json_array.operate_on_path(&path, &mut op)?; let column_name = op.result(); json.append_jsonb_to_end(column_name.data()); let val = record_cursor.get_value(&record, i)?; if let ValueRef::Blob(..) = val { return Err(LimboError::InvalidArgument( "bin_record_json_object: formatting of BLOB values stored in binary record is not supported".to_string() )); } let val_json = json::convert_ref_dbtype_to_jsonb(val, json::Conv::NotStrict)?; json.append_jsonb_to_end(val_json.data()); } json.finalize_unsafe(json::jsonb::ElementType::OBJECT)?; state.registers[*dest] = Register::Value(json::json_string_to_db_type( json, json::jsonb::ElementType::OBJECT, json::OutputVariant::String, )?); } } ScalarFunc::Attach => { assert_eq!(arg_count, 3); let filename = state.registers[*start_reg].get_value(); let dbname = state.registers[*start_reg + 1].get_value(); let _key = state.registers[*start_reg + 2].get_value(); // Not used in read-only implementation let Value::Text(filename_str) = filename else { return Err(LimboError::InvalidArgument( "attach: filename argument must be text".to_string(), )); }; let Value::Text(dbname_str) = dbname else { return Err(LimboError::InvalidArgument( "attach: database name argument must be text".to_string(), )); }; program .connection .attach_database(filename_str.as_str(), dbname_str.as_str())?; state.registers[*dest] = Register::Value(Value::Null); } ScalarFunc::Detach => { assert_eq!(arg_count, 1); let dbname = state.registers[*start_reg].get_value(); let Value::Text(dbname_str) = dbname else { return Err(LimboError::InvalidArgument( "detach: database name argument must be text".to_string(), )); }; // Call the detach_database method on the connection program.connection.detach_database(dbname_str.as_str())?; // Set result to NULL (detach doesn't return a value) state.registers[*dest] = Register::Value(Value::Null); } ScalarFunc::Unlikely | ScalarFunc::Likely | ScalarFunc::Likelihood => { panic!( "{scalar_func:?} should be stripped during expression translation and never reach VDBE", ); } }, crate::function::Func::Vector(vector_func) => match vector_func { VectorFunc::Vector => { let result = vector32(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::Vector32 => { let result = vector32(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::Vector32Sparse => { let result = vector32_sparse(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::Vector64 => { let result = vector64(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorExtract => { let result = vector_extract(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorDistanceCos => { let result = vector_distance_cos(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorDistanceL2 => { let result = vector_distance_l2(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorDistanceJaccard => { let result = vector_distance_jaccard(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorConcat => { let result = vector_concat(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result); } VectorFunc::VectorSlice => { let result = vector_slice(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest] = Register::Value(result) } }, crate::function::Func::External(f) => match f.func { ExtFunc::Scalar(f) => { if arg_count == 0 { let result_c_value: ExtValue = unsafe { (f)(0, std::ptr::null()) }; match Value::from_ffi(result_c_value) { Ok(result_ov) => { state.registers[*dest] = Register::Value(result_ov); } Err(e) => { return Err(e); } } } else { let register_slice = &state.registers[*start_reg..*start_reg + arg_count]; let mut ext_values: Vec = Vec::with_capacity(arg_count); for ov in register_slice.iter() { let val = ov.get_value().to_ffi(); ext_values.push(val); } let argv_ptr = ext_values.as_ptr(); let result_c_value: ExtValue = unsafe { (f)(arg_count as i32, argv_ptr) }; match Value::from_ffi(result_c_value) { Ok(result_ov) => { state.registers[*dest] = Register::Value(result_ov); } Err(e) => { return Err(e); } } } } _ => unreachable!("aggregate called in scalar context"), }, crate::function::Func::Math(math_func) => match math_func.arity() { MathFuncArity::Nullary => match math_func { MathFunc::Pi => { state.registers[*dest] = Register::Value(Value::Float(std::f64::consts::PI)); } _ => { unreachable!("Unexpected mathematical Nullary function {:?}", math_func); } }, MathFuncArity::Unary => { let reg_value = &state.registers[*start_reg]; let result = reg_value.get_value().exec_math_unary(math_func); state.registers[*dest] = Register::Value(result); } MathFuncArity::Binary => { let lhs = &state.registers[*start_reg]; let rhs = &state.registers[*start_reg + 1]; let result = lhs.get_value().exec_math_binary(rhs.get_value(), math_func); state.registers[*dest] = Register::Value(result); } MathFuncArity::UnaryOrBinary => match math_func { MathFunc::Log => { let result = match arg_count { 1 => { let arg = &state.registers[*start_reg]; arg.get_value().exec_math_log(None) } 2 => { let base = &state.registers[*start_reg]; let arg = &state.registers[*start_reg + 1]; arg.get_value().exec_math_log(Some(base.get_value())) } _ => unreachable!( "{:?} function with unexpected number of arguments", math_func ), }; state.registers[*dest] = Register::Value(result); } _ => unreachable!( "Unexpected mathematical UnaryOrBinary function {:?}", math_func ), }, }, crate::function::Func::AlterTable(alter_func) => { let r#type = &state.registers[*start_reg].get_value().clone(); let Value::Text(name) = &state.registers[*start_reg + 1].get_value() else { panic!("sqlite_schema.name should be TEXT") }; let name = name.to_string(); let Value::Text(tbl_name) = &state.registers[*start_reg + 2].get_value() else { panic!("sqlite_schema.tbl_name should be TEXT") }; let tbl_name = tbl_name.to_string(); let Value::Integer(root_page) = &state.registers[*start_reg + 3].get_value().clone() else { panic!("sqlite_schema.root_page should be INTEGER") }; let sql = &state.registers[*start_reg + 4].get_value().clone(); let (new_name, new_tbl_name, new_sql) = match alter_func { AlterTableFunc::RenameTable => { let rename_from = { match &state.registers[*start_reg + 5].get_value() { Value::Text(rename_from) => normalize_ident(rename_from.as_str()), _ => panic!("rename_from parameter should be TEXT"), } }; let original_rename_to = { match &state.registers[*start_reg + 6].get_value() { Value::Text(rename_to) => rename_to, _ => panic!("rename_to parameter should be TEXT"), } }; let rename_to = normalize_ident(original_rename_to.as_str()); let new_name = if let Some(column) = &name.strip_prefix(&format!("sqlite_autoindex_{rename_from}_")) { format!("sqlite_autoindex_{rename_to}_{column}") } else if name == rename_from { rename_to.clone() } else { name }; let new_tbl_name = if tbl_name == rename_from { rename_to.clone() } else { tbl_name }; let new_sql = 'sql: { let Value::Text(sql) = sql else { break 'sql None; }; let mut parser = Parser::new(sql.as_str().as_bytes()); let ast::Cmd::Stmt(stmt) = parser.next().unwrap().unwrap() else { todo!() }; match stmt { ast::Stmt::CreateIndex { tbl_name, unique, if_not_exists, idx_name, columns, where_clause, using, with_clause, } => { let table_name = normalize_ident(tbl_name.as_str()); if rename_from != table_name { break 'sql None; } Some( ast::Stmt::CreateIndex { tbl_name: ast::Name::exact(original_rename_to.to_string()), unique, if_not_exists, idx_name, columns, where_clause, using, with_clause, } .to_string(), ) } ast::Stmt::CreateTable { tbl_name, temporary, if_not_exists, body, } => { let this_table = normalize_ident(tbl_name.name.as_str()); let ast::CreateTableBody::ColumnsAndConstraints { mut columns, mut constraints, options, } = body else { todo!() }; let mut any_change = false; // Rewrite FK targets in both paths for c in &mut constraints { if let ast::TableConstraint::ForeignKey { clause, .. } = &mut c.constraint { any_change |= rewrite_fk_parent_table_if_needed( clause, &rename_from, original_rename_to.as_str(), ); } } for col in &mut columns { any_change |= rewrite_inline_col_fk_target_if_needed( col, &rename_from, original_rename_to.as_str(), ); } if this_table == rename_from { // Rebuild with new table identifier so SQL persists the new name. let new_stmt = ast::Stmt::CreateTable { tbl_name: ast::QualifiedName { db_name: None, name: ast::Name::exact(original_rename_to.to_string()), alias: None, }, temporary, if_not_exists, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, }; Some(new_stmt.to_string()) } else { // Other tables: only emit if we actually changed their FK targets. if !any_change { break 'sql None; } Some( ast::Stmt::CreateTable { tbl_name, temporary, if_not_exists, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, } .to_string(), ) } } ast::Stmt::CreateVirtualTable(ast::CreateVirtualTable { tbl_name, if_not_exists, module_name, args, }) => { let this_table = normalize_ident(tbl_name.name.as_str()); if this_table != rename_from { None } else { let new_stmt = ast::Stmt::CreateVirtualTable(ast::CreateVirtualTable { tbl_name: ast::QualifiedName { db_name: tbl_name.db_name, name: ast::Name::exact( original_rename_to.to_string(), ), alias: None, }, if_not_exists, module_name, args, }); Some(new_stmt.to_string()) } } _ => None, } }; (new_name, new_tbl_name, new_sql) } AlterTableFunc::AlterColumn | AlterTableFunc::RenameColumn => { let table = { match &state.registers[*start_reg + 5].get_value() { Value::Text(rename_to) => normalize_ident(rename_to.as_str()), _ => panic!("table parameter should be TEXT"), } }; let original_rename_from = { match &state.registers[*start_reg + 6].get_value() { Value::Text(rename_from) => rename_from, _ => panic!("rename_from parameter should be TEXT"), } }; let rename_from = normalize_ident(original_rename_from.as_str()); let column_def = { match &state.registers[*start_reg + 7].get_value() { Value::Text(column_def) => column_def.as_str(), _ => panic!("rename_to parameter should be TEXT"), } }; let column_def = Parser::new(column_def.as_bytes()) .parse_column_definition(true) .unwrap(); let rename_to = normalize_ident(column_def.col_name.as_str()); let new_sql = 'sql: { let Value::Text(sql) = sql else { break 'sql None; }; let mut parser = Parser::new(sql.as_str().as_bytes()); let ast::Cmd::Stmt(stmt) = parser.next().unwrap().unwrap() else { todo!() }; match stmt { ast::Stmt::CreateIndex { tbl_name, mut columns, unique, if_not_exists, idx_name, where_clause, using, with_clause, } => { if table != normalize_ident(tbl_name.as_str()) { break 'sql None; } for column in &mut columns { match column.expr.as_mut() { ast::Expr::Id(id) if normalize_ident(id.as_str()) == rename_from => { *id = Name::exact( column_def.col_name.as_str().to_owned(), ); } _ => {} } } Some( ast::Stmt::CreateIndex { tbl_name, columns, unique, if_not_exists, idx_name, where_clause, using, with_clause, } .to_string(), ) } ast::Stmt::CreateTable { tbl_name, body, temporary, if_not_exists, } => { let ast::CreateTableBody::ColumnsAndConstraints { mut columns, mut constraints, options, } = body else { todo!() }; let normalized_tbl_name = normalize_ident(tbl_name.name.as_str()); if normalized_tbl_name == table { // This is the table being altered - update its column let column = columns .iter_mut() .find(|column| { column.col_name.as_str() == original_rename_from.as_str() }) .expect("column being renamed should be present"); match alter_func { AlterTableFunc::AlterColumn => *column = column_def.clone(), AlterTableFunc::RenameColumn => { column.col_name = column_def.col_name.clone() } _ => unreachable!(), } // Update table-level constraints (PRIMARY KEY, UNIQUE, FOREIGN KEY) for constraint in &mut constraints { match &mut constraint.constraint { ast::TableConstraint::PrimaryKey { columns: pk_cols, .. } => { for col in pk_cols { let (ast::Expr::Name(ref name) | ast::Expr::Id(ref name)) = *col.expr else { return Err(LimboError::ParseError("Unexpected expression in PRIMARY KEY constraint".to_string())); }; if normalize_ident(name.as_str()) == rename_from { *col.expr = ast::Expr::Name(Name::exact( column_def.col_name.as_str().to_owned(), )); } } } ast::TableConstraint::Unique { columns: uniq_cols, .. } => { for col in uniq_cols { let (ast::Expr::Name(ref name) | ast::Expr::Id(ref name)) = *col.expr else { return Err(LimboError::ParseError("Unexpected expression in UNIQUE constraint".to_string())); }; if normalize_ident(name.as_str()) == rename_from { *col.expr = ast::Expr::Name(Name::exact( column_def.col_name.as_str().to_owned(), )); } } } ast::TableConstraint::ForeignKey { columns: child_cols, clause, .. } => { // Update child columns in this table's FK definitions for child_col in child_cols { if normalize_ident(child_col.col_name.as_str()) == rename_from { child_col.col_name = Name::exact( column_def.col_name.as_str().to_owned(), ); } } rewrite_fk_parent_cols_if_self_ref( clause, &normalized_tbl_name, &rename_from, column_def.col_name.as_str(), ); } _ => {} } for col in &mut columns { rewrite_column_references_if_needed( col, &normalized_tbl_name, &rename_from, column_def.col_name.as_str(), ); } } } else { // This is a different table, check if it has FKs referencing the renamed column let mut fk_updated = false; for constraint in &mut constraints { if let ast::TableConstraint::ForeignKey { columns: _, clause: ForeignKeyClause { tbl_name, columns: parent_cols, .. }, .. } = &mut constraint.constraint { // Check if this FK references the table being altered if normalize_ident(tbl_name.as_str()) == table { // Update parent column references if they match the renamed column for parent_col in parent_cols { if normalize_ident(parent_col.col_name.as_str()) == rename_from { parent_col.col_name = Name::exact( column_def.col_name.as_str().to_owned(), ); fk_updated = true; } } } } } for col in &mut columns { let before = fk_updated; let mut local_col = col.clone(); rewrite_column_references_if_needed( &mut local_col, &table, &rename_from, column_def.col_name.as_str(), ); if local_col != *col { *col = local_col; fk_updated = true; } } // Only return updated SQL if we actually changed something if !fk_updated { break 'sql None; } } Some( ast::Stmt::CreateTable { tbl_name, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, temporary, if_not_exists, } .to_string(), ) } _ => None, } }; (name, tbl_name, new_sql) } }; state.registers[*dest] = Register::Value(r#type.clone()); state.registers[*dest + 1] = Register::Value(Value::Text(Text::from(new_name))); state.registers[*dest + 2] = Register::Value(Value::Text(Text::from(new_tbl_name))); state.registers[*dest + 3] = Register::Value(Value::Integer(*root_page)); if let Some(new_sql) = new_sql { state.registers[*dest + 4] = Register::Value(Value::Text(Text::from(new_sql))); } else { state.registers[*dest + 4] = Register::Value(sql.clone()); } } crate::function::Func::Agg(_) => { unreachable!("Aggregate functions should not be handled here") } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sequence( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Sequence { cursor_id, target_reg }, insn ); let cursor_seq = state.cursor_seqs.get_mut(*cursor_id).unwrap(); let seq_num = *cursor_seq; *cursor_seq += 1; state.registers[*target_reg] = Register::Value(Value::Integer(seq_num)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sequence_test( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SequenceTest { cursor_id, target_pc, value_reg }, insn ); let cursor_seq = state.cursor_seqs.get_mut(*cursor_id).unwrap(); let was_zero = *cursor_seq == 0; *cursor_seq += 1; state.pc = if was_zero { target_pc.as_offset_int() } else { state.pc + 1 }; Ok(InsnFunctionStepResult::Step) } pub fn op_init_coroutine( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( InitCoroutine { yield_reg, jump_on_definition, start_offset, }, insn ); assert!(jump_on_definition.is_offset()); let start_offset = start_offset.as_offset_int(); state.registers[*yield_reg] = Register::Value(Value::Integer(start_offset as i64)); state.ended_coroutine.unset(*yield_reg); let jump_on_definition = jump_on_definition.as_offset_int(); state.pc = if jump_on_definition == 0 { state.pc + 1 } else { jump_on_definition }; Ok(InsnFunctionStepResult::Step) } pub fn op_end_coroutine( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(EndCoroutine { yield_reg }, insn); if let Value::Integer(pc) = state.registers[*yield_reg].get_value() { state.ended_coroutine.set(*yield_reg); let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("EndCoroutine: pc overflow: {pc}")); state.pc = pc - 1; // yield jump is always next to yield. Here we subtract 1 to go back to yield instruction } else { unreachable!(); } Ok(InsnFunctionStepResult::Step) } pub fn op_yield( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Yield { yield_reg, end_offset, }, insn ); if let Value::Integer(pc) = state.registers[*yield_reg].get_value() { if state.ended_coroutine.get(*yield_reg) { state.pc = end_offset.as_offset_int(); } else { let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("Yield: pc overflow: {pc}")); // swap the program counter with the value in the yield register // this is the mechanism that allows jumping back and forth between the coroutine and the caller (state.pc, state.registers[*yield_reg]) = (pc, Register::Value(Value::Integer((state.pc + 1) as i64))); } } else { unreachable!( "yield_reg {} contains non-integer value: {:?}", *yield_reg, state.registers[*yield_reg] ); } Ok(InsnFunctionStepResult::Step) } pub struct OpInsertState { pub sub_state: OpInsertSubState, pub old_record: Option<(i64, Vec)>, } #[derive(Debug, PartialEq)] pub enum OpInsertSubState { /// If this insert overwrites a record, capture the old record for incremental view maintenance. MaybeCaptureRecord, /// Seek to the correct position if needed. /// In a table insert, if the caller does not pass InsertFlags::REQUIRE_SEEK, they must ensure that a seek has already happened to the correct location. /// This typically happens by invoking either Insn::NewRowid or Insn::NotExists, because: /// 1. op_new_rowid() seeks to the end of the table, which is the correct insertion position. /// 2. op_not_exists() seeks to the position in the table where the target rowid would be inserted. Seek, /// Insert the row into the table. Insert, /// Updating last_insert_rowid may return IO, so we need a separate state for it so that we don't /// start inserting the same row multiple times. UpdateLastRowid, /// If there are dependent incremental views, apply the change. ApplyViewChange, } pub fn op_insert( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Insert { cursor: cursor_id, key_reg, record_reg, flag, table_name, }, insn ); loop { match &state.op_insert_state.sub_state { OpInsertSubState::MaybeCaptureRecord => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); // If there are no dependent views, we don't need to capture the old record. // We also don't need to do it if the rowid of the UPDATEd row was changed, because that means // we deleted it earlier and `op_delete` already captured the change. if dependent_views.is_empty() || flag.has(InsertFlags::UPDATE_ROWID_CHANGE) { if flag.has(InsertFlags::REQUIRE_SEEK) { state.op_insert_state.sub_state = OpInsertSubState::Seek; } else { state.op_insert_state.sub_state = OpInsertSubState::Insert; } continue; } turso_assert!(!flag.has(InsertFlags::REQUIRE_SEEK), "to capture old record accurately, we must be located at the correct position in the table"); // Get the key we're going to insert let insert_key = match &state.registers[*key_reg].get_value() { Value::Integer(i) => *i, _ => { // If key is not an integer, we can't check - assume no old record state.op_insert_state.old_record = None; state.op_insert_state.sub_state = if flag.has(InsertFlags::REQUIRE_SEEK) { OpInsertSubState::Seek } else { OpInsertSubState::Insert }; continue; } }; let old_record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); // Get the current key - for INSERT operations, there may not be a current row let maybe_key = return_if_io!(cursor.rowid()); if let Some(key) = maybe_key { // Only capture as old record if the cursor is at the position we're inserting to if key == insert_key { // Get the current record before deletion and extract values let maybe_record = return_if_io!(cursor.record()); if let Some(record) = maybe_record { let mut values = record .get_values() .into_iter() .map(|v| v.to_owned()) .collect::>(); // Fix rowid alias columns: replace Null with actual rowid value if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < values.len() { values[i] = Value::Integer(key); } } } Some((key, values)) } else { None } } else { // Cursor is at wrong position - this is a fresh INSERT, not a replacement None } } else { // No current row - this is a fresh INSERT, not an UPDATE None } }; state.op_insert_state.old_record = old_record; if flag.has(InsertFlags::REQUIRE_SEEK) { state.op_insert_state.sub_state = OpInsertSubState::Seek; } else { state.op_insert_state.sub_state = OpInsertSubState::Insert; } continue; } OpInsertSubState::Seek => { if let SeekInternalResult::IO(io) = seek_internal( program, state, pager, mv_store, RecordSource::Unpacked { start_reg: *key_reg, num_regs: 1, }, *cursor_id, false, SeekOp::GE { eq_only: true }, )? { return Ok(InsnFunctionStepResult::IO(io)); } state.op_insert_state.sub_state = OpInsertSubState::Insert; } OpInsertSubState::Insert => { let key = match &state.registers[*key_reg].get_value() { Value::Integer(i) => *i, _ => unreachable!("expected integer key"), }; let record = match &state.registers[*record_reg] { Register::Record(r) => std::borrow::Cow::Borrowed(r), Register::Value(value) => { let x = 1; let regs = &state.registers[*record_reg..*record_reg + 1]; let new_regs = [&state.registers[*record_reg]]; let record = ImmutableRecord::from_registers(new_regs, new_regs.len()); std::borrow::Cow::Owned(record) } Register::Aggregate(..) => unreachable!("Cannot insert an aggregate value."), }; { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.insert(&BTreeKey::new_table_rowid(key, Some(&record)))); } // Increment metrics for row write state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); // Only update last_insert_rowid for regular table inserts, not schema modifications let root_page = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); cursor.root_page() }; if root_page != 1 && table_name != "sqlite_sequence" && !flag.has(InsertFlags::EPHEMERAL_TABLE_INSERT) { state.op_insert_state.sub_state = OpInsertSubState::UpdateLastRowid; } else { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if !dependent_views.is_empty() { state.op_insert_state.sub_state = OpInsertSubState::ApplyViewChange; } else { break; } } } OpInsertSubState::UpdateLastRowid => { let maybe_rowid = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if let Some(rowid) = maybe_rowid { program.connection.update_last_rowid(rowid); program .n_change .fetch_add(1, std::sync::atomic::Ordering::SeqCst); } let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if !dependent_views.is_empty() { state.op_insert_state.sub_state = OpInsertSubState::ApplyViewChange; continue; } break; } OpInsertSubState::ApplyViewChange => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); assert!(!dependent_views.is_empty()); let (key, values) = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); let key = match &state.registers[*key_reg].get_value() { Value::Integer(i) => *i, _ => unreachable!("expected integer key"), }; let record = match &state.registers[*record_reg] { Register::Record(r) => std::borrow::Cow::Borrowed(r), Register::Value(value) => { let x = 1; let regs = &state.registers[*record_reg..*record_reg + 1]; let new_regs = [&state.registers[*record_reg]]; let record = ImmutableRecord::from_registers(new_regs, new_regs.len()); std::borrow::Cow::Owned(record) } Register::Aggregate(..) => { unreachable!("Cannot insert an aggregate value.") } }; // Add insertion of new row to view deltas let mut new_values = record .get_values() .into_iter() .map(|v| v.to_owned()) .collect::>(); // Fix rowid alias columns: replace Null with actual rowid value let schema = program.connection.schema.read(); if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < new_values.len() { new_values[i] = Value::Integer(key); } } } (key, new_values) }; if let Some((key, values)) = state.op_insert_state.old_record.take() { for view_name in dependent_views.iter() { let tx_state = program .connection .view_transaction_states .get_or_create(view_name); tx_state.delete(table_name, key, values.clone()); } } for view_name in dependent_views.iter() { let tx_state = program .connection .view_transaction_states .get_or_create(view_name); tx_state.insert(table_name, key, values.clone()); } break; } } } state.op_insert_state.sub_state = OpInsertSubState::MaybeCaptureRecord; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_int_64( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Int64 { _p1, out_reg, _p3, value, }, insn ); state.registers[*out_reg] = Register::Value(Value::Integer(*value)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub struct OpDeleteState { pub sub_state: OpDeleteSubState, pub deleted_record: Option<(i64, Vec)>, } pub enum OpDeleteSubState { /// Capture the record before deletion, if the are dependent views. MaybeCaptureRecord, /// Delete the record. Delete, /// Apply the change to the dependent views. ApplyViewChange, } pub fn op_delete( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Delete { cursor_id, table_name, is_part_of_update, }, insn ); loop { match &state.op_delete_state.sub_state { OpDeleteSubState::MaybeCaptureRecord => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if dependent_views.is_empty() { state.op_delete_state.sub_state = OpDeleteSubState::Delete; continue; } let deleted_record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); // Get the current key let maybe_key = return_if_io!(cursor.rowid()); let key = maybe_key.ok_or_else(|| { LimboError::InternalError("Cannot delete: no current row".to_string()) })?; // Get the current record before deletion and extract values let maybe_record = return_if_io!(cursor.record()); if let Some(record) = maybe_record { let mut values = record .get_values() .into_iter() .map(|v| v.to_owned()) .collect::>(); // Fix rowid alias columns: replace Null with actual rowid value if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < values.len() { values[i] = Value::Integer(key); } } } Some((key, values)) } else { None } }; state.op_delete_state.deleted_record = deleted_record; state.op_delete_state.sub_state = OpDeleteSubState::Delete; continue; } OpDeleteSubState::Delete => { { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.delete()); } // Increment metrics for row write (DELETE is a write operation) state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if dependent_views.is_empty() { break; } state.op_delete_state.sub_state = OpDeleteSubState::ApplyViewChange; continue; } OpDeleteSubState::ApplyViewChange => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); assert!(!dependent_views.is_empty()); let maybe_deleted_record = state.op_delete_state.deleted_record.take(); if let Some((key, values)) = maybe_deleted_record { for view_name in dependent_views { let tx_state = program .connection .view_transaction_states .get_or_create(&view_name); tx_state.delete(table_name, key, values.clone()); } } break; } } } state.op_delete_state.sub_state = OpDeleteSubState::MaybeCaptureRecord; if !is_part_of_update { // DELETEs do not count towards the total changes if they are part of an UPDATE statement, // i.e. the DELETE and subsequent INSERT of a row are the same "change". program .n_change .fetch_add(1, std::sync::atomic::Ordering::SeqCst); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug)] pub enum OpIdxDeleteState { Seeking, Verifying, Deleting, } pub fn op_idx_delete( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxDelete { cursor_id, start_reg, num_regs, raise_error_if_no_matching_entry, }, insn ); tracing::info!("idx_delete cursor: {:?}", program.cursor_ref[*cursor_id]); if let Some(Cursor::IndexMethod(cursor)) = &mut state.cursors[*cursor_id] { return_if_io!(cursor.delete(&state.registers[*start_reg..*start_reg + *num_regs])); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } loop { #[cfg(debug_assertions)] tracing::debug!( "op_idx_delete(cursor_id={}, start_reg={}, num_regs={}, rootpage={}, state={:?})", cursor_id, start_reg, num_regs, state.get_cursor(*cursor_id).as_btree_mut().root_page(), state.op_idx_delete_state ); match &state.op_idx_delete_state { Some(OpIdxDeleteState::Seeking) => { let found = match seek_internal( program, state, pager, mv_store, RecordSource::Unpacked { start_reg: *start_reg, num_regs: *num_regs, }, *cursor_id, true, SeekOp::GE { eq_only: true }, ) { Ok(SeekInternalResult::Found) => true, Ok(SeekInternalResult::NotFound) => false, Ok(SeekInternalResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(e) => return Err(e), }; if !found { // If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry is found // Also, do not raise this (self-correcting and non-critical) error if in writable_schema mode. if *raise_error_if_no_matching_entry { let reg_values = (*start_reg..*start_reg + *num_regs) .map(|i| &state.registers[i]) .collect::>(); return Err(LimboError::Corrupt(format!( "IdxDelete: no matching index entry found for key {reg_values:?} while seeking" ))); } state.pc += 1; state.op_idx_delete_state = None; return Ok(InsnFunctionStepResult::Step); } state.op_idx_delete_state = Some(OpIdxDeleteState::Verifying); } Some(OpIdxDeleteState::Verifying) => { let rowid = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if rowid.is_none() && *raise_error_if_no_matching_entry { let reg_values = (*start_reg..*start_reg + *num_regs) .map(|i| &state.registers[i]) .collect::>(); return Err(LimboError::Corrupt(format!( "IdxDelete: no matching index entry found for key while verifying: {reg_values:?}" ))); } state.op_idx_delete_state = Some(OpIdxDeleteState::Deleting); } Some(OpIdxDeleteState::Deleting) => { { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.delete()); } // Increment metrics for index write (delete is a write operation) state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); state.pc += 1; state.op_idx_delete_state = None; return Ok(InsnFunctionStepResult::Step); } None => { state.op_idx_delete_state = Some(OpIdxDeleteState::Seeking); } } } } #[derive(Debug, PartialEq, Copy, Clone)] pub enum OpIdxInsertState { /// Optional seek step done before an unique constraint check or if the caller indicates a seek is required. MaybeSeek, /// Optional unique constraint check done before an insert. UniqueConstraintCheck, /// Main insert step. This is always performed. Insert, } pub fn op_idx_insert( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IdxInsert { cursor_id, record_reg, flags, unpacked_start, unpacked_count, .. }, *insn ); if let Some(Cursor::IndexMethod(cursor)) = &mut state.cursors[cursor_id] { let Some(start) = unpacked_start else { return Err(LimboError::InternalError( "IndexMethod must receive unpacked values".to_string(), )); }; let Some(count) = unpacked_count else { return Err(LimboError::InternalError( "IndexMethod must receive unpacked values".to_string(), )); }; return_if_io!(cursor.insert(&state.registers[start..start + count as usize])); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let record_to_insert = match &state.registers[record_reg] { Register::Record(ref r) => r, o => { return Err(LimboError::InternalError(format!( "expected record, got {o:?}" ))); } }; match state.op_idx_insert_state { OpIdxInsertState::MaybeSeek => { let (_, cursor_type) = program.cursor_ref.get(cursor_id).unwrap(); let CursorType::BTreeIndex(index_meta) = cursor_type else { panic!("IdxInsert: not a BTreeIndex cursor"); }; // TODO: currently we never pass USE_SEEK, so this other check is a bit redundant and we always seek, // but I guess it's FutureProofed™® if !index_meta.unique && flags.has(IdxInsertFlags::USE_SEEK) { state.op_idx_insert_state = OpIdxInsertState::Insert; return Ok(InsnFunctionStepResult::Step); } match seek_internal( program, state, pager, mv_store, RecordSource::Packed { record_reg }, cursor_id, true, SeekOp::GE { eq_only: true }, )? { SeekInternalResult::Found => { state.op_idx_insert_state = if index_meta.unique { OpIdxInsertState::UniqueConstraintCheck } else { OpIdxInsertState::Insert }; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::NotFound => { state.op_idx_insert_state = OpIdxInsertState::Insert; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), } } OpIdxInsertState::UniqueConstraintCheck => { let ignore_conflict = 'i: { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); let record_opt = return_if_io!(cursor.record()); let Some(record) = record_opt.as_ref() else { // Cursor not pointing at a record — table is empty or past last break 'i false; }; // Cursor is pointing at a record; if the index has a rowid, exclude it from the comparison since it's a pointer to the table row; // UNIQUE indexes disallow duplicates like (a=1,b=2,rowid=1) and (a=1,b=2,rowid=2). let existing_key = if cursor.has_rowid() { let count = cursor.record_cursor_mut().count(record); &record.get_values()[..count.saturating_sub(1)] } else { &record.get_values()[..] }; let inserted_key_vals = &record_to_insert.get_values(); if existing_key.len() != inserted_key_vals.len() { break 'i false; } let conflict = compare_immutable( existing_key, inserted_key_vals, &cursor.get_index_info().key_info, ) == std::cmp::Ordering::Equal; if conflict { if flags.has(IdxInsertFlags::NO_OP_DUPLICATE) { break 'i true; } return Err(LimboError::Constraint( "UNIQUE constraint failed: duplicate key".into(), )); } false }; state.op_idx_insert_state = if ignore_conflict { state.pc += 1; OpIdxInsertState::MaybeSeek } else { OpIdxInsertState::Insert }; Ok(InsnFunctionStepResult::Step) } OpIdxInsertState::Insert => { { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.insert(&BTreeKey::new_index_key(record_to_insert))); } // Increment metrics for index write if flags.has(IdxInsertFlags::NCHANGE) { state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); } state.op_idx_insert_state = OpIdxInsertState::MaybeSeek; state.pc += 1; Ok(InsnFunctionStepResult::Step) } } } #[derive(Debug, Clone, Copy)] pub enum OpNewRowidState { Start, SeekingToLast, ReadingMaxRowid, GeneratingRandom { attempts: u32, }, VerifyingCandidate { attempts: u32, candidate: i64, }, /// In case a rowid was generated and not provided by the user, we need to call next() on the cursor /// after generating the rowid. This is because the rowid was generated by seeking to the last row in the /// table, and we need to insert _after_ that row. GoNext, } pub fn op_new_rowid( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( NewRowid { cursor, rowid_reg, prev_largest_reg, }, insn ); if let Some(mv_store) = mv_store { // With MVCC we can't simply find last rowid and get rowid + 1 as a result. To not have two conflicting rowids concurrently we need to call `get_next_rowid` // which will make sure we don't collide. let rowid = { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; let mvcc_cursor = cursor.downcast_mut::().unwrap(); mvcc_cursor.get_next_rowid() }; state.registers[*rowid_reg] = Register::Value(Value::Integer(rowid)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } const MAX_ROWID: i64 = i64::MAX; const MAX_ATTEMPTS: u32 = 100; loop { match state.op_new_rowid_state { OpNewRowidState::Start => { state.op_new_rowid_state = OpNewRowidState::SeekingToLast; } OpNewRowidState::SeekingToLast => { { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.seek_to_last()); } state.op_new_rowid_state = OpNewRowidState::ReadingMaxRowid; } OpNewRowidState::ReadingMaxRowid => { let current_max = { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if *prev_largest_reg > 0 { state.registers[*prev_largest_reg] = Register::Value(Value::Integer(current_max.unwrap_or(0))); } match current_max { Some(rowid) if rowid < MAX_ROWID => { // Can use sequential state.registers[*rowid_reg] = Register::Value(Value::Integer(rowid + 1)); state.op_new_rowid_state = OpNewRowidState::GoNext; continue; } Some(_) => { // Must use random (rowid == MAX_ROWID) state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: 0 }; } None => { // Empty table state.registers[*rowid_reg] = Register::Value(Value::Integer(1)); state.op_new_rowid_state = OpNewRowidState::GoNext; continue; } } } OpNewRowidState::GeneratingRandom { attempts } => { if attempts >= MAX_ATTEMPTS { return Err(LimboError::DatabaseFull("Unable to find an unused rowid after 100 attempts - database is probably full".to_string())); } // Generate a random i64 and constrain it to the lower half of the rowid range. // We use the lower half (1 to MAX_ROWID/2) because we're in random mode only // when sequential allocation reached MAX_ROWID, meaning the upper range is full. let mut random_rowid: i64 = pager.io.generate_random_number(); random_rowid &= MAX_ROWID >> 1; // Mask to keep value in range [0, MAX_ROWID/2] random_rowid += 1; // Ensure positive state.op_new_rowid_state = OpNewRowidState::VerifyingCandidate { attempts, candidate: random_rowid, }; } OpNewRowidState::VerifyingCandidate { attempts, candidate, } => { let exists = { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); let seek_result = return_if_io!(cursor .seek(SeekKey::TableRowId(candidate), SeekOp::GE { eq_only: true })); matches!(seek_result, SeekResult::Found) }; if !exists { // Found unused rowid! state.registers[*rowid_reg] = Register::Value(Value::Integer(candidate)); state.op_new_rowid_state = OpNewRowidState::Start; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } else { // Collision, try again state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: attempts + 1, }; } } OpNewRowidState::GoNext => { { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.next()); } state.op_new_rowid_state = OpNewRowidState::Start; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_must_be_int( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(MustBeInt { reg }, insn); match &state.registers[*reg].get_value() { Value::Integer(_) => {} Value::Float(f) => match cast_real_to_integer(*f) { Ok(i) => state.registers[*reg] = Register::Value(Value::Integer(i)), Err(_) => crate::bail_parse_error!("datatype mismatch"), }, Value::Text(text) => match checked_cast_text_to_numeric(text.as_str(), true) { Ok(Value::Integer(i)) => state.registers[*reg] = Register::Value(Value::Integer(i)), Ok(Value::Float(f)) => match cast_real_to_integer(f) { Ok(i) => state.registers[*reg] = Register::Value(Value::Integer(i)), Err(_) => crate::bail_parse_error!("datatype mismatch"), }, _ => crate::bail_parse_error!("datatype mismatch"), }, _ => { crate::bail_parse_error!("datatype mismatch"); } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_soft_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(SoftNull { reg }, insn); state.registers[*reg] = Register::Value(Value::Null); state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Clone, Copy)] pub enum OpNoConflictState { Start, Seeking(RecordSource), } /// If a matching record is not found in the btree ("no conflict"), jump to the target PC. /// Otherwise, continue execution. pub fn op_no_conflict( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( NoConflict { cursor_id, target_pc, record_reg, num_regs, }, insn ); loop { match state.op_no_conflict_state { OpNoConflictState::Start => { let cursor_ref = state.get_cursor(*cursor_id); let cursor = cursor_ref.as_btree_mut(); let record_source = if *num_regs == 0 { RecordSource::Packed { record_reg: *record_reg, } } else { RecordSource::Unpacked { start_reg: *record_reg, num_regs: *num_regs, } }; // If there is at least one NULL in the index record, there cannot be a conflict so we can immediately jump. let contains_nulls = match &record_source { RecordSource::Packed { record_reg } => { let Register::Record(record) = &state.registers[*record_reg] else { return Err(LimboError::InternalError( "NoConflict: expected a record in the register".into(), )); }; record .get_values() .iter() .any(|val| matches!(val, ValueRef::Null)) } RecordSource::Unpacked { start_reg, num_regs, } => (0..*num_regs).any(|i| { matches!( &state.registers[start_reg + i], Register::Value(Value::Null) ) }), }; if contains_nulls { state.pc = target_pc.as_offset_int(); state.op_no_conflict_state = OpNoConflictState::Start; return Ok(InsnFunctionStepResult::Step); } else { state.op_no_conflict_state = OpNoConflictState::Seeking(record_source); } } OpNoConflictState::Seeking(record_source) => { return match seek_internal( program, state, pager, mv_store, record_source, *cursor_id, true, SeekOp::GE { eq_only: true }, )? { SeekInternalResult::Found => { state.pc += 1; state.op_no_conflict_state = OpNoConflictState::Start; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::NotFound => { state.pc = target_pc.as_offset_int(); state.op_no_conflict_state = OpNoConflictState::Start; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), }; } } } } pub fn op_not_exists( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( NotExists { cursor, rowid_reg, target_pc, }, insn ); let cursor = must_be_btree_cursor!(*cursor, program.cursor_ref, state, "NotExists"); let cursor = cursor.as_btree_mut(); let exists = return_if_io!(cursor.exists(state.registers[*rowid_reg].get_value())); if exists { state.pc += 1; } else { state.pc = target_pc.as_offset_int(); } Ok(InsnFunctionStepResult::Step) } pub fn op_offset_limit( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( OffsetLimit { limit_reg, combined_reg, offset_reg, }, insn ); let limit_val = match state.registers[*limit_reg].get_value() { Value::Integer(val) => val, _ => { return Err(LimboError::InternalError( "OffsetLimit: the value in limit_reg is not an integer".into(), )); } }; let offset_val = match state.registers[*offset_reg].get_value() { Value::Integer(val) if *val < 0 => 0, Value::Integer(val) if *val >= 0 => *val, _ => { return Err(LimboError::InternalError( "OffsetLimit: the value in offset_reg is not an integer".into(), )); } }; let offset_limit_sum = limit_val.overflowing_add(offset_val); if *limit_val <= 0 || offset_limit_sum.1 { state.registers[*combined_reg] = Register::Value(Value::Integer(-1)); } else { state.registers[*combined_reg] = Register::Value(Value::Integer(offset_limit_sum.0)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } // this cursor may be reused for next insert // Update: tablemoveto is used to travers on not exists, on insert depending on flags if nonseek it traverses again. // If not there might be some optimizations obviously. pub fn op_open_write( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( OpenWrite { cursor_id, root_page, db, }, insn ); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let pager = program.get_pager_from_database_index(db); if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.open_write(&program.connection)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let root_page = match root_page { RegisterOrLiteral::Literal(lit) => *lit, RegisterOrLiteral::Register(reg) => match &state.registers[*reg].get_value() { Value::Integer(val) => *val, _ => { return Err(LimboError::InternalError( "OpenWrite: the value in root_page is not an integer".into(), )); } }, }; const SQLITE_SCHEMA_ROOT_PAGE: i64 = 1; if root_page == SQLITE_SCHEMA_ROOT_PAGE { if let Some(mv_store) = mv_store { let Some(tx_id) = program.connection.get_mv_tx_id() else { return Err(LimboError::InternalError( "Schema changes in MVCC mode require an exclusive MVCC transaction".to_string(), )); }; if !mv_store.is_exclusive_tx(&tx_id) { // Schema changes in MVCC mode require an exclusive transaction return Err(LimboError::TableLocked); } } } let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); let cursors = &mut state.cursors; let maybe_index = match cursor_type { CursorType::BTreeIndex(index) => Some(index), _ => None, }; // Check if we can reuse the existing cursor let can_reuse_cursor = if let Some(Some(Cursor::BTree(btree_cursor))) = cursors.get(*cursor_id) { // Reuse if the root_page matches (same table/index) btree_cursor.root_page() == root_page } else { false }; if !can_reuse_cursor { let maybe_promote_to_mvcc_cursor = |btree_cursor: Box| -> Result> { if let Some(tx_id) = program.connection.get_mv_tx_id() { let mv_store = mv_store.unwrap().clone(); Ok(Box::new(MvCursor::new( mv_store, tx_id, root_page, pager.clone(), btree_cursor, )?)) } else { Ok(btree_cursor) } }; if let Some(index) = maybe_index { let conn = program.connection.clone(); let schema = conn.schema.read(); let table = schema .get_table(&index.table_name) .and_then(|table| table.btree()); let num_columns = index.columns.len(); let btree_cursor = Box::new(BTreeCursor::new_index( pager.clone(), root_page, index.as_ref(), num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor)?; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } else { let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::MaterializedView(table_rc, _) => table_rc.columns.len(), _ => unreachable!( "Expected BTreeTable or MaterializedView. This should not have happened." ), }; let btree_cursor = Box::new(BTreeCursor::new_table( pager.clone(), root_page, num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor)?; cursors .get_mut(*cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_copy( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Copy { src_reg, dst_reg, extra_amount, }, insn ); for i in 0..=*extra_amount { state.registers[*dst_reg + i] = state.registers[*src_reg + i].clone(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_create_btree( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(CreateBtree { db, root, flags }, insn); assert_eq!(*db, 0); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } if *db > 0 { // TODO: implement temp databases todo!("temp databases not implemented yet"); } if let Some(mv_store) = mv_store { let root_page = mv_store.get_next_table_id(); state.registers[*root] = Register::Value(Value::Integer(root_page)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } // FIXME: handle page cache is full let root_page = return_if_io!(pager.btree_create(flags)); state.registers[*root] = Register::Value(Value::Integer(root_page as i64)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_create( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(IndexMethodCreate { db, cursor_id }, insn); assert_eq!(*db, 0); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } if let Some(mv_store) = mv_store { todo!("MVCC is not supported yet"); } if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } } let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.create(&program.connection)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_destroy( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(IndexMethodDestroy { db, cursor_id }, insn); assert_eq!(*db, 0); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } if let Some(mv_store) = mv_store { todo!("MVCC is not supported yet"); } if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } } let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.destroy(&program.connection)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_query( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IndexMethodQuery { db, cursor_id, start_reg, count_reg, pc_if_empty, }, insn ); assert_eq!(*db, 0); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } if let Some(mv_store) = mv_store { todo!("MVCC is not supported yet"); } let cursor = state.cursors[*cursor_id].as_mut().unwrap(); let cursor = cursor.as_index_method_mut(); let has_rows = return_if_io!(cursor.query_start(&state.registers[*start_reg..*start_reg + *count_reg])); if !has_rows { state.pc = pc_if_empty.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub enum OpDestroyState { CreateCursor, DestroyBtree(Arc>), } pub fn op_destroy( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Destroy { root, former_root_reg, is_temp, }, insn ); if *is_temp == 1 { todo!("temp databases not implemented yet."); } if mv_store.is_some() { // MVCC only does pager operations in checkpoint state.pc += 1; return Ok(InsnFunctionStepResult::Step); } loop { match state.op_destroy_state { OpDestroyState::CreateCursor => { // Destroy doesn't do anything meaningful with the table/index distinction so we can just use a // table btree cursor for both. let cursor = BTreeCursor::new(pager.clone(), *root, 0); state.op_destroy_state = OpDestroyState::DestroyBtree(Arc::new(RwLock::new(cursor))); } OpDestroyState::DestroyBtree(ref mut cursor) => { let maybe_former_root_page = return_if_io!(cursor.write().btree_destroy()); state.registers[*former_root_reg] = Register::Value(Value::Integer(maybe_former_root_page.unwrap_or(0) as i64)); state.op_destroy_state = OpDestroyState::CreateCursor; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_reset_sorter( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ResetSorter { cursor_id }, insn); let (_, cursor_type) = program.cursor_ref.get(*cursor_id).unwrap(); let cursor = state.get_cursor(*cursor_id); match cursor_type { CursorType::BTreeTable(table) => { let cursor = cursor.as_btree_mut(); return_if_io!(cursor.clear_btree()); } CursorType::Sorter => { unimplemented!("ResetSorter is not supported for sorter cursors yet") } _ => panic!("ResetSorter is not supported for {cursor_type:?}"), } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_table( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(DropTable { db, table_name, .. }, insn); if *db > 0 { todo!("temp databases not implemented yet"); } let conn = program.connection.clone(); { conn.with_schema_mut(|schema| { schema.remove_indices_for_table(table_name); schema.remove_table(table_name); }); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_view( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, _mv_store: Option<&Arc>, ) -> Result { load_insn!(DropView { db, view_name }, insn); if *db > 0 { todo!("temp databases not implemented yet"); } let conn = program.connection.clone(); conn.with_schema_mut(|schema| { schema.remove_view(view_name)?; Ok::<(), crate::LimboError>(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_close( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Close { cursor_id }, insn); let cursors = &mut state.cursors; cursors.get_mut(*cursor_id).unwrap().take(); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_is_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(IsNull { reg, target_pc }, insn); if matches!(state.registers[*reg], Register::Value(Value::Null)) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_coll_seq( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, _mv_store: Option<&Arc>, ) -> Result { let Insn::CollSeq { reg, collation } = insn else { unreachable!("unexpected Insn {:?}", insn) }; // Set the current collation sequence for use by subsequent functions state.current_collation = Some(*collation); // If P1 is not zero, initialize that register to 0 if let Some(reg_idx) = reg { state.registers[*reg_idx] = Register::Value(Value::Integer(0)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_page_count( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(PageCount { db, dest }, insn); if *db > 0 { // TODO: implement temp databases todo!("temp databases not implemented yet"); } let count = match with_header(pager, mv_store, program, |header| { header.database_size.get() }) { Err(_) => 0.into(), Ok(IOResult::Done(v)) => v.into(), Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; state.registers[*dest] = Register::Value(Value::Integer(count)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_parse_schema( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( ParseSchema { db: _, where_clause, }, insn ); let conn = program.connection.clone(); // set auto commit to false in order for parse schema to not commit changes as transaction state is stored in connection, // and we use the same connection for nested query. let previous_auto_commit = conn.auto_commit.load(Ordering::SeqCst); conn.auto_commit.store(false, Ordering::SeqCst); let maybe_nested_stmt_err = if let Some(where_clause) = where_clause { let stmt = conn.prepare(format!("SELECT * FROM sqlite_schema WHERE {where_clause}"))?; conn.with_schema_mut(|schema| { // TODO: This function below is synchronous, make it async let existing_views = schema.incremental_views.clone(); conn.start_nested(); parse_schema_rows( stmt, schema, &conn.syms.read(), program.connection.get_mv_tx(), existing_views, ) }) } else { let stmt = conn.prepare("SELECT * FROM sqlite_schema")?; conn.with_schema_mut(|schema| { // TODO: This function below is synchronous, make it async let existing_views = schema.incremental_views.clone(); conn.start_nested(); parse_schema_rows( stmt, schema, &conn.syms.read(), program.connection.get_mv_tx(), existing_views, ) }) }; conn.end_nested(); conn.auto_commit .store(previous_auto_commit, Ordering::SeqCst); maybe_nested_stmt_err?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_populate_materialized_views( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, _mv_store: Option<&Arc>, ) -> Result { load_insn!(PopulateMaterializedViews { cursors }, insn); let conn = program.connection.clone(); // For each view, get its cursor and root page let mut view_info = Vec::new(); { let cursors_ref = &state.cursors; for (view_name, cursor_id) in cursors { // Get the cursor to find the root page let cursor = cursors_ref .get(*cursor_id) .and_then(|c| c.as_ref()) .ok_or_else(|| { LimboError::InternalError(format!("Cursor {cursor_id} not found")) })?; let root_page = match cursor { crate::types::Cursor::BTree(btree_cursor) => btree_cursor.root_page(), _ => { return Err(LimboError::InternalError( "Expected BTree cursor for materialized view".into(), )) } }; view_info.push((view_name.clone(), root_page, *cursor_id)); } } // Now populate the views (after releasing the schema borrow) for (view_name, _root_page, cursor_id) in view_info { let schema = conn.schema.read(); if let Some(view) = schema.get_materialized_view(&view_name) { let mut view = view.lock().unwrap(); // Drop the schema borrow before calling populate_from_table drop(schema); // Get the cursor for writing // Get a mutable reference to the cursor let cursors_ref = &mut state.cursors; let cursor = cursors_ref .get_mut(cursor_id) .and_then(|c| c.as_mut()) .ok_or_else(|| { LimboError::InternalError(format!( "Cursor {cursor_id} not found for population" )) })?; // Extract the BTreeCursor let btree_cursor = match cursor { crate::types::Cursor::BTree(btree_cursor) => btree_cursor, _ => { return Err(LimboError::InternalError( "Expected BTree cursor for materialized view population".into(), )) } }; // Now populate it with the cursor for writing return_if_io!(view.populate_from_table(&conn, pager, btree_cursor.as_mut())); } } // All views populated, advance to next instruction state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_read_cookie( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ReadCookie { db, dest, cookie }, insn); if *db > 0 { // TODO: implement temp databases todo!("temp databases not implemented yet"); } let cookie_value = match with_header(pager, mv_store, program, |header| match cookie { Cookie::ApplicationId => header.application_id.get().into(), Cookie::UserVersion => header.user_version.get().into(), Cookie::SchemaVersion => header.schema_cookie.get().into(), Cookie::LargestRootPageNumber => header.vacuum_mode_largest_root_page.get().into(), cookie => todo!("{cookie:?} is not yet implement for ReadCookie"), }) { Err(_) => 0.into(), Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; state.registers[*dest] = Register::Value(Value::Integer(cookie_value)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_set_cookie( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( SetCookie { db, cookie, value, p5, }, insn ); if *db > 0 { todo!("temp databases not implemented yet"); } return_if_io!(with_header_mut(pager, mv_store, program, |header| { match cookie { Cookie::ApplicationId => header.application_id = (*value).into(), Cookie::UserVersion => header.user_version = (*value).into(), Cookie::LargestRootPageNumber => { header.vacuum_mode_largest_root_page = (*value as u32).into(); } Cookie::IncrementalVacuum => header.incremental_vacuum_enabled = (*value as u32).into(), Cookie::SchemaVersion => { // we update transaction state to indicate that the schema has changed match program.connection.get_tx_state() { TransactionState::Write { schema_did_change } => { program.connection.set_tx_state(TransactionState::Write { schema_did_change: true }); }, TransactionState::Read => unreachable!("invalid transaction state for SetCookie: TransactionState::Read, should be write"), TransactionState::None => unreachable!("invalid transaction state for SetCookie: TransactionState::None, should be write"), TransactionState::PendingUpgrade => unreachable!("invalid transaction state for SetCookie: TransactionState::PendingUpgrade, should be write"), } program .connection .with_schema_mut(|schema| schema.schema_version = *value as u32); header.schema_cookie = (*value as u32).into(); } cookie => todo!("{cookie:?} is not yet implement for SetCookie"), }; })); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_shift_right( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ShiftRight { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_shift_right(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_shift_left( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ShiftLeft { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_shift_left(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_add_imm( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(AddImm { register, value }, insn); let current = &state.registers[*register]; let current_value = match current { Register::Value(val) => val, Register::Aggregate(_) => &Value::Null, Register::Record(_) => &Value::Null, }; let int_val = match current_value { Value::Integer(i) => i + value, Value::Float(f) => (*f as i64) + value, Value::Text(s) => s.as_str().parse::().unwrap_or(0) + value, Value::Blob(_) => *value, // BLOB becomes the added value Value::Null => *value, // NULL becomes the added value }; state.registers[*register] = Register::Value(Value::Integer(int_val)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_variable( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Variable { index, dest }, insn); state.registers[*dest] = Register::Value(state.get_parameter(*index)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_zero_or_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(ZeroOrNull { rg1, rg2, dest }, insn); if state.registers[*rg1].is_null() || state.registers[*rg2].is_null() { state.registers[*dest] = Register::Value(Value::Null) } else { state.registers[*dest] = Register::Value(Value::Integer(0)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_not( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Not { reg, dest }, insn); state.registers[*dest] = Register::Value(state.registers[*reg].get_value().exec_boolean_not()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_concat( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Concat { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_concat(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_and( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(And { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_and(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_or( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(Or { lhs, rhs, dest }, insn); state.registers[*dest] = Register::Value( state.registers[*lhs] .get_value() .exec_or(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_noop( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { // Do nothing // Advance the program counter for the next opcode state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Default)] pub enum OpOpenEphemeralState { #[default] Start, StartingTxn { pager: Arc, }, CreateBtree { pager: Arc, }, // clippy complains this variant is too big when compared to the rest of the variants // so it says we need to box it here Rewind { cursor: Box, }, } pub fn op_open_ephemeral( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { let (cursor_id, is_table) = match insn { Insn::OpenEphemeral { cursor_id, is_table, } => (*cursor_id, *is_table), Insn::OpenAutoindex { cursor_id } => (*cursor_id, false), _ => unreachable!("unexpected Insn {:?}", insn), }; match &mut state.op_open_ephemeral_state { OpOpenEphemeralState::Start => { tracing::trace!("Start"); let page_size = return_if_io!(with_header(pager, mv_store, program, |header| header.page_size)); let conn = program.connection.clone(); let io = conn.pager.load().io.clone(); let rand_num = io.generate_random_number(); let db_file: Arc; let db_file_io: Arc; // we support OPFS in WASM - but it require files to be pre-opened in the browser before use // we can fix this if we will make open_file interface async // but for now for simplicity we use MemoryIO for all intermediate calculations #[cfg(target_family = "wasm")] { use crate::MemoryIO; db_file_io = Arc::new(MemoryIO::new()); let file = db_file_io.open_file("temp-file", OpenFlags::Create, false)?; db_file = Arc::new(DatabaseFile::new(file)); } #[cfg(not(target_family = "wasm"))] { let temp_dir = temp_dir(); let rand_path = std::path::Path::new(&temp_dir).join(format!("tursodb-ephemeral-{rand_num}")); let Some(rand_path_str) = rand_path.to_str() else { return Err(LimboError::InternalError( "Failed to convert path to string".to_string(), )); }; let file = io.open_file(rand_path_str, OpenFlags::Create, false)?; db_file = Arc::new(DatabaseFile::new(file)); db_file_io = io; } let buffer_pool = program.connection.db.buffer_pool.clone(); let page_cache = Arc::new(RwLock::new(PageCache::default())); let pager = Arc::new(Pager::new( db_file, None, db_file_io, page_cache, buffer_pool.clone(), Arc::new(AtomicDbState::new(DbState::Uninitialized)), Arc::new(Mutex::new(())), )?); pager.set_page_size(page_size); state.op_open_ephemeral_state = OpOpenEphemeralState::StartingTxn { pager }; } OpOpenEphemeralState::StartingTxn { pager } => { tracing::trace!("StartingTxn"); pager .begin_read_tx() // we have to begin a read tx before beginning a write .expect("Failed to start read transaction"); return_if_io!(pager.begin_write_tx()); state.op_open_ephemeral_state = OpOpenEphemeralState::CreateBtree { pager: pager.clone(), }; } OpOpenEphemeralState::CreateBtree { pager } => { tracing::trace!("CreateBtree"); // FIXME: handle page cache is full let flag = if is_table { &CreateBTreeFlags::new_table() } else { &CreateBTreeFlags::new_index() }; let root_page = return_if_io!(pager.btree_create(flag)) as i64; let (_, cursor_type) = program.cursor_ref.get(cursor_id).unwrap(); let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::BTreeIndex(index_arc) => index_arc.columns.len(), _ => unreachable!("This should not have happened"), }; let cursor = if let CursorType::BTreeIndex(index) = cursor_type { BTreeCursor::new_index(pager.clone(), root_page, index, num_columns) } else { BTreeCursor::new_table(pager.clone(), root_page, num_columns) }; state.op_open_ephemeral_state = OpOpenEphemeralState::Rewind { cursor: Box::new(cursor), }; } OpOpenEphemeralState::Rewind { cursor } => { return_if_io!(cursor.rewind()); let cursors = &mut state.cursors; let (_, cursor_type) = program.cursor_ref.get(cursor_id).unwrap(); let OpOpenEphemeralState::Rewind { cursor } = std::mem::take(&mut state.op_open_ephemeral_state) else { unreachable!() }; // Table content is erased if the cursor already exists match cursor_type { CursorType::BTreeTable(_) => { cursors .get_mut(cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(_) => { cursors .get_mut(cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } CursorType::Pseudo(_) => { panic!("OpenEphemeral on pseudo cursor"); } CursorType::Sorter => { panic!("OpenEphemeral on sorter cursor"); } CursorType::VirtualTable(_) => { panic!("OpenEphemeral on virtual table cursor, use Insn::VOpen instead"); } CursorType::IndexMethod(..) => { panic!("OpenEphemeral on index method cursor") } CursorType::MaterializedView(_, _) => { panic!("OpenEphemeral on materialized view cursor"); } } state.pc += 1; state.op_open_ephemeral_state = OpOpenEphemeralState::Start; } } Ok(InsnFunctionStepResult::Step) } pub fn op_open_dup( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( OpenDup { new_cursor_id, original_cursor_id, }, insn ); let original_cursor = state.get_cursor(*original_cursor_id); let original_cursor = original_cursor.as_btree_mut(); let root_page = original_cursor.root_page(); // We use the pager from the original cursor instead of the one attached to // the connection because each ephemeral table creates its own pager (and // a separate database file). let pager = original_cursor.get_pager(); let (_, cursor_type) = program.cursor_ref.get(*original_cursor_id).unwrap(); match cursor_type { CursorType::BTreeTable(table) => { let cursor = Box::new(BTreeCursor::new_table( pager.clone(), root_page, table.columns.len(), )); let cursor: Box = if let Some(tx_id) = program.connection.get_mv_tx_id() { let mv_store = mv_store.unwrap().clone(); Box::new(MvCursor::new( mv_store, tx_id, root_page, pager.clone(), cursor, )?) } else { cursor }; let cursors = &mut state.cursors; cursors .get_mut(*new_cursor_id) .unwrap() .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(table) => { // In principle, we could implement OpenDup for BTreeIndex, // but doing so now would create dead code since we have no use case, // and it wouldn't be possible to test it. unimplemented!("OpenDup is not supported for BTreeIndex yet") } _ => panic!("OpenDup is not supported for {cursor_type:?}"), } state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Execute the [Insn::Once] instruction. /// /// This instruction is used to execute a block of code only once. /// If the instruction is executed again, it will jump to the target program counter. pub fn op_once( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Once { target_pc_when_reentered, }, insn ); assert!(target_pc_when_reentered.is_offset()); let offset = state.pc; if state.once.iter().any(|o| o == offset) { state.pc = target_pc_when_reentered.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } state.once.push(offset); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_found( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { let (cursor_id, target_pc, record_reg, num_regs) = match insn { Insn::NotFound { cursor_id, target_pc, record_reg, num_regs, } => (cursor_id, target_pc, record_reg, num_regs), Insn::Found { cursor_id, target_pc, record_reg, num_regs, } => (cursor_id, target_pc, record_reg, num_regs), _ => unreachable!("unexpected Insn {:?}", insn), }; let not = matches!(insn, Insn::NotFound { .. }); let record_source = if *num_regs == 0 { RecordSource::Packed { record_reg: *record_reg, } } else { RecordSource::Unpacked { start_reg: *record_reg, num_regs: *num_regs, } }; let seek_result = match seek_internal( program, state, pager, mv_store, record_source, *cursor_id, true, SeekOp::GE { eq_only: true }, ) { Ok(SeekInternalResult::Found) => SeekResult::Found, Ok(SeekInternalResult::NotFound) => SeekResult::NotFound, Ok(SeekInternalResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(e) => return Err(e), }; let found = matches!(seek_result, SeekResult::Found); let do_jump = (!found && not) || (found && !not); if do_jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_affinity( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Affinity { start_reg, count, affinities, }, insn ); if affinities.len() != count.get() { return Err(LimboError::InternalError( "Affinity: the length of affinities does not match the count".into(), )); } for (i, affinity_char) in affinities.chars().enumerate().take(count.get()) { let reg_index = *start_reg + i; let affinity = Affinity::from_char(affinity_char); apply_affinity_char(&mut state.registers[reg_index], affinity); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_count( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( Count { cursor_id, target_reg, exact, }, insn ); let count = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Count"); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.count()) }; state.registers[*target_reg] = Register::Value(Value::Integer(count as i64)); // For optimized COUNT(*) queries, the count represents rows that would be read // SQLite tracks this differently (as pages read), but for consistency we track as rows if *exact { state.metrics.rows_read = state.metrics.rows_read.saturating_add(count as u64); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug)] pub enum OpIntegrityCheckState { Start, Checking { errors: Vec, current_root_idx: usize, state: IntegrityCheckState, }, } pub fn op_integrity_check( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( IntegrityCk { max_errors, roots, message_register, }, insn ); match &mut state.op_integrity_check_state { OpIntegrityCheckState::Start => { let (freelist_trunk_page, db_size) = return_if_io!(with_header(pager, mv_store, program, |header| ( header.freelist_trunk_page.get(), header.database_size.get() ))); let mut errors = Vec::new(); let mut integrity_check_state = IntegrityCheckState::new(db_size as usize); let mut current_root_idx = 0; // check freelist pages first, if there are any for database if freelist_trunk_page > 0 { let expected_freelist_count = return_if_io!(with_header(pager, mv_store, program, |header| header .freelist_pages .get())); integrity_check_state.set_expected_freelist_count(expected_freelist_count as usize); integrity_check_state.start( freelist_trunk_page as i64, PageCategory::FreeListTrunk, &mut errors, ); } else { integrity_check_state.start(roots[0], PageCategory::Normal, &mut errors); current_root_idx += 1; } state.op_integrity_check_state = OpIntegrityCheckState::Checking { errors, state: integrity_check_state, current_root_idx, }; } OpIntegrityCheckState::Checking { errors, current_root_idx, state: integrity_check_state, } => { return_if_io!(integrity_check(integrity_check_state, errors, pager)); if *current_root_idx < roots.len() { integrity_check_state.start(roots[*current_root_idx], PageCategory::Normal, errors); *current_root_idx += 1; return Ok(InsnFunctionStepResult::Step); } else { if integrity_check_state.freelist_count.actual_count != integrity_check_state.freelist_count.expected_count { errors.push(IntegrityCheckError::FreelistCountMismatch { actual_count: integrity_check_state.freelist_count.actual_count, expected_count: integrity_check_state.freelist_count.expected_count, }); } #[cfg(not(feature = "omit_autovacuum"))] { let auto_vacuum_mode = pager.get_auto_vacuum_mode(); if !matches!( auto_vacuum_mode, crate::storage::pager::AutoVacuumMode::None ) { tracing::debug!("Integrity check: auto-vacuum mode detected ({:?}). Scanning for pointer-map pages.", auto_vacuum_mode); let page_size = pager.get_page_size_unchecked().get() as usize; for page_number in 2..=integrity_check_state.db_size { if crate::storage::pager::ptrmap::is_ptrmap_page( page_number as u32, page_size, ) { tracing::debug!("Integrity check: Found and marking pointer-map page as visited: page_id={}", page_number); integrity_check_state.start( page_number as i64, PageCategory::PointerMap, errors, ); } } } } for page_number in 2..=integrity_check_state.db_size { if !integrity_check_state .page_reference .contains_key(&(page_number as i64)) { errors.push(IntegrityCheckError::PageNeverUsed { page_id: page_number as i64, }); } } let message = if errors.is_empty() { "ok".to_string() } else { errors .iter() .map(|e| e.to_string()) .collect::>() .join("\n") }; state.registers[*message_register] = Register::Value(Value::build_text(message)); state.op_integrity_check_state = OpIntegrityCheckState::Start; state.pc += 1; } } } Ok(InsnFunctionStepResult::Step) } pub fn op_cast( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, _mv_store: Option<&Arc>, ) -> Result { load_insn!(Cast { reg, affinity }, insn); let value = state.registers[*reg].get_value().clone(); let result = match affinity { Affinity::Blob => value.exec_cast("BLOB"), Affinity::Text => value.exec_cast("TEXT"), Affinity::Numeric => value.exec_cast("NUMERIC"), Affinity::Integer => value.exec_cast("INTEGER"), Affinity::Real => value.exec_cast("REAL"), }; state.registers[*reg] = Register::Value(result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_rename_table( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(RenameTable { from, to }, insn); let normalized_from = normalize_ident(from.as_str()); let normalized_to = normalize_ident(to.as_str()); let conn = program.connection.clone(); conn.with_schema_mut(|schema| { if let Some(mut indexes) = schema.indexes.remove(&normalized_from) { indexes.iter_mut().for_each(|index| { let index = Arc::make_mut(index); index.table_name = normalized_to.to_owned(); }); schema.indexes.insert(normalized_to.to_owned(), indexes); }; let mut table = schema .tables .remove(&normalized_from) .expect("table being renamed should be in schema"); match Arc::make_mut(&mut table) { Table::BTree(btree) => { let btree = Arc::make_mut(btree); // update this table's own foreign keys for fk_arc in &mut btree.foreign_keys { let fk = Arc::make_mut(fk_arc); if normalize_ident(&fk.parent_table) == normalized_from { fk.parent_table = normalized_to.clone(); } } btree.name = normalized_to.to_owned(); } Table::Virtual(vtab) => { Arc::make_mut(vtab).name = normalized_to.clone(); } _ => panic!("only btree and virtual tables can be renamed"), } schema.tables.insert(normalized_to.to_owned(), table); for (tname, t_arc) in schema.tables.iter_mut() { // skip the table we just renamed if normalize_ident(tname) == normalized_to { continue; } if let Table::BTree(ref mut child_btree_arc) = Arc::make_mut(t_arc) { let child_btree = Arc::make_mut(child_btree_arc); for fk_arc in &mut child_btree.foreign_keys { if normalize_ident(&fk_arc.parent_table) == normalized_from { let fk = Arc::make_mut(fk_arc); fk.parent_table = normalized_to.clone(); } } } } }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_column( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( DropColumn { table, column_index }, insn ); let conn = program.connection.clone(); let normalized_table_name = normalize_ident(table.as_str()); let column_name = { let schema = conn.schema.read(); let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); table .get_column_at(*column_index) .expect("column being ALTERed should be in schema") .name .as_ref() .expect("column being ALTERed should be named") .clone() }; conn.with_schema_mut(|schema| { let table = schema .tables .get_mut(&normalized_table_name) .expect("table being renamed should be in schema"); let table = Arc::get_mut(table).expect("this should be the only strong reference"); let Table::BTree(btree) = table else { panic!("only btree tables can be renamed"); }; let btree = Arc::make_mut(btree); btree.columns.remove(*column_index) }); { let schema = conn.schema.read(); if let Some(indexes) = schema.indexes.get(&normalized_table_name) { for index in indexes { if index .columns .iter() .any(|column| column.pos_in_table == *column_index) { return Err(LimboError::ParseError(format!( "cannot drop column \"{column_name}\": indexed" ))); } } } } // Update index.pos_in_table for all indexes. // For example, if the dropped column had index 2, then anything that was indexed on column 3 or higher should be decremented by 1. conn.with_schema_mut(|schema| { if let Some(indexes) = schema.indexes.get_mut(&normalized_table_name) { for index in indexes { let index = Arc::get_mut(index).expect("this should be the only strong reference"); for index_column in index.columns.iter_mut() { if index_column.pos_in_table > *column_index { index_column.pos_in_table -= 1; } } } } }); { let schema = conn.schema.read(); for (view_name, view) in schema.views.iter() { let view_select_sql = format!("SELECT * FROM {view_name}"); let _ = conn.prepare(view_select_sql.as_str()).map_err(|e| { LimboError::ParseError(format!( "cannot drop column \"{}\": referenced in VIEW {view_name}: {}", column_name, view.sql, )) })?; } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_add_column( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(AddColumn { table, column }, insn); let conn = program.connection.clone(); conn.with_schema_mut(|schema| { let table = schema .tables .get_mut(table) .expect("table being renamed should be in schema"); let table = Arc::make_mut(table); let Table::BTree(btree) = table else { panic!("only btree tables can be renamed"); }; let btree = Arc::make_mut(btree); btree.columns.push(column.clone()) }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_alter_column( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( AlterColumn { table: table_name, column_index, definition, rename, }, insn ); let conn = program.connection.clone(); let normalized_table_name = normalize_ident(table_name.as_str()); let old_column_name = { let schema = conn.schema.read(); let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); table .get_column_at(*column_index) .expect("column being ALTERed should be in schema") .name .as_ref() .expect("column being ALTERed should be named") .clone() }; let new_column = crate::schema::Column::from(definition); let new_name = definition.col_name.as_str().to_owned(); conn.with_schema_mut(|schema| { let table_arc = schema .tables .get_mut(&normalized_table_name) .expect("table being ALTERed should be in schema"); let table = Arc::make_mut(table_arc); let Table::BTree(ref mut btree_arc) = table else { panic!("only btree tables can be altered"); }; let btree = Arc::make_mut(btree_arc); let col = btree .columns .get_mut(*column_index) .expect("column being ALTERed should be in schema"); // Update indexes on THIS table that name the old column (you already had this) if let Some(idxs) = schema.indexes.get_mut(&normalized_table_name) { for idx in idxs { let idx = Arc::make_mut(idx); for ic in &mut idx.columns { if ic.name.eq_ignore_ascii_case( col.name.as_ref().expect("btree column should be named"), ) { ic.name = new_name.clone(); } } } } if *rename { col.name = Some(new_name.clone()); } else { *col = new_column.clone(); } // Keep primary_key_columns consistent (names may change on rename) for (pk_name, _ord) in &mut btree.primary_key_columns { if pk_name.eq_ignore_ascii_case(&old_column_name) { *pk_name = new_name.clone(); } } // Maintain rowid-alias bit after change/rename (INTEGER PRIMARY KEY) if !*rename { // recompute alias from `new_column` btree.columns[*column_index].set_rowid_alias(new_column.is_rowid_alias()); } // Update this table’s OWN foreign keys for fk_arc in &mut btree.foreign_keys { let fk = Arc::make_mut(fk_arc); // child side: rename child column if it matches for cc in &mut fk.child_columns { if cc.eq_ignore_ascii_case(&old_column_name) { *cc = new_name.clone(); } } // parent side: if self-referencing, rename parent column too if normalize_ident(&fk.parent_table) == normalized_table_name { for pc in &mut fk.parent_columns { if pc.eq_ignore_ascii_case(&old_column_name) { *pc = new_name.clone(); } } } } // fix OTHER tables that reference this table as parent for (tname, t_arc) in schema.tables.iter_mut() { if normalize_ident(tname) == normalized_table_name { continue; } if let Table::BTree(ref mut child_btree_arc) = Arc::make_mut(t_arc) { let child_btree = Arc::make_mut(child_btree_arc); for fk_arc in &mut child_btree.foreign_keys { if normalize_ident(&fk_arc.parent_table) != normalized_table_name { continue; } let fk = Arc::make_mut(fk_arc); for pc in &mut fk.parent_columns { if pc.eq_ignore_ascii_case(&old_column_name) { *pc = new_name.clone(); } } } } } }); let schema = conn.schema.read(); if *rename { let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); let column = table .get_column_at(*column_index) .expect("column being ALTERed should be in schema"); for (view_name, view) in schema.views.iter() { let view_select_sql = format!("SELECT * FROM {view_name}"); // FIXME: this should rewrite the view to reference the new column name let _ = conn.prepare(view_select_sql.as_str()).map_err(|e| { LimboError::ParseError(format!( "cannot rename column \"{}\": referenced in VIEW {view_name}: {}", old_column_name, view.sql, )) })?; } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_if_neg( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(IfNeg { reg, target_pc }, insn); match &state.registers[*reg] { Register::Value(Value::Integer(i)) if *i < 0 => { state.pc = target_pc.as_offset_int(); } Register::Value(Value::Float(f)) if *f < 0.0 => { state.pc = target_pc.as_offset_int(); } Register::Value(Value::Null) => { state.pc += 1; } _ => { state.pc += 1; } } Ok(InsnFunctionStepResult::Step) } fn handle_text_sum(acc: &mut Value, sum_state: &mut SumAggState, parsed_number: ParsedNumber) { match parsed_number { ParsedNumber::Integer(i) => match acc { Value::Null => { *acc = Value::Integer(i); } Value::Integer(acc_i) => match acc_i.checked_add(i) { Some(sum) => *acc = Value::Integer(sum), None => { let acc_f = *acc_i as f64; *acc = Value::Float(acc_f); sum_state.approx = true; sum_state.ovrfl = true; apply_kbn_step_int(acc, i, sum_state); } }, Value::Float(_) => { apply_kbn_step_int(acc, i, sum_state); } _ => unreachable!(), }, ParsedNumber::Float(f) => { if !sum_state.approx { if let Value::Integer(current_sum) = *acc { *acc = Value::Float(current_sum as f64); } else if matches!(*acc, Value::Null) { *acc = Value::Float(0.0); } sum_state.approx = true; } apply_kbn_step(acc, f, sum_state); } ParsedNumber::None => { if !sum_state.approx { if let Value::Integer(current_sum) = *acc { *acc = Value::Float(current_sum as f64); } else if matches!(*acc, Value::Null) { *acc = Value::Float(0.0); } sum_state.approx = true; } } } } pub fn op_fk_counter( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!( FkCounter { increment_value, deferred, }, insn ); if !*deferred { state .fk_immediate_violations_during_stmt .fetch_add(*increment_value, Ordering::AcqRel); } else { // Transaction-level counter: add/subtract for deferred FKs. program .connection .fk_deferred_violations .fetch_add(*increment_value, Ordering::AcqRel); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_fk_if_zero( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, _mv_store: Option<&Arc>, ) -> Result { load_insn!( FkIfZero { deferred, target_pc, }, insn ); let fk_enabled = program.connection.foreign_keys_enabled(); // Jump if any: // Foreign keys are disabled globally // p1 is true AND deferred constraint counter is zero // p1 is false AND deferred constraint counter is non-zero if !fk_enabled { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } let v = if *deferred { program.connection.get_deferred_foreign_key_violations() } else { state .fk_immediate_violations_during_stmt .load(Ordering::Acquire) }; state.pc = if v == 0 { target_pc.as_offset_int() } else { state.pc + 1 }; Ok(InsnFunctionStepResult::Step) } mod cmath { extern "C" { pub fn exp(x: f64) -> f64; pub fn log(x: f64) -> f64; pub fn log10(x: f64) -> f64; pub fn log2(x: f64) -> f64; pub fn pow(x: f64, y: f64) -> f64; pub fn sin(x: f64) -> f64; pub fn sinh(x: f64) -> f64; pub fn asin(x: f64) -> f64; pub fn asinh(x: f64) -> f64; pub fn cos(x: f64) -> f64; pub fn cosh(x: f64) -> f64; pub fn acos(x: f64) -> f64; pub fn acosh(x: f64) -> f64; pub fn tan(x: f64) -> f64; pub fn tanh(x: f64) -> f64; pub fn atan(x: f64) -> f64; pub fn atanh(x: f64) -> f64; pub fn atan2(x: f64, y: f64) -> f64; } } enum TrimType { All, Left, Right, } impl TrimType { fn trim<'a>(&self, text: &'a str, pattern: &[char]) -> &'a str { match self { TrimType::All => text.trim_matches(pattern), TrimType::Right => text.trim_end_matches(pattern), TrimType::Left => text.trim_start_matches(pattern), } } } impl Value { pub fn exec_lower(&self) -> Option { self.cast_text() .map(|s| Value::build_text(s.to_ascii_lowercase())) } pub fn exec_length(&self) -> Self { match self { Value::Text(t) => { let s = t.as_str(); let len_before_null = s.find('\0').map_or_else( || s.chars().count(), |null_pos| s[..null_pos].chars().count(), ); Value::Integer(len_before_null as i64) } Value::Integer(_) | Value::Float(_) => { // For numbers, SQLite returns the length of the string representation Value::Integer(self.to_string().chars().count() as i64) } Value::Blob(blob) => Value::Integer(blob.len() as i64), _ => self.to_owned(), } } pub fn exec_octet_length(&self) -> Self { match self { Value::Text(_) | Value::Integer(_) | Value::Float(_) => { Value::Integer(self.to_string().into_bytes().len() as i64) } Value::Blob(blob) => Value::Integer(blob.len() as i64), _ => self.to_owned(), } } pub fn exec_upper(&self) -> Option { self.cast_text() .map(|s| Value::build_text(s.to_ascii_uppercase())) } pub fn exec_sign(&self) -> Option { let v = Numeric::from_value_strict(self).try_into_f64()?; Some(Value::Integer(if v > 0.0 { 1 } else if v < 0.0 { -1 } else { 0 })) } /// Generates the Soundex code for a given word pub fn exec_soundex(&self) -> Value { let s = match self { Value::Null => return Value::build_text("?000"), Value::Text(s) => { // return ?000 if non ASCII alphabet character is found if !s.as_str().chars().all(|c| c.is_ascii_alphabetic()) { return Value::build_text("?000"); } s.clone() } _ => return Value::build_text("?000"), // For unsupported types, return NULL }; // Remove numbers and spaces let word: String = s .as_str() .chars() .filter(|c| !c.is_ascii_digit()) .collect::() .replace(" ", ""); if word.is_empty() { return Value::build_text("0000"); } let soundex_code = |c| match c { 'b' | 'f' | 'p' | 'v' => Some('1'), 'c' | 'g' | 'j' | 'k' | 'q' | 's' | 'x' | 'z' => Some('2'), 'd' | 't' => Some('3'), 'l' => Some('4'), 'm' | 'n' => Some('5'), 'r' => Some('6'), _ => None, }; // Convert the word to lowercase for consistent lookups let word = word.to_lowercase(); let first_letter = word.chars().next().unwrap(); // Remove all occurrences of 'h' and 'w' except the first letter let code: String = word .chars() .skip(1) .filter(|&ch| ch != 'h' && ch != 'w') .fold(first_letter.to_string(), |mut acc, ch| { acc.push(ch); acc }); // Replace consonants with digits based on Soundex mapping let tmp: String = code .chars() .map(|ch| match soundex_code(ch) { Some(code) => code.to_string(), None => ch.to_string(), }) .collect(); // Remove adjacent same digits let tmp = tmp.chars().fold(String::new(), |mut acc, ch| { if !acc.ends_with(ch) { acc.push(ch); } acc }); // Remove all occurrences of a, e, i, o, u, y except the first letter let mut result = tmp .chars() .enumerate() .filter(|(i, ch)| *i == 0 || !matches!(ch, 'a' | 'e' | 'i' | 'o' | 'u' | 'y')) .map(|(_, ch)| ch) .collect::(); // If the first symbol is a digit, replace it with the saved first letter if let Some(first_digit) = result.chars().next() { if first_digit.is_ascii_digit() { result.replace_range(0..1, &first_letter.to_string()); } } // Append zeros if the result contains less than 4 characters while result.len() < 4 { result.push('0'); } // Retain the first 4 characters and convert to uppercase result.truncate(4); Value::build_text(result.to_uppercase()) } pub fn exec_abs(&self) -> Result { Ok(match self { Value::Null => Value::Null, Value::Integer(v) => { Value::Integer(v.checked_abs().ok_or(LimboError::IntegerOverflow)?) } Value::Float(non_nan) => Value::Float(non_nan.abs()), _ => { let s = match self { Value::Text(text) => text.to_string(), Value::Blob(blob) => String::from_utf8_lossy(blob).to_string(), _ => unreachable!(), }; crate::numeric::str_to_f64(s) .map(|v| Value::Float(f64::from(v).abs())) .unwrap_or(Value::Float(0.0)) } }) } pub fn exec_random(generate_random_number: F) -> Self where F: Fn() -> i64, { Value::Integer(generate_random_number()) } pub fn exec_randomblob(&self, fill_bytes: F) -> Value where F: Fn(&mut [u8]), { let length = match self { Value::Integer(i) => *i, Value::Float(f) => *f as i64, Value::Text(t) => t.as_str().parse().unwrap_or(1), _ => 1, } .max(1) as usize; let mut blob: Vec = vec![0; length]; fill_bytes(&mut blob); Value::Blob(blob) } pub fn exec_quote(&self) -> Self { match self { Value::Null => Value::build_text("NULL"), Value::Integer(_) | Value::Float(_) => self.to_owned(), Value::Blob(_) => todo!(), Value::Text(s) => { let mut quoted = String::with_capacity(s.as_str().len() + 2); quoted.push('\''); for c in s.as_str().chars() { if c == '\0' { break; } else if c == '\'' { quoted.push('\''); quoted.push(c); } else { quoted.push(c); } } quoted.push('\''); Value::build_text(quoted) } } } pub fn exec_nullif(&self, second_value: &Self) -> Self { if self != second_value { self.clone() } else { Value::Null } } pub fn exec_substring( value: &Value, start_value: &Value, length_value: Option<&Value>, ) -> Value { /// Function is stabilized but not released for version 1.88 \ /// https://doc.rust-lang.org/src/core/str/mod.rs.html#453 const fn ceil_char_boundary(s: &str, index: usize) -> usize { const fn is_utf8_char_boundary(c: u8) -> bool { // This is bit magic equivalent to: b < 128 || b >= 192 (c as i8) >= -0x40 } if index >= s.len() { s.len() } else { let mut i = index; while i < s.len() { if is_utf8_char_boundary(s.as_bytes()[i]) { break; } i += 1; } // The character boundary will be within four bytes of the index debug_assert!(i <= index + 3); i } } fn calculate_postions( start: i64, bytes_len: usize, length_value: Option<&Value>, ) -> (usize, usize) { let bytes_len = bytes_len as i64; // The left-most character of X is number 1. // If Y is negative then the first character of the substring is found by counting from the right rather than the left. let first_position = if start < 0 { bytes_len.saturating_sub((start).abs()) } else { start - 1 }; // If Z is negative then the abs(Z) characters preceding the Y-th character are returned. let last_position = match length_value { Some(Value::Integer(length)) => first_position + *length, _ => bytes_len, }; let (start, end) = if first_position <= last_position { (first_position, last_position) } else { (last_position, first_position) }; ( start.clamp(-0, bytes_len) as usize, end.clamp(0, bytes_len) as usize, ) } let start_value = start_value.exec_cast("INT"); let length_value = length_value.map(|value| value.exec_cast("INT")); match (value, start_value) { (Value::Blob(b), Value::Integer(start)) => { let (start, end) = calculate_postions(start, b.len(), length_value.as_ref()); Value::from_blob(b[start..end].to_vec()) } (value, Value::Integer(start)) => { if let Some(text) = value.cast_text() { let (mut start, mut end) = calculate_postions(start, text.len(), length_value.as_ref()); // https://github.com/sqlite/sqlite/blob/a248d84f/src/func.c#L417 let s = text.as_str(); let mut start_byte_idx = 0; end -= start; while start > 0 { start_byte_idx = ceil_char_boundary(s, start_byte_idx + 1); start -= 1; } let mut end_byte_idx = start_byte_idx; while end > 0 { end_byte_idx = ceil_char_boundary(s, end_byte_idx + 1); end -= 1; } Value::build_text(&s[start_byte_idx..end_byte_idx]) } else { Value::Null } } _ => Value::Null, } } pub fn exec_instr(&self, pattern: &Value) -> Value { if self == &Value::Null || pattern == &Value::Null { return Value::Null; } if let (Value::Blob(reg), Value::Blob(pattern)) = (self, pattern) { let result = reg .windows(pattern.len()) .position(|window| window == *pattern) .map_or(0, |i| i + 1); return Value::Integer(result as i64); } let reg_str; let reg = match self { Value::Text(s) => s.as_str(), _ => { reg_str = self.to_string(); reg_str.as_str() } }; let pattern_str; let pattern = match pattern { Value::Text(s) => s.as_str(), _ => { pattern_str = pattern.to_string(); pattern_str.as_str() } }; match reg.find(pattern) { Some(position) => Value::Integer(position as i64 + 1), None => Value::Integer(0), } } pub fn exec_typeof(&self) -> Value { match self { Value::Null => Value::build_text("null"), Value::Integer(_) => Value::build_text("integer"), Value::Float(_) => Value::build_text("real"), Value::Text(_) => Value::build_text("text"), Value::Blob(_) => Value::build_text("blob"), } } pub fn exec_hex(&self) -> Value { match self { Value::Text(_) | Value::Integer(_) | Value::Float(_) => { let text = self.to_string(); Value::build_text(hex::encode_upper(text)) } Value::Blob(blob_bytes) => Value::build_text(hex::encode_upper(blob_bytes)), Value::Null => Value::build_text(""), } } pub fn exec_unhex(&self, ignored_chars: Option<&Value>) -> Value { match self { Value::Null => Value::Null, _ => match ignored_chars { None => match self .cast_text() .map(|s| hex::decode(&s[0..s.find('\0').unwrap_or(s.len())])) { Some(Ok(bytes)) => Value::Blob(bytes), _ => Value::Null, }, Some(ignore) => match ignore { Value::Text(_) => { let pat = ignore.to_string(); let trimmed = self .to_string() .trim_start_matches(|x| pat.contains(x)) .trim_end_matches(|x| pat.contains(x)) .to_string(); match hex::decode(trimmed) { Ok(bytes) => Value::Blob(bytes), _ => Value::Null, } } _ => Value::Null, }, }, } } pub fn exec_unicode(&self) -> Value { match self { Value::Text(_) | Value::Integer(_) | Value::Float(_) | Value::Blob(_) => { let text = self.to_string(); if let Some(first_char) = text.chars().next() { Value::Integer(first_char as u32 as i64) } else { Value::Null } } _ => Value::Null, } } pub fn exec_round(&self, precision: Option<&Value>) -> Value { let Some(f) = Numeric::from(self).try_into_f64() else { return Value::Null; }; let precision = match precision.map(|v| Numeric::from(v).try_into_f64()) { None => 0.0, Some(Some(v)) => v, Some(None) => return Value::Null, }; if !(-4503599627370496.0..=4503599627370496.0).contains(&f) { return Value::Float(f); } let precision = if precision < 1.0 { 0.0 } else { precision }; let precision = precision.clamp(0.0, 30.0) as usize; if precision == 0 { return Value::Float(((f + if f < 0.0 { -0.5 } else { 0.5 }) as i64) as f64); } let f: f64 = crate::numeric::str_to_f64(format!("{f:.precision$}")) .unwrap() .into(); Value::Float(f) } fn _exec_trim(&self, pattern: Option<&Value>, trim_type: TrimType) -> Value { match (self, pattern) { (Value::Text(_) | Value::Integer(_) | Value::Float(_), Some(pattern)) => { let pattern_chars: Vec = pattern.to_string().chars().collect(); let text = self.to_string(); Value::build_text(trim_type.trim(&text, &pattern_chars)) } (Value::Text(t), None) => Value::build_text(trim_type.trim(t.as_str(), &[' '])), (reg, _) => reg.to_owned(), } } // Implements TRIM pattern matching. pub fn exec_trim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::All) } // Implements RTRIM pattern matching. pub fn exec_rtrim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::Right) } // Implements LTRIM pattern matching. pub fn exec_ltrim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::Left) } pub fn exec_zeroblob(&self) -> Value { let length: i64 = match self { Value::Integer(i) => *i, Value::Float(f) => *f as i64, Value::Text(s) => s.as_str().parse().unwrap_or(0), _ => 0, }; Value::Blob(vec![0; length.max(0) as usize]) } // exec_if returns whether you should jump pub fn exec_if(&self, jump_if_null: bool, not: bool) -> bool { Numeric::from(self) .try_into_bool() .map(|jump| if not { !jump } else { jump }) .unwrap_or(jump_if_null) } pub fn exec_cast(&self, datatype: &str) -> Value { if matches!(self, Value::Null) { return Value::Null; } match affinity(datatype) { // NONE Casting a value to a type-name with no affinity causes the value to be converted into a BLOB. Casting to a BLOB consists of first casting the value to TEXT in the encoding of the database connection, then interpreting the resulting byte sequence as a BLOB instead of as TEXT. // Historically called NONE, but it's the same as BLOB Affinity::Blob => { // Convert to TEXT first, then interpret as BLOB // TODO: handle encoding let text = self.to_string(); Value::Blob(text.into_bytes()) } // TEXT To cast a BLOB value to TEXT, the sequence of bytes that make up the BLOB is interpreted as text encoded using the database encoding. // Casting an INTEGER or REAL value into TEXT renders the value as if via sqlite3_snprintf() except that the resulting TEXT uses the encoding of the database connection. Affinity::Text => { // Convert everything to text representation // TODO: handle encoding and whatever sqlite3_snprintf does Value::build_text(self.to_string()) } Affinity::Real => match self { Value::Blob(b) => { let text = String::from_utf8_lossy(b); Value::Float( crate::numeric::str_to_f64(&text) .map(f64::from) .unwrap_or(0.0), ) } Value::Text(t) => { Value::Float(crate::numeric::str_to_f64(t).map(f64::from).unwrap_or(0.0)) } Value::Integer(i) => Value::Float(*i as f64), Value::Float(f) => Value::Float(*f), _ => Value::Float(0.0), }, Affinity::Integer => match self { Value::Blob(b) => { // Convert BLOB to TEXT first let text = String::from_utf8_lossy(b); Value::Integer(crate::numeric::str_to_i64(&text).unwrap_or(0)) } Value::Text(t) => Value::Integer(crate::numeric::str_to_i64(t).unwrap_or(0)), Value::Integer(i) => Value::Integer(*i), // A cast of a REAL value into an INTEGER results in the integer between the REAL value and zero // that is closest to the REAL value. If a REAL is greater than the greatest possible signed integer (+9223372036854775807) // then the result is the greatest possible signed integer and if the REAL is less than the least possible signed integer (-9223372036854775808) // then the result is the least possible signed integer. Value::Float(f) => { let i = f.trunc() as i128; if i > i64::MAX as i128 { Value::Integer(i64::MAX) } else if i < i64::MIN as i128 { Value::Integer(i64::MIN) } else { Value::Integer(i as i64) } } _ => Value::Integer(0), }, Affinity::Numeric => match self { Value::Null => Value::Null, Value::Integer(v) => Value::Integer(*v), Value::Float(v) => Self::Float(*v), _ => { let s = match self { Value::Text(text) => text.to_string(), Value::Blob(blob) => String::from_utf8_lossy(blob.as_slice()).to_string(), _ => unreachable!(), }; match crate::numeric::str_to_f64(&s) { Some(parsed) => { let Some(int) = crate::numeric::str_to_i64(&s) else { return Value::Integer(0); }; if f64::from(parsed) == int as f64 { return Value::Integer(int); } Value::Float(parsed.into()) } None => Value::Integer(0), } } }, } } pub fn exec_replace(source: &Value, pattern: &Value, replacement: &Value) -> Value { // The replace(X,Y,Z) function returns a string formed by substituting string Z for every occurrence of // string Y in string X. The BINARY collating sequence is used for comparisons. If Y is an empty string // then return X unchanged. If Z is not initially a string, it is cast to a UTF-8 string prior to processing. // If any of the arguments is NULL, the result is NULL. if matches!(source, Value::Null) || matches!(pattern, Value::Null) || matches!(replacement, Value::Null) { return Value::Null; } let source = source.exec_cast("TEXT"); let pattern = pattern.exec_cast("TEXT"); let replacement = replacement.exec_cast("TEXT"); // If any of the casts failed, panic as text casting is not expected to fail. match (&source, &pattern, &replacement) { (Value::Text(source), Value::Text(pattern), Value::Text(replacement)) => { if pattern.as_str().is_empty() { return Value::Text(source.clone()); } let result = source .as_str() .replace(pattern.as_str(), replacement.as_str()); Value::build_text(result) } _ => unreachable!("text cast should never fail"), } } fn exec_math_unary(&self, function: &MathFunc) -> Value { let v = Numeric::from_value_strict(self); // In case of some functions and integer input, return the input as is if let Numeric::Integer(i) = v { if matches! { function, MathFunc::Ceil | MathFunc::Ceiling | MathFunc::Floor | MathFunc::Trunc } { return Value::Integer(i); } } let Some(f) = v.try_into_f64() else { return Value::Null; }; if matches! { function, MathFunc::Ln | MathFunc::Log10 | MathFunc::Log2 } && f <= 0.0 { return Value::Null; } let result = match function { MathFunc::Acos => unsafe { cmath::acos(f) }, MathFunc::Acosh => unsafe { cmath::acosh(f) }, MathFunc::Asin => unsafe { cmath::asin(f) }, MathFunc::Asinh => unsafe { cmath::asinh(f) }, MathFunc::Atan => unsafe { cmath::atan(f) }, MathFunc::Atanh => unsafe { cmath::atanh(f) }, MathFunc::Ceil | MathFunc::Ceiling => libm::ceil(f), MathFunc::Cos => unsafe { cmath::cos(f) }, MathFunc::Cosh => unsafe { cmath::cosh(f) }, MathFunc::Degrees => f.to_degrees(), MathFunc::Exp => unsafe { cmath::exp(f) }, MathFunc::Floor => libm::floor(f), MathFunc::Ln => unsafe { cmath::log(f) }, MathFunc::Log10 => unsafe { cmath::log10(f) }, MathFunc::Log2 => unsafe { cmath::log2(f) }, MathFunc::Radians => f.to_radians(), MathFunc::Sin => unsafe { cmath::sin(f) }, MathFunc::Sinh => unsafe { cmath::sinh(f) }, MathFunc::Sqrt => libm::sqrt(f), MathFunc::Tan => unsafe { cmath::tan(f) }, MathFunc::Tanh => unsafe { cmath::tanh(f) }, MathFunc::Trunc => libm::trunc(f), _ => unreachable!("Unexpected mathematical unary function {:?}", function), }; if result.is_nan() { Value::Null } else { Value::Float(result) } } fn exec_math_binary(&self, rhs: &Value, function: &MathFunc) -> Value { let Some(lhs) = Numeric::from_value_strict(self).try_into_f64() else { return Value::Null; }; let Some(rhs) = Numeric::from_value_strict(rhs).try_into_f64() else { return Value::Null; }; let result = match function { MathFunc::Atan2 => unsafe { cmath::atan2(lhs, rhs) }, MathFunc::Mod => libm::fmod(lhs, rhs), MathFunc::Pow | MathFunc::Power => unsafe { cmath::pow(lhs, rhs) }, _ => unreachable!("Unexpected mathematical binary function {:?}", function), }; if result.is_nan() { Value::Null } else { Value::Float(result) } } fn exec_math_log(&self, base: Option<&Value>) -> Value { let Some(f) = Numeric::from_value_strict(self).try_into_f64() else { return Value::Null; }; let base = match base.map(|value| Numeric::from_value_strict(value).try_into_f64()) { Some(Some(f)) => f, Some(None) => return Value::Null, None => 10.0, }; if f <= 0.0 || base <= 0.0 || base == 1.0 { return Value::Null; } if base == 2.0 { return Value::Float(libm::log2(f)); } else if base == 10.0 { return Value::Float(libm::log10(f)); }; let log_x = libm::log(f); let log_base = libm::log(base); if log_base <= 0.0 { return Value::Null; } let result = log_x / log_base; Value::Float(result) } pub fn exec_add(&self, rhs: &Value) -> Value { (Numeric::from(self) + Numeric::from(rhs)).into() } pub fn exec_subtract(&self, rhs: &Value) -> Value { (Numeric::from(self) - Numeric::from(rhs)).into() } pub fn exec_multiply(&self, rhs: &Value) -> Value { (Numeric::from(self) * Numeric::from(rhs)).into() } pub fn exec_divide(&self, rhs: &Value) -> Value { (Numeric::from(self) / Numeric::from(rhs)).into() } pub fn exec_bit_and(&self, rhs: &Value) -> Value { (NullableInteger::from(self) & NullableInteger::from(rhs)).into() } pub fn exec_bit_or(&self, rhs: &Value) -> Value { (NullableInteger::from(self) | NullableInteger::from(rhs)).into() } pub fn exec_remainder(&self, rhs: &Value) -> Value { let convert_to_float = matches!(Numeric::from(self), Numeric::Float(_)) || matches!(Numeric::from(rhs), Numeric::Float(_)); match NullableInteger::from(self) % NullableInteger::from(rhs) { NullableInteger::Null => Value::Null, NullableInteger::Integer(v) => { if convert_to_float { Value::Float(v as f64) } else { Value::Integer(v) } } } } pub fn exec_bit_not(&self) -> Value { (!NullableInteger::from(self)).into() } pub fn exec_shift_left(&self, rhs: &Value) -> Value { (NullableInteger::from(self) << NullableInteger::from(rhs)).into() } pub fn exec_shift_right(&self, rhs: &Value) -> Value { (NullableInteger::from(self) >> NullableInteger::from(rhs)).into() } pub fn exec_boolean_not(&self) -> Value { match Numeric::from(self).try_into_bool() { None => Value::Null, Some(v) => Value::Integer(!v as i64), } } pub fn exec_concat(&self, rhs: &Value) -> Value { if let (Value::Blob(lhs), Value::Blob(rhs)) = (self, rhs) { return Value::build_text(String::from_utf8_lossy( &[lhs.as_slice(), rhs.as_slice()].concat(), )); } let Some(lhs) = self.cast_text() else { return Value::Null; }; let Some(rhs) = rhs.cast_text() else { return Value::Null; }; Value::build_text(lhs + &rhs) } pub fn exec_and(&self, rhs: &Value) -> Value { match ( Numeric::from(self).try_into_bool(), Numeric::from(rhs).try_into_bool(), ) { (Some(false), _) | (_, Some(false)) => Value::Integer(0), (None, _) | (_, None) => Value::Null, _ => Value::Integer(1), } } pub fn exec_or(&self, rhs: &Value) -> Value { match ( Numeric::from(self).try_into_bool(), Numeric::from(rhs).try_into_bool(), ) { (Some(true), _) | (_, Some(true)) => Value::Integer(1), (None, _) | (_, None) => Value::Null, _ => Value::Integer(0), } } // Implements LIKE pattern matching. Caches the constructed regex if a cache is provided pub fn exec_like( regex_cache: Option<&mut HashMap>, pattern: &str, text: &str, ) -> bool { if let Some(cache) = regex_cache { match cache.get(pattern) { Some(re) => re.is_match(text), None => { let re = construct_like_regex(pattern); let res = re.is_match(text); cache.insert(pattern.to_string(), re); res } } } else { let re = construct_like_regex(pattern); re.is_match(text) } } pub fn exec_min<'a, T: Iterator>(regs: T) -> Value { regs.min().map(|v| v.to_owned()).unwrap_or(Value::Null) } pub fn exec_max<'a, T: Iterator>(regs: T) -> Value { regs.max().map(|v| v.to_owned()).unwrap_or(Value::Null) } } fn exec_concat_strings(registers: &[Register]) -> Value { let mut result = String::new(); for reg in registers { match reg.get_value() { Value::Null => continue, Value::Blob(_) => todo!("TODO concat blob"), v => result.push_str(&format!("{v}")), } } Value::build_text(result) } fn exec_concat_ws(registers: &[Register]) -> Value { if registers.is_empty() { return Value::Null; } let separator = match registers[0].get_value() { Value::Null | Value::Blob(_) => return Value::Null, v => format!("{v}"), }; let parts = registers[1..] .iter() .filter_map(|reg| match reg.get_value() { Value::Text(_) | Value::Integer(_) | Value::Float(_) => { Some(format!("{}", reg.get_value())) } _ => None, }); let result = parts.collect::>().join(&separator); Value::build_text(result) } fn exec_char(values: &[Register]) -> Value { let result: String = values .iter() .filter_map(|x| { if let Value::Integer(i) = x.get_value() { Some(*i as u8 as char) } else { None } }) .collect(); Value::build_text(result) } fn construct_like_regex(pattern: &str) -> Regex { let mut regex_pattern = String::with_capacity(pattern.len() * 2); regex_pattern.push('^'); for c in pattern.chars() { match c { '\\' => regex_pattern.push_str("\\\\"), '%' => regex_pattern.push_str(".*"), '_' => regex_pattern.push('.'), ch => { if regex_syntax::is_meta_character(c) { regex_pattern.push('\\'); } regex_pattern.push(ch); } } } regex_pattern.push('$'); RegexBuilder::new(®ex_pattern) .case_insensitive(true) .dot_matches_new_line(true) .build() .unwrap() } fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { if let Register::Value(value) = target { if matches!(value, Value::Blob(_)) { return true; } match affinity { Affinity::Blob => return true, Affinity::Text => { if matches!(value, Value::Text(_) | Value::Null) { return true; } let text = value.to_string(); *value = Value::Text(text.into()); return true; } Affinity::Integer | Affinity::Numeric => { if matches!(value, Value::Integer(_)) { return true; } if !matches!(value, Value::Text(_) | Value::Float(_)) { return true; } if let Value::Float(fl) = *value { // For floats, try to convert to integer if it's exact // This is similar to sqlite3VdbeIntegerAffinity return try_float_to_integer_affinity(value, fl); } if let Value::Text(t) = value { let text = t.as_str().trim(); // Handle hex numbers - they shouldn't be converted if text.starts_with("0x") { return false; } // For affinity conversion, only convert strings that are entirely numeric let num = if let Ok(i) = text.parse::() { Value::Integer(i) } else if let Ok(f) = text.parse::() { Value::Float(f) } else { return false; }; match num { Value::Integer(i) => { *value = Value::Integer(i); return true; } Value::Float(fl) => { // For Numeric affinity, try to convert float to int if exact if affinity == Affinity::Numeric { return try_float_to_integer_affinity(value, fl); } else { *value = Value::Float(fl); return true; } } other => { *value = other; return true; } } } return false; } Affinity::Real => { if let Value::Integer(i) = *value { *value = Value::Float(i as f64); return true; } if let Value::Text(t) = value { let s = t.as_str(); if s.starts_with("0x") { return false; } if let Ok(num) = checked_cast_text_to_numeric(s, false) { *value = num; return true; } else { return false; } } return true; } } } true } fn try_float_to_integer_affinity(value: &mut Value, fl: f64) -> bool { // Check if the float can be exactly represented as an integer if let Ok(int_val) = cast_real_to_integer(fl) { // Additional check: ensure round-trip conversion is exact // and value is within safe bounds (similar to SQLite's checks) if (int_val as f64) == fl && int_val > i64::MIN + 1 && int_val < i64::MAX - 1 { *value = Value::Integer(int_val); return true; } } // If we can't convert to exact integer, keep as float for Numeric affinity // but return false to indicate the conversion wasn't "complete" *value = Value::Float(fl); false } // Compat for applications that test for SQLite. fn execute_sqlite_version() -> String { "3.50.4".to_string() } fn execute_turso_version(version_integer: i64) -> String { let major = version_integer / 1_000_000; let minor = (version_integer % 1_000_000) / 1_000; let release = version_integer % 1_000; format!("{major}.{minor}.{release}") } pub fn extract_int_value(value: &Value) -> i64 { match value { Value::Integer(i) => *i, Value::Float(f) => { // Use sqlite3RealToI64 equivalent if *f < -9223372036854774784.0 { i64::MIN } else if *f > 9223372036854774784.0 { i64::MAX } else { *f as i64 } } Value::Text(t) => { // Try to parse as integer, return 0 if failed t.as_str().parse::().unwrap_or(0) } Value::Blob(b) => { // Try to parse blob as string then as integer if let Ok(s) = std::str::from_utf8(b) { s.parse::().unwrap_or(0) } else { 0 } } Value::Null => 0, } } #[derive(Debug, PartialEq)] enum NumericParseResult { NotNumeric, // not a valid number PureInteger, // pure integer (entire string) HasDecimalOrExp, // has decimal point or exponent (entire string) ValidPrefixOnly, // valid prefix but not entire string } #[derive(Debug)] enum ParsedNumber { None, Integer(i64), Float(f64), } impl ParsedNumber { fn as_integer(&self) -> Option { match self { ParsedNumber::Integer(i) => Some(*i), _ => None, } } fn as_float(&self) -> Option { match self { ParsedNumber::Float(f) => Some(*f), _ => None, } } } fn try_for_float(text: &str) -> (NumericParseResult, ParsedNumber) { let bytes = text.as_bytes(); if bytes.is_empty() { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let mut pos = 0; let len = bytes.len(); while pos < len && is_space(bytes[pos]) { pos += 1; } if pos >= len { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let start_pos = pos; let mut sign = 1i64; if bytes[pos] == b'-' { sign = -1; pos += 1; } else if bytes[pos] == b'+' { pos += 1; } if pos >= len { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let mut significand = 0u64; let mut digit_count = 0; let mut decimal_adjust = 0i32; let mut has_digits = false; // Parse digits before decimal point while pos < len && bytes[pos].is_ascii_digit() { has_digits = true; let digit = (bytes[pos] - b'0') as u64; if significand <= (u64::MAX - 9) / 10 { significand = significand * 10 + digit; digit_count += 1; } else { // Skip overflow digits but adjust exponent decimal_adjust += 1; } pos += 1; } let mut has_decimal = false; let mut has_exponent = false; // Check for decimal point if pos < len && bytes[pos] == b'.' { has_decimal = true; pos += 1; // Parse fractional digits while pos < len && bytes[pos].is_ascii_digit() { has_digits = true; let digit = (bytes[pos] - b'0') as u64; if significand <= (u64::MAX - 9) / 10 { significand = significand * 10 + digit; digit_count += 1; decimal_adjust -= 1; } pos += 1; } } if !has_digits { return (NumericParseResult::NotNumeric, ParsedNumber::None); } // Check for exponent let mut exponent = 0i32; if pos < len && (bytes[pos] == b'e' || bytes[pos] == b'E') { has_exponent = true; pos += 1; if pos >= len { // Incomplete exponent, but we have valid digits before return create_result_from_significand( significand, sign, decimal_adjust, has_decimal, has_exponent, NumericParseResult::ValidPrefixOnly, ); } let mut exp_sign = 1i32; if bytes[pos] == b'-' { exp_sign = -1; pos += 1; } else if bytes[pos] == b'+' { pos += 1; } if pos >= len || !bytes[pos].is_ascii_digit() { // Incomplete exponent return create_result_from_significand( significand, sign, decimal_adjust, has_decimal, false, NumericParseResult::ValidPrefixOnly, ); } // Parse exponent digits while pos < len && bytes[pos].is_ascii_digit() { let digit = (bytes[pos] - b'0') as i32; if exponent < 10000 { exponent = exponent * 10 + digit; } else { exponent = 10000; // Cap at large value } pos += 1; } exponent *= exp_sign; } // Skip trailing whitespace while pos < len && is_space(bytes[pos]) { pos += 1; } // Determine if we consumed the entire string let consumed_all = pos >= len; let final_exponent = decimal_adjust + exponent; let parse_result = if !consumed_all { NumericParseResult::ValidPrefixOnly } else if has_decimal || has_exponent { NumericParseResult::HasDecimalOrExp } else { NumericParseResult::PureInteger }; create_result_from_significand( significand, sign, final_exponent, has_decimal, has_exponent, parse_result, ) } fn create_result_from_significand( significand: u64, sign: i64, exponent: i32, has_decimal: bool, has_exponent: bool, parse_result: NumericParseResult, ) -> (NumericParseResult, ParsedNumber) { if significand == 0 { match parse_result { NumericParseResult::PureInteger => { return (parse_result, ParsedNumber::Integer(0)); } _ => { return (parse_result, ParsedNumber::Float(0.0)); } } } // For pure integers without exponent, try to return as integer if !has_decimal && !has_exponent && exponent == 0 && significand <= i64::MAX as u64 { let signed_val = (significand as i64).wrapping_mul(sign); return (parse_result, ParsedNumber::Integer(signed_val)); } // Convert to float let mut result = significand as f64; let mut exp = exponent; match exp.cmp(&0) { std::cmp::Ordering::Greater => { while exp >= 100 { result *= 1e100; exp -= 100; } while exp >= 10 { result *= 1e10; exp -= 10; } while exp >= 1 { result *= 10.0; exp -= 1; } } std::cmp::Ordering::Less => { while exp <= -100 { result *= 1e-100; exp += 100; } while exp <= -10 { result *= 1e-10; exp += 10; } while exp <= -1 { result *= 0.1; exp += 1; } } std::cmp::Ordering::Equal => {} } if sign < 0 { result = -result; } (parse_result, ParsedNumber::Float(result)) } pub fn is_space(byte: u8) -> bool { matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') } fn real_to_i64(r: f64) -> i64 { if r < -9223372036854774784.0 { i64::MIN } else if r > 9223372036854774784.0 { i64::MAX } else { r as i64 } } fn apply_integer_affinity(register: &mut Register) -> bool { let Register::Value(Value::Float(f)) = register else { return false; }; let ix = real_to_i64(*f); // Only convert if round-trip is exact and not at extreme values if *f == (ix as f64) && ix > i64::MIN && ix < i64::MAX { *register = Register::Value(Value::Integer(ix)); true } else { false } } /// Try to convert a value into a numeric representation if we can /// do so without loss of information. In other words, if the string /// looks like a number, convert it into a number. If it does not /// look like a number, leave it alone. pub fn apply_numeric_affinity(register: &mut Register, try_for_int: bool) -> bool { let Register::Value(Value::Text(text)) = register else { return false; // Only apply to text values }; let text_str = text.as_str(); let (parse_result, parsed_value) = try_for_float(text_str); // Only convert if we have a complete valid number (not just a prefix) match parse_result { NumericParseResult::NotNumeric | NumericParseResult::ValidPrefixOnly => { false // Leave as text } NumericParseResult::PureInteger => { if let Some(int_val) = parsed_value.as_integer() { *register = Register::Value(Value::Integer(int_val)); true } else if let Some(float_val) = parsed_value.as_float() { *register = Register::Value(Value::Float(float_val)); if try_for_int { apply_integer_affinity(register); } true } else { false } } NumericParseResult::HasDecimalOrExp => { if let Some(float_val) = parsed_value.as_float() { *register = Register::Value(Value::Float(float_val)); // If try_for_int is true, try to convert float to int if exact if try_for_int { apply_integer_affinity(register); } true } else { false } } } } fn is_numeric_value(reg: &Register) -> bool { matches!(reg.get_value(), Value::Integer(_) | Value::Float(_)) } fn stringify_register(reg: &mut Register) -> bool { match reg.get_value() { Value::Integer(i) => { *reg = Register::Value(Value::build_text(i.to_string())); true } Value::Float(f) => { *reg = Register::Value(Value::build_text(f.to_string())); true } Value::Text(_) | Value::Null | Value::Blob(_) => false, } } pub fn op_max_pgcnt( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(MaxPgcnt { db, dest, new_max }, insn); if *db > 0 { return Err(LimboError::InternalError( "temp/attached databases not implemented yet".to_string(), )); } let result_value = if *new_max == 0 { // If new_max is 0, just return current maximum without changing it pager.get_max_page_count() } else { // Set new maximum page count (will be clamped to current database size) return_if_io!(pager.set_max_page_count(*new_max as u32)) }; state.registers[*dest] = Register::Value(Value::Integer(result_value.into())); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_journal_mode( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, mv_store: Option<&Arc>, ) -> Result { load_insn!(JournalMode { db, dest, new_mode }, insn); if *db > 0 { return Err(LimboError::InternalError( "temp/attached databases not implemented yet".to_string(), )); } // Currently, Turso only supports WAL mode // If a new mode is specified, we validate it but always return "wal" if let Some(mode) = new_mode { let mode_bytes = mode.as_bytes(); // Valid journal modes in SQLite are: delete, truncate, persist, memory, wal, off // We accept any valid mode but always use WAL match_ignore_ascii_case!(match mode_bytes { b"delete" | b"truncate" | b"persist" | b"memory" | b"wal" | b"off" => { // Mode is valid, but we stay in WAL mode } _ => { // Invalid journal mode return Err(LimboError::ParseError(format!( "Unknown journal mode: {mode}" ))); } }) } // Always return "wal" as the current journal mode state.registers[*dest] = Register::Value(Value::build_text("wal")); state.pc += 1; Ok(InsnFunctionStepResult::Step) } fn with_header( pager: &Arc, mv_store: Option<&Arc>, program: &Program, f: F, ) -> Result> where F: Fn(&DatabaseHeader) -> T, { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id(); mv_store.with_header(f, tx_id.as_ref()).map(IOResult::Done) } else { pager.with_header(&f) } } fn with_header_mut( pager: &Arc, mv_store: Option<&Arc>, program: &Program, f: F, ) -> Result> where F: Fn(&mut DatabaseHeader) -> T, { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id(); mv_store .with_header_mut(f, tx_id.as_ref()) .map(IOResult::Done) } else { pager.with_header_mut(&f) } } fn get_schema_cookie( pager: &Arc, mv_store: Option<&Arc>, program: &Program, ) -> Result> { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id(); mv_store .with_header(|header| header.schema_cookie.get(), tx_id.as_ref()) .map(IOResult::Done) } else { pager.get_schema_cookie() } } #[cfg(test)] mod tests { use rand::{Rng, RngCore}; use super::*; use crate::types::Value; #[test] fn test_apply_numeric_affinity_partial_numbers() { let mut reg = Register::Value(Value::Text("123abc".into())); assert!(!apply_numeric_affinity(&mut reg, false)); assert!(matches!(reg, Register::Value(Value::Text(_)))); let mut reg = Register::Value(Value::Text("-53093015420544-15062897".into())); assert!(!apply_numeric_affinity(&mut reg, false)); assert!(matches!(reg, Register::Value(Value::Text(_)))); let mut reg = Register::Value(Value::Text("123.45xyz".into())); assert!(!apply_numeric_affinity(&mut reg, false)); assert!(matches!(reg, Register::Value(Value::Text(_)))); } #[test] fn test_apply_numeric_affinity_complete_numbers() { let mut reg = Register::Value(Value::Text("123".into())); assert!(apply_numeric_affinity(&mut reg, false)); assert_eq!(*reg.get_value(), Value::Integer(123)); let mut reg = Register::Value(Value::Text("123.45".into())); assert!(apply_numeric_affinity(&mut reg, false)); assert_eq!(*reg.get_value(), Value::Float(123.45)); let mut reg = Register::Value(Value::Text(" -456 ".into())); assert!(apply_numeric_affinity(&mut reg, false)); assert_eq!(*reg.get_value(), Value::Integer(-456)); let mut reg = Register::Value(Value::Text("0".into())); assert!(apply_numeric_affinity(&mut reg, false)); assert_eq!(*reg.get_value(), Value::Integer(0)); } #[test] fn test_exec_add() { let inputs = vec![ (Value::Integer(3), Value::Integer(1)), (Value::Float(3.0), Value::Float(1.0)), (Value::Float(3.0), Value::Integer(1)), (Value::Integer(3), Value::Float(1.0)), (Value::Null, Value::Null), (Value::Null, Value::Integer(1)), (Value::Null, Value::Float(1.0)), (Value::Null, Value::Text("2".into())), (Value::Integer(1), Value::Null), (Value::Float(1.0), Value::Null), (Value::Text("1".into()), Value::Null), (Value::Text("1".into()), Value::Text("3".into())), (Value::Text("1.0".into()), Value::Text("3.0".into())), (Value::Text("1.0".into()), Value::Float(3.0)), (Value::Text("1.0".into()), Value::Integer(3)), (Value::Float(1.0), Value::Text("3.0".into())), (Value::Integer(1), Value::Text("3".into())), ]; let outputs = [ Value::Integer(4), Value::Float(4.0), Value::Float(4.0), Value::Float(4.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Integer(4), Value::Float(4.0), Value::Float(4.0), Value::Float(4.0), Value::Float(4.0), Value::Float(4.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_add(rhs), outputs[i], "Wrong ADD for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_subtract() { let inputs = vec![ (Value::Integer(3), Value::Integer(1)), (Value::Float(3.0), Value::Float(1.0)), (Value::Float(3.0), Value::Integer(1)), (Value::Integer(3), Value::Float(1.0)), (Value::Null, Value::Null), (Value::Null, Value::Integer(1)), (Value::Null, Value::Float(1.0)), (Value::Null, Value::Text("1".into())), (Value::Integer(1), Value::Null), (Value::Float(1.0), Value::Null), (Value::Text("4".into()), Value::Null), (Value::Text("1".into()), Value::Text("3".into())), (Value::Text("1.0".into()), Value::Text("3.0".into())), (Value::Text("1.0".into()), Value::Float(3.0)), (Value::Text("1.0".into()), Value::Integer(3)), (Value::Float(1.0), Value::Text("3.0".into())), (Value::Integer(1), Value::Text("3".into())), ]; let outputs = [ Value::Integer(2), Value::Float(2.0), Value::Float(2.0), Value::Float(2.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Integer(-2), Value::Float(-2.0), Value::Float(-2.0), Value::Float(-2.0), Value::Float(-2.0), Value::Float(-2.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_subtract(rhs), outputs[i], "Wrong subtract for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_multiply() { let inputs = vec![ (Value::Integer(3), Value::Integer(2)), (Value::Float(3.0), Value::Float(2.0)), (Value::Float(3.0), Value::Integer(2)), (Value::Integer(3), Value::Float(2.0)), (Value::Null, Value::Null), (Value::Null, Value::Integer(1)), (Value::Null, Value::Float(1.0)), (Value::Null, Value::Text("1".into())), (Value::Integer(1), Value::Null), (Value::Float(1.0), Value::Null), (Value::Text("4".into()), Value::Null), (Value::Text("2".into()), Value::Text("3".into())), (Value::Text("2.0".into()), Value::Text("3.0".into())), (Value::Text("2.0".into()), Value::Float(3.0)), (Value::Text("2.0".into()), Value::Integer(3)), (Value::Float(2.0), Value::Text("3.0".into())), (Value::Integer(2), Value::Text("3.0".into())), ]; let outputs = [ Value::Integer(6), Value::Float(6.0), Value::Float(6.0), Value::Float(6.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Integer(6), Value::Float(6.0), Value::Float(6.0), Value::Float(6.0), Value::Float(6.0), Value::Float(6.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_multiply(rhs), outputs[i], "Wrong multiply for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_divide() { let inputs = vec![ (Value::Integer(1), Value::Integer(0)), (Value::Float(1.0), Value::Float(0.0)), (Value::Integer(i64::MIN), Value::Integer(-1)), (Value::Float(6.0), Value::Float(2.0)), (Value::Float(6.0), Value::Integer(2)), (Value::Integer(6), Value::Integer(2)), (Value::Null, Value::Integer(2)), (Value::Integer(2), Value::Null), (Value::Null, Value::Null), (Value::Text("6".into()), Value::Text("2".into())), (Value::Text("6".into()), Value::Integer(2)), ]; let outputs = [ Value::Null, Value::Null, Value::Float(9.223372036854776e18), Value::Float(3.0), Value::Float(3.0), Value::Float(3.0), Value::Null, Value::Null, Value::Null, Value::Float(3.0), Value::Float(3.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_divide(rhs), outputs[i], "Wrong divide for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_remainder() { let inputs = vec![ (Value::Null, Value::Null), (Value::Null, Value::Float(1.0)), (Value::Null, Value::Integer(1)), (Value::Null, Value::Text("1".into())), (Value::Float(1.0), Value::Null), (Value::Integer(1), Value::Null), (Value::Integer(12), Value::Integer(0)), (Value::Float(12.0), Value::Float(0.0)), (Value::Float(12.0), Value::Integer(0)), (Value::Integer(12), Value::Float(0.0)), (Value::Integer(i64::MIN), Value::Integer(-1)), (Value::Integer(12), Value::Integer(3)), (Value::Float(12.0), Value::Float(3.0)), (Value::Float(12.0), Value::Integer(3)), (Value::Integer(12), Value::Float(3.0)), (Value::Integer(12), Value::Integer(-3)), (Value::Float(12.0), Value::Float(-3.0)), (Value::Float(12.0), Value::Integer(-3)), (Value::Integer(12), Value::Float(-3.0)), (Value::Text("12.0".into()), Value::Text("3.0".into())), (Value::Text("12.0".into()), Value::Float(3.0)), (Value::Float(12.0), Value::Text("3.0".into())), ]; let outputs = vec![ Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Float(0.0), Value::Integer(0), Value::Float(0.0), Value::Float(0.0), Value::Float(0.0), Value::Integer(0), Value::Float(0.0), Value::Float(0.0), Value::Float(0.0), Value::Float(0.0), Value::Float(0.0), Value::Float(0.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_remainder(rhs), outputs[i], "Wrong remainder for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_and() { let inputs = vec![ (Value::Integer(0), Value::Null), (Value::Null, Value::Integer(1)), (Value::Null, Value::Null), (Value::Float(0.0), Value::Null), (Value::Integer(1), Value::Float(2.2)), (Value::Integer(0), Value::Text("string".into())), (Value::Integer(0), Value::Text("1".into())), (Value::Integer(1), Value::Text("1".into())), ]; let outputs = [ Value::Integer(0), Value::Null, Value::Null, Value::Integer(0), Value::Integer(1), Value::Integer(0), Value::Integer(0), Value::Integer(1), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_and(rhs), outputs[i], "Wrong AND for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_or() { let inputs = vec![ (Value::Integer(0), Value::Null), (Value::Null, Value::Integer(1)), (Value::Null, Value::Null), (Value::Float(0.0), Value::Null), (Value::Integer(1), Value::Float(2.2)), (Value::Float(0.0), Value::Integer(0)), (Value::Integer(0), Value::Text("string".into())), (Value::Integer(0), Value::Text("1".into())), (Value::Integer(0), Value::Text("".into())), ]; let outputs = [ Value::Null, Value::Integer(1), Value::Null, Value::Null, Value::Integer(1), Value::Integer(0), Value::Integer(0), Value::Integer(1), Value::Integer(0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_or(rhs), outputs[i], "Wrong OR for lhs: {lhs}, rhs: {rhs}" ); } } use crate::vdbe::{Bitfield, Register}; use super::{exec_char, execute_turso_version}; use std::collections::HashMap; #[test] fn test_length() { let input_str = Value::build_text("bob"); let expected_len = Value::Integer(3); assert_eq!(input_str.exec_length(), expected_len); let input_integer = Value::Integer(123); let expected_len = Value::Integer(3); assert_eq!(input_integer.exec_length(), expected_len); let input_float = Value::Float(123.456); let expected_len = Value::Integer(7); assert_eq!(input_float.exec_length(), expected_len); let expected_blob = Value::Blob("example".as_bytes().to_vec()); let expected_len = Value::Integer(7); assert_eq!(expected_blob.exec_length(), expected_len); } #[test] fn test_quote() { let input = Value::build_text("abc\0edf"); let expected = Value::build_text("'abc'"); assert_eq!(input.exec_quote(), expected); let input = Value::Integer(123); let expected = Value::Integer(123); assert_eq!(input.exec_quote(), expected); let input = Value::build_text("hello''world"); let expected = Value::build_text("'hello''''world'"); assert_eq!(input.exec_quote(), expected); } #[test] fn test_typeof() { let input = Value::Null; let expected: Value = Value::build_text("null"); assert_eq!(input.exec_typeof(), expected); let input = Value::Integer(123); let expected: Value = Value::build_text("integer"); assert_eq!(input.exec_typeof(), expected); let input = Value::Float(123.456); let expected: Value = Value::build_text("real"); assert_eq!(input.exec_typeof(), expected); let input = Value::build_text("hello"); let expected: Value = Value::build_text("text"); assert_eq!(input.exec_typeof(), expected); let input = Value::Blob("limbo".as_bytes().to_vec()); let expected: Value = Value::build_text("blob"); assert_eq!(input.exec_typeof(), expected); } #[test] fn test_unicode() { assert_eq!(Value::build_text("a").exec_unicode(), Value::Integer(97)); assert_eq!( Value::build_text("😊").exec_unicode(), Value::Integer(128522) ); assert_eq!(Value::build_text("").exec_unicode(), Value::Null); assert_eq!(Value::Integer(23).exec_unicode(), Value::Integer(50)); assert_eq!(Value::Integer(0).exec_unicode(), Value::Integer(48)); assert_eq!(Value::Float(0.0).exec_unicode(), Value::Integer(48)); assert_eq!(Value::Float(23.45).exec_unicode(), Value::Integer(50)); assert_eq!(Value::Null.exec_unicode(), Value::Null); assert_eq!( Value::Blob("example".as_bytes().to_vec()).exec_unicode(), Value::Integer(101) ); } #[test] fn test_min_max() { let input_int_vec = [ Register::Value(Value::Integer(-1)), Register::Value(Value::Integer(10)), ]; assert_eq!( Value::exec_min(input_int_vec.iter().map(|v| v.get_value())), Value::Integer(-1) ); assert_eq!( Value::exec_max(input_int_vec.iter().map(|v| v.get_value())), Value::Integer(10) ); let str1 = Register::Value(Value::build_text("A")); let str2 = Register::Value(Value::build_text("z")); let input_str_vec = [str2, str1.clone()]; assert_eq!( Value::exec_min(input_str_vec.iter().map(|v| v.get_value())), Value::build_text("A") ); assert_eq!( Value::exec_max(input_str_vec.iter().map(|v| v.get_value())), Value::build_text("z") ); let input_null_vec = [Register::Value(Value::Null), Register::Value(Value::Null)]; assert_eq!( Value::exec_min(input_null_vec.iter().map(|v| v.get_value())), Value::Null ); assert_eq!( Value::exec_max(input_null_vec.iter().map(|v| v.get_value())), Value::Null ); let input_mixed_vec = [Register::Value(Value::Integer(10)), str1]; assert_eq!( Value::exec_min(input_mixed_vec.iter().map(|v| v.get_value())), Value::Integer(10) ); assert_eq!( Value::exec_max(input_mixed_vec.iter().map(|v| v.get_value())), Value::build_text("A") ); } #[test] fn test_trim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text("Bob and Alice"); assert_eq!(input_str.exec_trim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text("Alice"); assert_eq!(input_str.exec_trim(Some(&pattern_str)), expected_str); let input_str = Value::build_text("\ta"); let expected_str = Value::build_text("\ta"); assert_eq!(input_str.exec_trim(None), expected_str); let input_str = Value::build_text("\na"); let expected_str = Value::build_text("\na"); assert_eq!(input_str.exec_trim(None), expected_str); } #[test] fn test_ltrim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text("Bob and Alice "); assert_eq!(input_str.exec_ltrim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text("Alice "); assert_eq!(input_str.exec_ltrim(Some(&pattern_str)), expected_str); } #[test] fn test_rtrim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text(" Bob and Alice"); assert_eq!(input_str.exec_rtrim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text(" Bob and Alice"); assert_eq!(input_str.exec_rtrim(Some(&pattern_str)), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("and Alice"); let expected_str = Value::build_text(" Bob"); assert_eq!(input_str.exec_rtrim(Some(&pattern_str)), expected_str); } #[test] fn test_soundex() { let input_str = Value::build_text("Pfister"); let expected_str = Value::build_text("P236"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("husobee"); let expected_str = Value::build_text("H210"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Tymczak"); let expected_str = Value::build_text("T522"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Ashcraft"); let expected_str = Value::build_text("A261"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Robert"); let expected_str = Value::build_text("R163"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Rupert"); let expected_str = Value::build_text("R163"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Rubin"); let expected_str = Value::build_text("R150"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Kant"); let expected_str = Value::build_text("K530"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Knuth"); let expected_str = Value::build_text("K530"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("x"); let expected_str = Value::build_text("X000"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("闪电五连鞭"); let expected_str = Value::build_text("?000"); assert_eq!(input_str.exec_soundex(), expected_str); } #[test] fn test_upper_case() { let input_str = Value::build_text("Limbo"); let expected_str = Value::build_text("LIMBO"); assert_eq!(input_str.exec_upper().unwrap(), expected_str); let input_int = Value::Integer(10); assert_eq!(input_int.exec_upper().unwrap(), Value::build_text("10")); assert_eq!(Value::Null.exec_upper(), None) } #[test] fn test_lower_case() { let input_str = Value::build_text("Limbo"); let expected_str = Value::build_text("limbo"); assert_eq!(input_str.exec_lower().unwrap(), expected_str); let input_int = Value::Integer(10); assert_eq!(input_int.exec_lower().unwrap(), Value::build_text("10")); assert_eq!(Value::Null.exec_lower(), None) } #[test] fn test_hex() { let input_str = Value::build_text("limbo"); let expected_val = Value::build_text("6C696D626F"); assert_eq!(input_str.exec_hex(), expected_val); let input_int = Value::Integer(100); let expected_val = Value::build_text("313030"); assert_eq!(input_int.exec_hex(), expected_val); let input_float = Value::Float(12.34); let expected_val = Value::build_text("31322E3334"); assert_eq!(input_float.exec_hex(), expected_val); let input_blob = Value::Blob(vec![0xff]); let expected_val = Value::build_text("FF"); assert_eq!(input_blob.exec_hex(), expected_val); } #[test] fn test_unhex() { let input = Value::build_text("6f"); let expected = Value::Blob(vec![0x6f]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("6f"); let expected = Value::Blob(vec![0x6f]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("611"); let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text(""); let expected = Value::Blob(vec![]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("61x"); let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); let input = Value::Null; let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); } #[test] fn test_abs() { let int_positive_reg = Value::Integer(10); let int_negative_reg = Value::Integer(-10); assert_eq!(int_positive_reg.exec_abs().unwrap(), int_positive_reg); assert_eq!(int_negative_reg.exec_abs().unwrap(), int_positive_reg); let float_positive_reg = Value::Integer(10); let float_negative_reg = Value::Integer(-10); assert_eq!(float_positive_reg.exec_abs().unwrap(), float_positive_reg); assert_eq!(float_negative_reg.exec_abs().unwrap(), float_positive_reg); assert_eq!( Value::build_text("a").exec_abs().unwrap(), Value::Float(0.0) ); assert_eq!(Value::Null.exec_abs().unwrap(), Value::Null); // ABS(i64::MIN) should return RuntimeError assert!(Value::Integer(i64::MIN).exec_abs().is_err()); } #[test] fn test_char() { assert_eq!( exec_char(&[ Register::Value(Value::Integer(108)), Register::Value(Value::Integer(105)) ]), Value::build_text("li") ); assert_eq!(exec_char(&[]), Value::build_text("")); assert_eq!( exec_char(&[Register::Value(Value::Null)]), Value::build_text("") ); assert_eq!( exec_char(&[Register::Value(Value::build_text("a"))]), Value::build_text("") ); } #[test] fn test_like_with_escape_or_regexmeta_chars() { assert!(Value::exec_like(None, r#"\%A"#, r#"\A"#)); assert!(Value::exec_like(None, "%a%a", "aaaa")); } #[test] fn test_like_no_cache() { assert!(Value::exec_like(None, "a%", "aaaa")); assert!(Value::exec_like(None, "%a%a", "aaaa")); assert!(!Value::exec_like(None, "%a.a", "aaaa")); assert!(!Value::exec_like(None, "a.a%", "aaaa")); assert!(!Value::exec_like(None, "%a.ab", "aaaa")); } #[test] fn test_like_with_cache() { let mut cache = HashMap::new(); assert!(Value::exec_like(Some(&mut cache), "a%", "aaaa")); assert!(Value::exec_like(Some(&mut cache), "%a%a", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "%a.a", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "a.a%", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "%a.ab", "aaaa")); // again after values have been cached assert!(Value::exec_like(Some(&mut cache), "a%", "aaaa")); assert!(Value::exec_like(Some(&mut cache), "%a%a", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "%a.a", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "a.a%", "aaaa")); assert!(!Value::exec_like(Some(&mut cache), "%a.ab", "aaaa")); } #[test] fn test_random() { match Value::exec_random(|| rand::rng().random()) { Value::Integer(value) => { // Check that the value is within the range of i64 assert!( (i64::MIN..=i64::MAX).contains(&value), "Random number out of range" ); } _ => panic!("exec_random did not return an Integer variant"), } } #[test] fn test_exec_randomblob() { struct TestCase { input: Value, expected_len: usize, } let test_cases = vec![ TestCase { input: Value::Integer(5), expected_len: 5, }, TestCase { input: Value::Integer(0), expected_len: 1, }, TestCase { input: Value::Integer(-1), expected_len: 1, }, TestCase { input: Value::build_text(""), expected_len: 1, }, TestCase { input: Value::build_text("5"), expected_len: 5, }, TestCase { input: Value::build_text("0"), expected_len: 1, }, TestCase { input: Value::build_text("-1"), expected_len: 1, }, TestCase { input: Value::Float(2.9), expected_len: 2, }, TestCase { input: Value::Float(-3.15), expected_len: 1, }, TestCase { input: Value::Null, expected_len: 1, }, ]; for test_case in &test_cases { let result = test_case.input.exec_randomblob(|dest| { rand::rng().fill_bytes(dest); }); match result { Value::Blob(blob) => { assert_eq!(blob.len(), test_case.expected_len); } _ => panic!("exec_randomblob did not return a Blob variant"), } } } #[test] fn test_exec_round() { let input_val = Value::Float(123.456); let expected_val = Value::Float(123.0); assert_eq!(input_val.exec_round(None), expected_val); let input_val = Value::Float(123.456); let precision_val = Value::Integer(2); let expected_val = Value::Float(123.46); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::Float(123.456); let precision_val = Value::build_text("1"); let expected_val = Value::Float(123.5); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::build_text("123.456"); let precision_val = Value::Integer(2); let expected_val = Value::Float(123.46); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::Integer(123); let precision_val = Value::Integer(1); let expected_val = Value::Float(123.0); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::Float(100.123); let expected_val = Value::Float(100.0); assert_eq!(input_val.exec_round(None), expected_val); let input_val = Value::Float(100.123); let expected_val = Value::Null; assert_eq!(input_val.exec_round(Some(&Value::Null)), expected_val); } #[test] fn test_exec_if() { let reg = Value::Integer(0); assert!(!reg.exec_if(false, false)); assert!(reg.exec_if(false, true)); let reg = Value::Integer(1); assert!(reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); let reg = Value::Null; assert!(!reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); let reg = Value::Null; assert!(reg.exec_if(true, false)); assert!(reg.exec_if(true, true)); let reg = Value::Null; assert!(!reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); } #[test] fn test_nullif() { assert_eq!( Value::Integer(1).exec_nullif(&Value::Integer(1)), Value::Null ); assert_eq!( Value::Float(1.1).exec_nullif(&Value::Float(1.1)), Value::Null ); assert_eq!( Value::build_text("limbo").exec_nullif(&Value::build_text("limbo")), Value::Null ); assert_eq!( Value::Integer(1).exec_nullif(&Value::Integer(2)), Value::Integer(1) ); assert_eq!( Value::Float(1.1).exec_nullif(&Value::Float(1.2)), Value::Float(1.1) ); assert_eq!( Value::build_text("limbo").exec_nullif(&Value::build_text("limb")), Value::build_text("limbo") ); } #[test] fn test_substring() { let str_value = Value::build_text("limbo"); let start_value = Value::Integer(1); let length_value = Value::Integer(3); let expected_val = Value::build_text("lim"); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::Integer(1); let length_value = Value::Integer(10); let expected_val = Value::build_text("limbo"); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::Integer(10); let length_value = Value::Integer(3); let expected_val = Value::build_text(""); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::Integer(3); let length_value = Value::Null; let expected_val = Value::build_text("mbo"); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::Integer(10); let length_value = Value::Null; let expected_val = Value::build_text(""); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); } #[test] fn test_exec_instr() { let input = Value::build_text("limbo"); let pattern = Value::build_text("im"); let expected = Value::Integer(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("limbo"); let expected = Value::Integer(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("o"); let expected = Value::Integer(5); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("liiiiimbo"); let pattern = Value::build_text("ii"); let expected = Value::Integer(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("limboX"); let expected = Value::Integer(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text(""); let expected = Value::Integer(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text(""); let pattern = Value::build_text("limbo"); let expected = Value::Integer(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text(""); let pattern = Value::build_text(""); let expected = Value::Integer(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Null; let pattern = Value::Null; let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::Null; let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Null; let pattern = Value::build_text("limbo"); let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Integer(123); let pattern = Value::Integer(2); let expected = Value::Integer(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Integer(123); let pattern = Value::Integer(5); let expected = Value::Integer(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Float(12.34); let pattern = Value::Float(2.3); let expected = Value::Integer(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Float(12.34); let pattern = Value::Float(5.6); let expected = Value::Integer(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Float(12.34); let pattern = Value::build_text("."); let expected = Value::Integer(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![1, 2, 3, 4, 5]); let pattern = Value::Blob(vec![3, 4]); let expected = Value::Integer(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![1, 2, 3, 4, 5]); let pattern = Value::Blob(vec![3, 2]); let expected = Value::Integer(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![0x61, 0x62, 0x63, 0x64, 0x65]); let pattern = Value::build_text("cd"); let expected = Value::Integer(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("abcde"); let pattern = Value::Blob(vec![0x63, 0x64]); let expected = Value::Integer(3); assert_eq!(input.exec_instr(&pattern), expected); } #[test] fn test_exec_sign() { let input = Value::Integer(42); let expected = Some(Value::Integer(1)); assert_eq!(input.exec_sign(), expected); let input = Value::Integer(-42); let expected = Some(Value::Integer(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::Integer(0); let expected = Some(Value::Integer(0)); assert_eq!(input.exec_sign(), expected); let input = Value::Float(0.0); let expected = Some(Value::Integer(0)); assert_eq!(input.exec_sign(), expected); let input = Value::Float(0.1); let expected = Some(Value::Integer(1)); assert_eq!(input.exec_sign(), expected); let input = Value::Float(42.0); let expected = Some(Value::Integer(1)); assert_eq!(input.exec_sign(), expected); let input = Value::Float(-42.0); let expected = Some(Value::Integer(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("abc"); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::build_text("42"); let expected = Some(Value::Integer(1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("-42"); let expected = Some(Value::Integer(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("0"); let expected = Some(Value::Integer(0)); assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"abc".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"42".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"-42".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"0".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Null; let expected = None; assert_eq!(input.exec_sign(), expected); } #[test] fn test_exec_zeroblob() { let input = Value::Integer(0); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::Null; let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::Integer(4); let expected = Value::Blob(vec![0; 4]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::Integer(-1); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::build_text("5"); let expected = Value::Blob(vec![0; 5]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::build_text("-5"); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::build_text("text"); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::Float(2.6); let expected = Value::Blob(vec![0; 2]); assert_eq!(input.exec_zeroblob(), expected); let input = Value::Blob(vec![1]); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob(), expected); } #[test] fn test_execute_sqlite_version() { let version_integer = 3046001; let expected = "3.46.1"; assert_eq!(execute_turso_version(version_integer), expected); } #[test] fn test_replace() { let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("aoa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text(""); let expected_str = Value::build_text("o"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text("abc"); let expected_str = Value::build_text("abcoabc"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("a"); let replace_str = Value::build_text("b"); let expected_str = Value::build_text("bob"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text(""); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("bob"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::Null; let replace_str = Value::build_text("a"); let expected_str = Value::Null; assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5"); let pattern_str = Value::Integer(5); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("boa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5.0"); let pattern_str = Value::Float(5.0); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("boa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5"); let pattern_str = Value::Float(5.0); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("bo5"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5.0"); let pattern_str = Value::Float(5.0); let replace_str = Value::Float(6.0); let expected_str = Value::build_text("bo6.0"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); // todo: change this test to use (0.1 + 0.2) instead of 0.3 when decimals are implemented. let input_str = Value::build_text("tes3"); let pattern_str = Value::Integer(3); let replace_str = Value::Float(0.3); let expected_str = Value::build_text("tes0.3"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); } #[test] fn test_bitfield() { let mut bitfield = Bitfield::<4>::new(); for i in 0..256 { bitfield.set(i); assert!(bitfield.get(i)); for j in 0..i { assert!(bitfield.get(j)); } for j in i + 1..256 { assert!(!bitfield.get(j)); } } for i in 0..256 { bitfield.unset(i); assert!(!bitfield.get(i)); } } }