diff --git a/core/schema.rs b/core/schema.rs index a30d59dcf..eceabbcfc 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -748,6 +748,22 @@ impl Affinity { ))), } } + + pub fn to_char_code(&self) -> u8 { + self.aff_mask() as u8 + } + + pub fn from_char_code(code: u8) -> Result { + Self::from_char(code as char) + } + + pub fn is_numeric(&self) -> bool { + matches!(self, Affinity::Integer | Affinity::Real | Affinity::Numeric) + } + + pub fn has_affinity(&self) -> bool { + !matches!(self, Affinity::Blob) + } } impl fmt::Display for Type { diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 3371dbaf9..c1beb32d2 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1,4 +1,4 @@ -use limbo_sqlite3_parser::ast::{self, UnaryOperator}; +use limbo_sqlite3_parser::ast::{self, Expr, UnaryOperator}; use tracing::{instrument, Level}; use super::emitter::Resolver; @@ -8,7 +8,7 @@ use super::plan::TableReferences; use crate::function::JsonFunc; use crate::function::{Func, FuncCtx, MathFuncArity, ScalarFunc, VectorFunc}; use crate::functions::datetime; -use crate::schema::{Table, Type}; +use crate::schema::{Affinity, Table, Type}; use crate::util::{exprs_are_equivalent, normalize_ident, parse_numeric_literal}; use crate::vdbe::builder::CursorKey; use crate::vdbe::{ @@ -462,7 +462,16 @@ pub fn translate_expr( let shared_reg = program.alloc_register(); translate_expr(program, referenced_tables, e1, shared_reg, resolver)?; - emit_binary_insn(program, op, shared_reg, shared_reg, target_register)?; + emit_binary_insn( + program, + op, + shared_reg, + shared_reg, + target_register, + e1, + e2, + referenced_tables, + )?; program.reset_collation(); Ok(target_register) } else { @@ -509,7 +518,16 @@ pub fn translate_expr( }; program.set_collation(collation_ctx); - emit_binary_insn(program, op, e1_reg, e2_reg, target_register)?; + emit_binary_insn( + program, + op, + e1_reg, + e2_reg, + target_register, + e1, + e2, + referenced_tables, + )?; program.reset_collation(); Ok(target_register) } @@ -2201,7 +2219,15 @@ fn emit_binary_insn( lhs: usize, rhs: usize, target_register: usize, + lhs_expr: &Expr, + rhs_expr: &Expr, + referenced_tables: Option<&TableReferences>, ) -> Result<()> { + let mut affinity = Affinity::Blob; + if op.is_comparison() { + affinity = comparison_affinity(lhs_expr, rhs_expr, referenced_tables); + } + match op { ast::Operator::NotEquals => { let if_true_label = program.allocate_label(); @@ -2211,7 +2237,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2228,7 +2254,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2245,7 +2271,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2262,7 +2288,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2279,7 +2305,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2296,7 +2322,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2390,7 +2416,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default().null_eq(), + flags: CmpInsFlags::default().null_eq().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -2405,7 +2431,7 @@ fn emit_binary_insn( lhs, rhs, target_pc: if_true_label, - flags: CmpInsFlags::default().null_eq(), + flags: CmpInsFlags::default().null_eq().with_affinity(affinity), collation: program.curr_collation(), }, target_register, @@ -3023,3 +3049,75 @@ where Ok(()) } + +pub fn get_expr_affinity( + expr: &ast::Expr, + referenced_tables: Option<&TableReferences>, +) -> Affinity { + match expr { + ast::Expr::Column { table, column, .. } => { + if let Some(tables) = referenced_tables { + if let Some(table_ref) = tables.find_table_by_internal_id(*table) { + if let Some(col) = table_ref.get_column_at(*column) { + return col.affinity(); + } + } + } + Affinity::Blob + } + ast::Expr::Cast { type_name, .. } => { + if let Some(type_name) = type_name { + crate::schema::affinity(&type_name.name) + } else { + Affinity::Blob + } + } + ast::Expr::Collate(expr, _) => get_expr_affinity(expr, referenced_tables), + // Literals have NO affinity in SQLite! + ast::Expr::Literal(_) => Affinity::Blob, // No affinity! + _ => Affinity::Blob, // This may need to change. For now this works. + } +} + +pub fn comparison_affinity( + lhs_expr: &ast::Expr, + rhs_expr: &ast::Expr, + referenced_tables: Option<&TableReferences>, +) -> Affinity { + let mut aff = get_expr_affinity(lhs_expr, referenced_tables); + + aff = compare_affinity(rhs_expr, aff, referenced_tables); + + // If no affinity determined (both operands are literals), default to BLOB + if !aff.has_affinity() { + Affinity::Blob + } else { + aff + } +} + +pub fn compare_affinity( + expr: &ast::Expr, + other_affinity: Affinity, + referenced_tables: Option<&TableReferences>, +) -> Affinity { + let expr_affinity = get_expr_affinity(expr, referenced_tables); + + if expr_affinity.has_affinity() && other_affinity.has_affinity() { + // Both sides have affinity - use numeric if either is numeric + if expr_affinity.is_numeric() || other_affinity.is_numeric() { + Affinity::Numeric + } else { + Affinity::Blob + } + } else { + // One or both sides have no affinity - use the one that does, or Blob if neither + if expr_affinity.has_affinity() { + expr_affinity + } else if other_affinity.has_affinity() { + other_affinity + } else { + Affinity::Blob + } + } +} diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index 8a35bb50b..17868d9a7 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -4,7 +4,7 @@ use limbo_sqlite3_parser::ast::{self, SortOrder}; use std::sync::Arc; use crate::{ - schema::{Index, IndexColumn, Table}, + schema::{Affinity, Index, IndexColumn, Table}, translate::{ plan::{DistinctCtx, Distinctness}, result_row::emit_select_result, @@ -1285,14 +1285,28 @@ fn emit_seek_termination( } program.preassign_label_to_next_insn(loop_start); let mut rowid_reg = None; + let mut affinity = None; if !is_index { rowid_reg = Some(program.alloc_register()); program.emit_insn(Insn::RowId { cursor_id: seek_cursor_id, dest: rowid_reg.unwrap(), }); - } + affinity = if let Some(table_ref) = tables + .joined_tables() + .iter() + .find(|t| t.columns().iter().any(|c| c.is_rowid_alias)) + { + if let Some(rowid_col_idx) = table_ref.columns().iter().position(|c| c.is_rowid_alias) { + Some(table_ref.columns()[rowid_col_idx].affinity()) + } else { + Some(Affinity::Numeric) + } + } else { + Some(Affinity::Numeric) + }; + } match (is_index, termination.op) { (true, SeekOp::GE { .. }) => program.emit_insn(Insn::IdxGE { cursor_id: seek_cursor_id, @@ -1322,28 +1336,36 @@ fn emit_seek_termination( lhs: rowid_reg.unwrap(), rhs: start_reg, target_pc: loop_end, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default() + .jump_if_null() + .with_affinity(affinity.unwrap()), collation: program.curr_collation(), }), (false, SeekOp::GT) => program.emit_insn(Insn::Gt { lhs: rowid_reg.unwrap(), rhs: start_reg, target_pc: loop_end, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default() + .jump_if_null() + .with_affinity(affinity.unwrap()), collation: program.curr_collation(), }), (false, SeekOp::LE { .. }) => program.emit_insn(Insn::Le { lhs: rowid_reg.unwrap(), rhs: start_reg, target_pc: loop_end, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default() + .jump_if_null() + .with_affinity(affinity.unwrap()), collation: program.curr_collation(), }), (false, SeekOp::LT) => program.emit_insn(Insn::Lt { lhs: rowid_reg.unwrap(), rhs: start_reg, target_pc: loop_end, - flags: CmpInsFlags::default(), + flags: CmpInsFlags::default() + .jump_if_null() + .with_affinity(affinity.unwrap()), collation: program.curr_collation(), }), }; diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 1e56778b7..ae0fe12ea 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -5,6 +5,7 @@ use crate::storage::database::FileMemoryStorage; use crate::storage::page_cache::DumbLruPageCache; use crate::storage::pager::CreateBTreeFlags; use crate::storage::wal::DummyWAL; +use crate::translate::collate::CollationSeq; use crate::types::ImmutableRecord; use crate::{ error::{LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_PRIMARYKEY}, @@ -531,325 +532,273 @@ pub fn op_not_null( Ok(InsnFunctionStepResult::Step) } -pub fn op_eq( +#[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: &Rc, mv_store: Option<&Rc>, ) -> Result { - let Insn::Eq { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) + 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 lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let cond = *state.registers[lhs].get_owned_value() == *state.registers[rhs].get_owned_value(); + let nulleq = flags.has_nulleq(); let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if (nulleq && cond) || (!nulleq && jump_if_null) { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_eq() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (lhs, rhs) => { - if *lhs == *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - } - Ok(InsnFunctionStepResult::Step) -} + let affinity = flags.get_affinity(); -pub fn op_ne( - program: &Program, - state: &mut ProgramState, - insn: &Insn, - pager: &Rc, - mv_store: Option<&Rc>, -) -> Result { - let Insn::Ne { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) - }; - assert!(target_pc.is_offset()); - let lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let cond = *state.registers[lhs].get_owned_value() != *state.registers[rhs].get_owned_value(); - let nulleq = flags.has_nulleq(); - let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if (nulleq && cond) || (!nulleq && jump_if_null) { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_ne() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (lhs, rhs) => { - if *lhs != *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - } - Ok(InsnFunctionStepResult::Step) -} + let lhs_value = state.registers[lhs].get_owned_value(); + let rhs_value = state.registers[rhs].get_owned_value(); -pub fn op_lt( - program: &Program, - state: &mut ProgramState, - insn: &Insn, - pager: &Rc, - mv_store: Option<&Rc>, -) -> Result { - let Insn::Lt { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) - }; - assert!(target_pc.is_offset()); - let lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if jump_if_null { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_lt() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (lhs, rhs) => { - if *lhs < *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } + // 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.to_offset_int(); + } else { + state.pc += 1; } + return Ok(InsnFunctionStepResult::Step); } - Ok(InsnFunctionStepResult::Step) -} -pub fn op_le( - program: &Program, - state: &mut ProgramState, - insn: &Insn, - pager: &Rc, - mv_store: Option<&Rc>, -) -> Result { - let Insn::Le { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) - }; - assert!(target_pc.is_offset()); - let lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if jump_if_null { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_le() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (lhs, rhs) => { - if *lhs <= *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } + // 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.to_offset_int(); + } else { + state.pc += 1; } + return Ok(InsnFunctionStepResult::Step); } - Ok(InsnFunctionStepResult::Step) -} -pub fn op_gt( - program: &Program, - state: &mut ProgramState, - insn: &Insn, - pager: &Rc, - mv_store: Option<&Rc>, -) -> Result { - let Insn::Gt { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) - }; - assert!(target_pc.is_offset()); - let lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if jump_if_null { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_gt() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - (lhs, rhs) => { - if *lhs > *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; - } - } - } - Ok(InsnFunctionStepResult::Step) -} + let mut lhs_temp_reg = state.registers[lhs].clone(); + let mut rhs_temp_reg = state.registers[rhs].clone(); -pub fn op_ge( - program: &Program, - state: &mut ProgramState, - insn: &Insn, - pager: &Rc, - mv_store: Option<&Rc>, -) -> Result { - let Insn::Ge { - lhs, - rhs, - target_pc, - flags, - collation, - } = insn - else { - unreachable!("unexpected Insn {:?}", insn) - }; - assert!(target_pc.is_offset()); - let lhs = *lhs; - let rhs = *rhs; - let target_pc = *target_pc; - let jump_if_null = flags.has_jump_if_null(); - let collation = collation.unwrap_or_default(); - match ( - state.registers[lhs].get_owned_value(), - state.registers[rhs].get_owned_value(), - ) { - (_, Value::Null) | (Value::Null, _) => { - if jump_if_null { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; + let mut lhs_converted = false; + let mut rhs_converted = false; + + // Apply affinity conversions + match affinity { + Affinity::Numeric | Affinity::Integer => { + let lhs_is_text = matches!(lhs_temp_reg.get_owned_value(), Value::Text(_)); + let rhs_is_text = matches!(rhs_temp_reg.get_owned_value(), Value::Text(_)); + + if lhs_is_text || rhs_is_text { + if lhs_is_text { + lhs_converted = apply_numeric_affinity(&mut lhs_temp_reg, false); + } + if rhs_is_text { + rhs_converted = apply_numeric_affinity(&mut rhs_temp_reg, false); + } } } - (Value::Text(lhs), Value::Text(rhs)) => { - let order = collation.compare_strings(lhs.as_str(), rhs.as_str()); - if order.is_ge() { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; + + Affinity::Text => { + let lhs_is_text = matches!(lhs_temp_reg.get_owned_value(), Value::Text(_)); + let rhs_is_text = matches!(rhs_temp_reg.get_owned_value(), Value::Text(_)); + + if lhs_is_text || rhs_is_text { + if is_numeric_value(&lhs_temp_reg) { + lhs_converted = stringify_register(&mut lhs_temp_reg); + } + + if is_numeric_value(&rhs_temp_reg) { + rhs_converted = stringify_register(&mut rhs_temp_reg); + } } } - (lhs, rhs) => { - if *lhs >= *rhs { - state.pc = target_pc.to_offset_int(); - } else { - state.pc += 1; + + Affinity::Real => { + if matches!(lhs_temp_reg.get_owned_value(), Value::Text(_)) { + lhs_converted = apply_numeric_affinity(&mut lhs_temp_reg, false); + } + + if matches!(rhs_temp_reg.get_owned_value(), Value::Text(_)) { + rhs_converted = apply_numeric_affinity(&mut rhs_temp_reg, false); + } + + if let Value::Integer(i) = lhs_temp_reg.get_owned_value() { + lhs_temp_reg = Register::Value(Value::Float(*i as f64)); + lhs_converted = true; + } + + if let Value::Integer(i) = rhs_temp_reg.get_owned_value() { + rhs_temp_reg = 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.get_owned_value(), + rhs_temp_reg.get_owned_value(), + &collation, + ); + + if lhs_converted { + state.registers[lhs] = lhs_temp_reg; + } + + if rhs_converted { + state.registers[rhs] = rhs_temp_reg; + } + + if should_jump { + state.pc = target_pc.to_offset_int(); + } else { + state.pc += 1; + } + Ok(InsnFunctionStepResult::Step) } @@ -2004,13 +1953,22 @@ pub fn op_seek_rowid( let rowid = match state.registers[*src_reg].get_owned_value() { Value::Integer(rowid) => Some(*rowid), Value::Null => None, + // For non-integer values try to apply affinity and convert them to integer. other => { - return Err(LimboError::InternalError(format!( - "SeekRowid: the value in the register is not an integer or NULL: {}", - 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_owned_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 found = return_if_io!( @@ -2125,34 +2083,100 @@ pub fn op_seek( } } else { let pc = { - let mut cursor = state.get_cursor(*cursor_id); - let cursor = cursor.as_btree_mut(); - let rowid = match state.registers[*start_reg].get_owned_value() { - Value::Null => { - // All integer values are greater than null so we just rewind the cursor - return_if_io!(cursor.rewind()); - None - } - Value::Integer(rowid) => Some(*rowid), - _ => { - return Err(LimboError::InternalError(format!( - "{}: the value in the register is not an integer", - op_name - ))); - } + let original_value = state.registers[*start_reg].get_owned_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_owned_value().clone(); + converted + } else { + true // Non-text values don't need conversion }; - let found = match rowid { - Some(rowid) => { - let found = return_if_io!(cursor.seek(SeekKey::TableRowId(rowid), op)); - if !found { - target_pc.to_offset_int() - } else { - state.pc + 1 + + 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 + }; + + if c > 0 { + // If approximation is larger than actual search term + 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, + } + } else if c < 0 { + // If approximation is smaller than actual search term + 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, + } + } else { + op + } + } + Value::Text(_) | Value::Blob(_) => { + match op { + SeekOp::GT | SeekOp::GE { .. } => { + // No integers are > or >= non-numeric text, jump to target (empty result) + state.pc = target_pc.to_offset_int(); + return Ok(InsnFunctionStepResult::Step); + } + SeekOp::LT | SeekOp::LE { .. } => { + // All integers are < or <= non-numeric text + // Move to last position and then use the normal seek logic + { + let mut cursor = state.get_cursor(*cursor_id); + let cursor = cursor.as_btree_mut(); + return_if_io!(cursor.last()); + } + state.pc += 1; + return Ok(InsnFunctionStepResult::Step); + } + } + } + _ => op, + } + } else { + op + }; + + let rowid = if matches!(original_value, Value::Null) { + match actual_op { + SeekOp::GE { .. } | SeekOp::GT => { + state.pc = target_pc.to_offset_int(); + return Ok(InsnFunctionStepResult::Step); + } + SeekOp::LE { .. } | SeekOp::LT => { + // No integers are < NULL, so jump to target + state.pc = target_pc.to_offset_int(); + return Ok(InsnFunctionStepResult::Step); } } - None => state.pc + 1, + } else { + int_key }; - found + let mut cursor = state.get_cursor(*cursor_id); + let cursor = cursor.as_btree_mut(); + let found = return_if_io!(cursor.seek(SeekKey::TableRowId(rowid), actual_op)); + + if !found { + target_pc.to_offset_int() + } else { + state.pc + 1 + } }; state.pc = pc; } @@ -5951,8 +5975,10 @@ fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { if matches!(value, Value::Blob(_)) { return true; } + match affinity { Affinity::Blob => return true, + Affinity::Text => { if matches!(value, Value::Text(_) | Value::Null) { return true; @@ -5961,6 +5987,7 @@ fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { *value = Value::Text(text.into()); return true; } + Affinity::Integer | Affinity::Numeric => { if matches!(value, Value::Integer(_)) { return true; @@ -5970,49 +5997,88 @@ fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { } if let Value::Float(fl) = *value { - if let Ok(int) = cast_real_to_integer(fl).map(Value::Integer) { - *value = int; - return true; - } - return false; + // For floats, try to convert to integer if it's exact + // This is similar to sqlite3VdbeIntegerAffinity + return try_float_to_integer_affinity(value, fl); } - let text = value.to_text().unwrap(); - let Ok(num) = checked_cast_text_to_numeric(&text) else { - return false; - }; + if let Value::Text(t) = value { + let text = t.as_str(); - *value = match &num { - Value::Float(fl) => { - cast_real_to_integer(*fl).map(Value::Integer).unwrap_or(num); - return true; - } - Value::Integer(_) if text.starts_with("0x") => { + // Handle hex numbers - they shouldn't be converted + if text.starts_with("0x") { return false; } - _ => num, - }; + + // Try to parse as number (similar to applyNumericAffinity) + let Ok(num) = checked_cast_text_to_numeric(text) 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); + if let Value::Integer(i) = *value { + *value = Value::Float(i as f64); return true; - } else if let Value::Text(t) = value { - if t.as_str().starts_with("0x") { + } + 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(t.as_str()) { + if let Ok(num) = checked_cast_text_to_numeric(s) { *value = num; return true; } else { return false; } } + return true; } - }; + } } - 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 } fn execute_sqlite_version(version_integer: i64) -> String { @@ -6023,10 +6089,415 @@ fn execute_sqlite_version(version_integer: i64) -> String { format!("{}.{}.{}", major, minor, release) } +pub fn extract_int_value(value: &Value) -> i64 { + match value { + Value::Integer(i) => *i, + Value::Float(f) => { + // Use sqlite3RealToI64 equivalent + if *f < -9223372036854774784.0 { + i64::MIN + } else if *f > 9223372036854774784.0 { + i64::MAX + } else { + *f as i64 + } + } + Value::Text(t) => { + // Try to parse as integer, return 0 if failed + t.as_str().parse::().unwrap_or(0) + } + Value::Blob(b) => { + // Try to parse blob as string then as integer + if let Ok(s) = std::str::from_utf8(b) { + s.parse::().unwrap_or(0) + } else { + 0 + } + } + Value::Null => 0, + } +} + +#[derive(Debug, PartialEq)] +enum NumericParseResult { + NotNumeric, // not a valid number + PureInteger, // pure integer (entire string) + HasDecimalOrExp, // has decimal point or exponent (entire string) + ValidPrefixOnly, // valid prefix but not entire string +} + +#[derive(Debug)] +enum ParsedNumber { + None, + Integer(i64), + Float(f64), +} + +impl ParsedNumber { + fn as_integer(&self) -> Option { + match self { + ParsedNumber::Integer(i) => Some(*i), + _ => None, + } + } + + fn as_float(&self) -> Option { + match self { + ParsedNumber::Float(f) => Some(*f), + _ => None, + } + } +} + +fn try_for_float(text: &str) -> (NumericParseResult, ParsedNumber) { + let bytes = text.as_bytes(); + if bytes.is_empty() { + return (NumericParseResult::NotNumeric, ParsedNumber::None); + } + + let mut pos = 0; + let len = bytes.len(); + + while pos < len && is_space(bytes[pos]) { + pos += 1; + } + + if pos >= len { + return (NumericParseResult::NotNumeric, ParsedNumber::None); + } + + let start_pos = pos; + + let mut sign = 1i64; + + if bytes[pos] == b'-' { + sign = -1; + pos += 1; + } else if bytes[pos] == b'+' { + pos += 1; + } + + if pos >= len { + return (NumericParseResult::NotNumeric, ParsedNumber::None); + } + + let mut significand = 0u64; + let mut digit_count = 0; + let mut decimal_adjust = 0i32; + let mut has_digits = false; + + // Parse digits before decimal point + while pos < len && bytes[pos].is_ascii_digit() { + has_digits = true; + let digit = (bytes[pos] - b'0') as u64; + + if significand <= (u64::MAX - 9) / 10 { + significand = significand * 10 + digit; + digit_count += 1; + } else { + // Skip overflow digits but adjust exponent + decimal_adjust += 1; + } + pos += 1; + } + + let mut has_decimal = false; + let mut has_exponent = false; + + // Check for decimal point + if pos < len && bytes[pos] == b'.' { + has_decimal = true; + pos += 1; + + // Parse fractional digits + while pos < len && bytes[pos].is_ascii_digit() { + has_digits = true; + let digit = (bytes[pos] - b'0') as u64; + + if significand <= (u64::MAX - 9) / 10 { + significand = significand * 10 + digit; + digit_count += 1; + decimal_adjust -= 1; + } + pos += 1; + } + } + + if !has_digits { + return (NumericParseResult::NotNumeric, ParsedNumber::None); + } + + // Check for exponent + let mut exponent = 0i32; + if pos < len && (bytes[pos] == b'e' || bytes[pos] == b'E') { + has_exponent = true; + pos += 1; + + if pos >= len { + // Incomplete exponent, but we have valid digits before + return create_result_from_significand( + significand, + sign, + decimal_adjust, + has_decimal, + has_exponent, + NumericParseResult::ValidPrefixOnly, + ); + } + + let mut exp_sign = 1i32; + if bytes[pos] == b'-' { + exp_sign = -1; + pos += 1; + } else if bytes[pos] == b'+' { + pos += 1; + } + + if pos >= len || !bytes[pos].is_ascii_digit() { + // Incomplete exponent + return create_result_from_significand( + significand, + sign, + decimal_adjust, + has_decimal, + false, + NumericParseResult::ValidPrefixOnly, + ); + } + + // Parse exponent digits + while pos < len && bytes[pos].is_ascii_digit() { + let digit = (bytes[pos] - b'0') as i32; + if exponent < 10000 { + exponent = exponent * 10 + digit; + } else { + exponent = 10000; // Cap at large value + } + pos += 1; + } + exponent *= exp_sign; + } + + // Skip trailing whitespace + while pos < len && is_space(bytes[pos]) { + pos += 1; + } + + // Determine if we consumed the entire string + let consumed_all = pos >= len; + let final_exponent = decimal_adjust + exponent; + + let parse_result = if !consumed_all { + NumericParseResult::ValidPrefixOnly + } else if has_decimal || has_exponent { + NumericParseResult::HasDecimalOrExp + } else { + NumericParseResult::PureInteger + }; + + create_result_from_significand( + significand, + sign, + final_exponent, + has_decimal, + has_exponent, + parse_result, + ) +} + +fn create_result_from_significand( + significand: u64, + sign: i64, + exponent: i32, + has_decimal: bool, + has_exponent: bool, + parse_result: NumericParseResult, +) -> (NumericParseResult, ParsedNumber) { + if significand == 0 { + match parse_result { + NumericParseResult::PureInteger => { + return (parse_result, ParsedNumber::Integer(0)); + } + _ => { + return (parse_result, ParsedNumber::Float(0.0)); + } + } + } + + // For pure integers without exponent, try to return as integer + if !has_decimal && !has_exponent && exponent == 0 { + let signed_val = (significand as i64).wrapping_mul(sign); + if (significand as i64) * sign == signed_val { + return (parse_result, ParsedNumber::Integer(signed_val)); + } + } + + // Convert to float + let mut result = significand as f64; + + let mut exp = exponent; + if exp > 0 { + while exp >= 100 { + result *= 1e100; + exp -= 100; + } + while exp >= 10 { + result *= 1e10; + exp -= 10; + } + while exp >= 1 { + result *= 10.0; + exp -= 1; + } + } else if exp < 0 { + while exp <= -100 { + result *= 1e-100; + exp += 100; + } + while exp <= -10 { + result *= 1e-10; + exp += 10; + } + while exp <= -1 { + result *= 0.1; + exp += 1; + } + } + + 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 { + 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_owned_value(), Value::Integer(_) | Value::Float(_)) +} + +fn stringify_register(reg: &mut Register) -> bool { + match reg.get_owned_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, + } +} + #[cfg(test)] mod tests { + use super::*; use crate::types::{Text, Value}; + #[test] + fn test_apply_numeric_affinity_partial_numbers() { + let mut reg = Register::Value(Value::Text(Text::from_str("123abc"))); + assert!(!apply_numeric_affinity(&mut reg, false)); + assert!(matches!(reg, Register::Value(Value::Text(_)))); + + let mut reg = Register::Value(Value::Text(Text::from_str("-53093015420544-15062897"))); + assert!(!apply_numeric_affinity(&mut reg, false)); + assert!(matches!(reg, Register::Value(Value::Text(_)))); + + let mut reg = Register::Value(Value::Text(Text::from_str("123.45xyz"))); + 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(Text::from_str("123"))); + assert!(apply_numeric_affinity(&mut reg, false)); + assert_eq!(*reg.get_owned_value(), Value::Integer(123)); + + let mut reg = Register::Value(Value::Text(Text::from_str("123.45"))); + assert!(apply_numeric_affinity(&mut reg, false)); + assert_eq!(*reg.get_owned_value(), Value::Float(123.45)); + + let mut reg = Register::Value(Value::Text(Text::from_str(" -456 "))); + assert!(apply_numeric_affinity(&mut reg, false)); + assert_eq!(*reg.get_owned_value(), Value::Integer(-456)); + + let mut reg = Register::Value(Value::Text(Text::from_str("0"))); + assert!(apply_numeric_affinity(&mut reg, false)); + assert_eq!(*reg.get_owned_value(), Value::Integer(0)); + } + #[test] fn test_exec_add() { let inputs = vec![ diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 23b9da480..db319f936 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -6,7 +6,7 @@ use std::{ use super::{execute, AggFunc, BranchOffset, CursorID, FuncCtx, InsnFunction, PageIdx}; use crate::{ - schema::{BTreeTable, Index}, + schema::{Affinity, BTreeTable, Index}, storage::{pager::CreateBTreeFlags, wal::CheckpointMode}, translate::collate::CollationSeq, }; @@ -20,6 +20,7 @@ pub struct CmpInsFlags(usize); impl CmpInsFlags { const NULL_EQ: usize = 0x80; const JUMP_IF_NULL: usize = 0x10; + const AFFINITY_MASK: usize = 0x47; fn has(&self, flag: usize) -> bool { (self.0 & flag) != 0 @@ -42,6 +43,17 @@ impl CmpInsFlags { pub fn has_nulleq(&self) -> bool { self.has(CmpInsFlags::NULL_EQ) } + + pub fn with_affinity(mut self, affinity: Affinity) -> Self { + let aff_code = affinity.to_char_code() as usize; + self.0 = (self.0 & !Self::AFFINITY_MASK) | aff_code; + self + } + + pub fn get_affinity(&self) -> Affinity { + let aff_code = (self.0 & Self::AFFINITY_MASK) as u8; + Affinity::from_char_code(aff_code).unwrap_or(Affinity::Blob) + } } #[derive(Clone, Copy, Debug, Default)] @@ -939,12 +951,12 @@ impl Insn { Insn::Move { .. } => execute::op_move, Insn::IfPos { .. } => execute::op_if_pos, Insn::NotNull { .. } => execute::op_not_null, - Insn::Eq { .. } => execute::op_eq, - Insn::Ne { .. } => execute::op_ne, - Insn::Lt { .. } => execute::op_lt, - Insn::Le { .. } => execute::op_le, - Insn::Gt { .. } => execute::op_gt, - Insn::Ge { .. } => execute::op_ge, + Insn::Eq { .. } + | Insn::Ne { .. } + | Insn::Lt { .. } + | Insn::Le { .. } + | Insn::Gt { .. } + | Insn::Ge { .. } => execute::op_comparison, Insn::If { .. } => execute::op_if, Insn::IfNot { .. } => execute::op_if_not, Insn::OpenRead { .. } => execute::op_open_read, @@ -980,10 +992,10 @@ impl Insn { Insn::IdxRowId { .. } => execute::op_idx_row_id, Insn::SeekRowid { .. } => execute::op_seek_rowid, Insn::DeferredSeek { .. } => execute::op_deferred_seek, - Insn::SeekGE { .. } => execute::op_seek, - Insn::SeekGT { .. } => execute::op_seek, - Insn::SeekLE { .. } => execute::op_seek, - Insn::SeekLT { .. } => execute::op_seek, + Insn::SeekGE { .. } + | Insn::SeekGT { .. } + | Insn::SeekLE { .. } + | Insn::SeekLT { .. } => execute::op_seek, Insn::SeekEnd { .. } => execute::op_seek_end, Insn::IdxGE { .. } => execute::op_idx_ge, Insn::IdxGT { .. } => execute::op_idx_gt, diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index 9f5437dad..55b8c3a25 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -51,7 +51,7 @@ mod tests { let insert = format!( "INSERT INTO t VALUES {}", - (1..2000) + (1..100) .map(|x| format!("({})", x)) .collect::>() .join(", ") @@ -69,28 +69,101 @@ mod tests { Some("ORDER BY x ASC"), ]; - for comp in COMPARISONS.iter() { - for order_by in ORDER_BY.iter() { - for max in 0..=2000 { - let query = format!( - "SELECT * FROM t WHERE x {} {} {}", - comp, - max, - order_by.unwrap_or("") - ); - log::trace!("query: {}", query); - let limbo = limbo_exec_rows(&db, &limbo_conn, &query); - let sqlite = sqlite_exec_rows(&sqlite_conn, &query); - assert_eq!( - limbo, sqlite, - "query: {}, limbo: {:?}, sqlite: {:?}", - query, limbo, sqlite - ); + let (mut rng, seed) = rng_from_time_or_env(); + tracing::info!("rowid_seek_fuzz seed: {}", seed); + + for iteration in 0..2 { + tracing::trace!("rowid_seek_fuzz iteration: {}", iteration); + + for comp in COMPARISONS.iter() { + for order_by in ORDER_BY.iter() { + let test_values = generate_random_comparison_values(&mut rng); + + for test_value in test_values.iter() { + let query = format!( + "SELECT * FROM t WHERE x {} {} {}", + comp, + test_value, + order_by.unwrap_or("") + ); + + log::trace!("query: {}", query); + let limbo_result = limbo_exec_rows(&db, &limbo_conn, &query); + let sqlite_result = sqlite_exec_rows(&sqlite_conn, &query); + assert_eq!( + limbo_result, sqlite_result, + "query: {}, limbo: {:?}, sqlite: {:?}, seed: {}", + query, limbo_result, sqlite_result, seed + ); + } } } } } + fn generate_random_comparison_values(rng: &mut ChaCha8Rng) -> Vec { + let mut values = Vec::new(); + + for _ in 0..1000 { + let val = rng.random_range(-10000..10000); + values.push(val.to_string()); + } + + values.push(i64::MAX.to_string()); + values.push(i64::MIN.to_string()); + values.push("0".to_string()); + + for _ in 0..5 { + let val: f64 = rng.random_range(-10000.0..10000.0); + values.push(val.to_string()); + } + + values.push("NULL".to_string()); // Man's greatest mistake + values.push("'NULL'".to_string()); // SQLite dared to one up on that mistake + values.push("0.0".to_string()); + values.push("-0.0".to_string()); + values.push("1.5".to_string()); + values.push("-1.5".to_string()); + values.push("999.999".to_string()); + + values.push("'text'".to_string()); + values.push("'123'".to_string()); + values.push("''".to_string()); + values.push("'0'".to_string()); + values.push("'hello'".to_string()); + + values.push("'0x10'".to_string()); + values.push("'+123'".to_string()); + values.push("' 123 '".to_string()); + values.push("'1.5e2'".to_string()); + values.push("'inf'".to_string()); + values.push("'-inf'".to_string()); + values.push("'nan'".to_string()); + + values.push("X'41'".to_string()); + values.push("X''".to_string()); + + values.push("(1 + 1)".to_string()); + // values.push("(SELECT 1)".to_string()); subqueries ain't implemented yet homes. + + values + } + + fn rng_from_time_or_env() -> (ChaCha8Rng, u64) { + let seed = std::env::var("SEED").map_or( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + |v| { + v.parse() + .expect("Failed to parse SEED environment variable as u64") + }, + ); + let rng = ChaCha8Rng::seed_from_u64(seed); + (rng, seed) + } + #[test] pub fn index_scan_fuzz() { let db = TempDatabase::new_with_rusqlite("CREATE TABLE t(x PRIMARY KEY)"); diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 7512b75d5..6d92c07ba 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -731,6 +731,21 @@ impl Operator { | Operator::NotEquals ) } + + /// Returns true if this operator is a comparison operator that may need affinity conversion + pub fn is_comparison(&self) -> bool { + matches!( + self, + Self::Equals + | Self::NotEquals + | Self::Less + | Self::LessEquals + | Self::Greater + | Self::GreaterEquals + | Self::Is + | Self::IsNot + ) + } } /// Unary operators