mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-29 05:54:21 +01:00
11573 lines
410 KiB
Rust
11573 lines
410 KiB
Rust
#![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,
|
||
};
|
||
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, 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<Pager>,
|
||
Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult>;
|
||
|
||
/// 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<CollationSeq>,
|
||
) -> 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<T> From<IOResult<T>> for InsnFunctionStepResult {
|
||
fn from(value: IOResult<T>) -> 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<CheckpointResult> },
|
||
CompleteResult { result: Result<CheckpointResult> },
|
||
}
|
||
|
||
pub fn op_checkpoint(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
load_insn!(
|
||
OpenRead {
|
||
cursor_id,
|
||
root_page,
|
||
db,
|
||
},
|
||
insn
|
||
);
|
||
|
||
let pager = program.get_pager_from_database_index(db);
|
||
|
||
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<dyn CursorTrait>| -> Result<Box<dyn CursorTrait>> {
|
||
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::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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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_open_pseudo(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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);
|
||
let index_cursor = index_cursor.as_btree_mut();
|
||
return_if_io!(index_cursor.rowid())
|
||
}) 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::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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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
|
||
}
|
||
_ => 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
err_code: usize,
|
||
description: &str,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
if err_code > 0 {
|
||
// Any non-FK constraint violation causes the statement subtransaction to roll back.
|
||
state.end_statement(&program.connection, pager, EndStatement::RollbackSavepoint)?;
|
||
}
|
||
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 {
|
||
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)?;
|
||
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)
|
||
}
|
||
}
|
||
|
||
pub fn op_halt(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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);
|
||
let index_cursor = index_cursor.as_btree_mut();
|
||
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!(),
|
||
}
|
||
};
|
||
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 {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
load_insn!(IdxRowId { cursor_id, dest }, insn);
|
||
let cursors = &mut state.cursors;
|
||
let cursor = cursors.get_mut(*cursor_id).unwrap().as_mut().unwrap();
|
||
let cursor = cursor.as_btree_mut();
|
||
let rowid = return_if_io!(cursor.rowid());
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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),
|
||
_ => unreachable!("apply_affinity_char with Numeric should produce an integer if it returns true"),
|
||
}
|
||
} 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
record_source: RecordSource,
|
||
cursor_id: usize,
|
||
is_index: bool,
|
||
op: SeekOp,
|
||
) -> Result<SeekInternalResult> {
|
||
/// wrapper so we can use the ? operator and handle errors correctly in this outer function
|
||
fn inner(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
record_source: RecordSource,
|
||
cursor_id: usize,
|
||
is_index: bool,
|
||
op: SeekOp,
|
||
) -> Result<SeekInternalResult> {
|
||
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->opcode<OP_IdxLT ){
|
||
/// assert( pOp->opcode==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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<ExtValue> = 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<ExtValue> = 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 table_name = normalize_ident(tbl_name.name.as_str());
|
||
|
||
if rename_from != table_name {
|
||
break 'sql None;
|
||
}
|
||
|
||
Some(
|
||
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,
|
||
}
|
||
.to_string(),
|
||
)
|
||
}
|
||
_ => todo!(),
|
||
}
|
||
};
|
||
|
||
(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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Value>)>,
|
||
}
|
||
|
||
#[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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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::<Vec<_>>();
|
||
|
||
// 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::<Vec<_>>();
|
||
|
||
// 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Value>)>,
|
||
}
|
||
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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::<Vec<_>>();
|
||
|
||
// 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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
load_insn!(
|
||
IdxDelete {
|
||
cursor_id,
|
||
start_reg,
|
||
num_regs,
|
||
raise_error_if_no_matching_entry,
|
||
},
|
||
insn
|
||
);
|
||
|
||
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::<Vec<_>>();
|
||
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::<Vec<_>>();
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
load_insn!(
|
||
IdxInsert {
|
||
cursor_id,
|
||
record_reg,
|
||
flags,
|
||
..
|
||
},
|
||
*insn
|
||
);
|
||
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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::<MvCursor>().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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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);
|
||
|
||
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<dyn CursorTrait>| -> Result<Box<dyn CursorTrait>> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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 enum OpDestroyState {
|
||
CreateCursor,
|
||
DestroyBtree(Arc<RwLock<BTreeCursor>>),
|
||
}
|
||
|
||
pub fn op_destroy(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
_mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
_mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
_mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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::<i64>().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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
// 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<Pager>,
|
||
},
|
||
CreateBtree {
|
||
pager: Arc<Pager>,
|
||
},
|
||
// 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<dyn CursorTrait>,
|
||
},
|
||
}
|
||
pub fn op_open_ephemeral(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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;
|
||
let db_file_io: Arc<dyn crate::IO>;
|
||
|
||
// 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 = 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 = 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::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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<dyn CursorTrait> =
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<IntegrityCheckError>,
|
||
current_root_idx: usize,
|
||
state: IntegrityCheckState,
|
||
},
|
||
}
|
||
pub fn op_integrity_check(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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,
|
||
});
|
||
}
|
||
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::<Vec<String>>()
|
||
.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<Pager>,
|
||
_mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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");
|
||
|
||
{
|
||
let table = Arc::make_mut(&mut table);
|
||
|
||
let Table::BTree(btree) = table else {
|
||
panic!("only btree tables can be renamed");
|
||
};
|
||
|
||
let btree = Arc::make_mut(btree);
|
||
btree.name = normalized_to.to_owned();
|
||
}
|
||
|
||
schema.tables.insert(normalized_to.to_owned(), table);
|
||
});
|
||
|
||
state.pc += 1;
|
||
Ok(InsnFunctionStepResult::Step)
|
||
}
|
||
|
||
pub fn op_drop_column(
|
||
program: &Program,
|
||
state: &mut ProgramState,
|
||
insn: &Insn,
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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].is_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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
_mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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> {
|
||
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> {
|
||
self.cast_text()
|
||
.map(|s| Value::build_text(s.to_ascii_uppercase()))
|
||
}
|
||
|
||
pub fn exec_sign(&self) -> Option<Value> {
|
||
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::<String>()
|
||
.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::<String>();
|
||
|
||
// 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<Self> {
|
||
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<F>(generate_random_number: F) -> Self
|
||
where
|
||
F: Fn() -> i64,
|
||
{
|
||
Value::Integer(generate_random_number())
|
||
}
|
||
|
||
pub fn exec_randomblob<F>(&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<u8> = 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<char> = 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<String, Regex>>,
|
||
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<Item = &'a Value>>(regs: T) -> Value {
|
||
regs.min().map(|v| v.to_owned()).unwrap_or(Value::Null)
|
||
}
|
||
|
||
pub fn exec_max<'a, T: Iterator<Item = &'a Value>>(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::<Vec<_>>().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::<i64>() {
|
||
Value::Integer(i)
|
||
} else if let Ok(f) = text.parse::<f64>() {
|
||
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::<i64>().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::<i64>().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<i64> {
|
||
match self {
|
||
ParsedNumber::Integer(i) => Some(*i),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn as_float(&self) -> Option<f64> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
) -> Result<InsnFunctionStepResult> {
|
||
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<T, F>(
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
program: &Program,
|
||
f: F,
|
||
) -> Result<IOResult<T>>
|
||
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<T, F>(
|
||
pager: &Arc<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
program: &Program,
|
||
f: F,
|
||
) -> Result<IOResult<T>>
|
||
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<Pager>,
|
||
mv_store: Option<&Arc<MvStore>>,
|
||
program: &Program,
|
||
) -> Result<IOResult<u32>> {
|
||
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));
|
||
}
|
||
}
|
||
}
|