diff --git a/core/storage/btree.rs b/core/storage/btree.rs index 081c229ed..b7dcaa5b2 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -2198,7 +2198,6 @@ impl BTreeCursor { SeekOp::LE { eq_only: false } => cmp.is_le(), SeekOp::LT => cmp.is_lt(), }; - (cmp, found) } diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index be98f6476..14424ea7f 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -27,6 +27,7 @@ use crate::{ }, types::SeekOp, vdbe::{ + affinity, builder::{CursorKey, CursorType, ProgramBuilder}, insn::{CmpInsFlags, IdxInsertFlags, Insn}, BranchOffset, CursorID, @@ -1375,6 +1376,24 @@ fn emit_seek( } } let num_regs = seek_def.size(&seek_def.start); + + if is_index { + let affinities: String = seek_def + .iter_affinity(&seek_def.start) + .map(|affinity| affinity.aff_mask()) + .collect(); + if affinities.chars().any(|c| c != affinity::SQLITE_AFF_NONE) { + program.emit_insn(Insn::Affinity { + start_reg, + count: std::num::NonZeroUsize::new(num_regs).unwrap(), + affinities: seek_def + .iter_affinity(&seek_def.start) + .map(|affinity| affinity.aff_mask()) + .collect(), + }); + } + } + match seek_def.start.op { SeekOp::GE { eq_only } => program.emit_insn(Insn::SeekGE { is_index, diff --git a/core/translate/optimizer/constraints.rs b/core/translate/optimizer/constraints.rs index 21fe6407a..4e6b6bbd0 100644 --- a/core/translate/optimizer/constraints.rs +++ b/core/translate/optimizer/constraints.rs @@ -8,11 +8,12 @@ use crate::{ schema::{Column, Index}, translate::{ collate::get_collseq_from_expr, - expr::as_binary_components, + expr::{as_binary_components, comparison_affinity}, plan::{JoinOrderMember, NonFromClauseSubquery, TableReferences, WhereTerm}, planner::{table_mask_from_expr, TableMask}, }, util::exprs_are_equivalent, + vdbe::affinity::Affinity, Result, }; use turso_ext::{ConstraintInfo, ConstraintOp}; @@ -68,16 +69,31 @@ pub enum BinaryExprSide { impl Constraint { /// Get the constraining expression and operator, e.g. ('>=', '2+3') from 't.x >= 2+3' - pub fn get_constraining_expr(&self, where_clause: &[WhereTerm]) -> (ast::Operator, ast::Expr) { + pub fn get_constraining_expr( + &self, + where_clause: &[WhereTerm], + referenced_tables: Option<&TableReferences>, + ) -> (ast::Operator, ast::Expr, Affinity) { let (idx, side) = self.where_clause_pos; let where_term = &where_clause[idx]; - let Ok(Some((lhs, _, rhs))) = as_binary_components(&where_term.expr) else { + let Ok(Some((lhs, op, rhs))) = as_binary_components(&where_term.expr) else { panic!("Expected a valid binary expression"); }; + let mut affinity = Affinity::Blob; + if op.is_comparison() { + affinity = comparison_affinity(lhs, rhs, referenced_tables); + } + if side == BinaryExprSide::Lhs { - (self.operator, lhs.clone()) + if affinity.expr_needs_no_affinity_change(lhs) { + affinity = Affinity::Blob; + } + (self.operator, lhs.clone(), affinity) } else { - (self.operator, rhs.clone()) + if affinity.expr_needs_no_affinity_change(rhs) { + affinity = Affinity::Blob; + } + (self.operator, rhs.clone(), affinity) } } @@ -416,13 +432,13 @@ pub struct RangeConstraintRef { /// Represent seek range which can be used in query planning to emit range scan over table or index pub struct SeekRangeConstraint { pub sort_order: SortOrder, - pub eq: Option<(ast::Operator, ast::Expr)>, - pub lower_bound: Option<(ast::Operator, ast::Expr)>, - pub upper_bound: Option<(ast::Operator, ast::Expr)>, + pub eq: Option<(ast::Operator, ast::Expr, Affinity)>, + pub lower_bound: Option<(ast::Operator, ast::Expr, Affinity)>, + pub upper_bound: Option<(ast::Operator, ast::Expr, Affinity)>, } impl SeekRangeConstraint { - pub fn new_eq(sort_order: SortOrder, eq: (ast::Operator, ast::Expr)) -> Self { + pub fn new_eq(sort_order: SortOrder, eq: (ast::Operator, ast::Expr, Affinity)) -> Self { Self { sort_order, eq: Some(eq), @@ -432,8 +448,8 @@ impl SeekRangeConstraint { } pub fn new_range( sort_order: SortOrder, - lower_bound: Option<(ast::Operator, ast::Expr)>, - upper_bound: Option<(ast::Operator, ast::Expr)>, + lower_bound: Option<(ast::Operator, ast::Expr, Affinity)>, + upper_bound: Option<(ast::Operator, ast::Expr, Affinity)>, ) -> Self { assert!(lower_bound.is_some() || upper_bound.is_some()); Self { @@ -451,19 +467,20 @@ impl RangeConstraintRef { &self, constraints: &[Constraint], where_clause: &[WhereTerm], + referenced_tables: Option<&TableReferences>, ) -> SeekRangeConstraint { if let Some(eq) = self.eq { return SeekRangeConstraint::new_eq( self.sort_order, - constraints[eq].get_constraining_expr(where_clause), + constraints[eq].get_constraining_expr(where_clause, referenced_tables), ); } SeekRangeConstraint::new_range( self.sort_order, self.lower_bound - .map(|x| constraints[x].get_constraining_expr(where_clause)), + .map(|x| constraints[x].get_constraining_expr(where_clause, referenced_tables)), self.upper_bound - .map(|x| constraints[x].get_constraining_expr(where_clause)), + .map(|x| constraints[x].get_constraining_expr(where_clause, referenced_tables)), ) } } diff --git a/core/translate/optimizer/mod.rs b/core/translate/optimizer/mod.rs index 6c36cb2ac..2b79c1463 100644 --- a/core/translate/optimizer/mod.rs +++ b/core/translate/optimizer/mod.rs @@ -32,7 +32,10 @@ use crate::{ util::{ exprs_are_equivalent, simple_bind_expr, try_capture_parameters, try_substitute_parameters, }, - vdbe::builder::{CursorKey, CursorType, ProgramBuilder}, + vdbe::{ + affinity::Affinity, + builder::{CursorKey, CursorType, ProgramBuilder}, + }, LimboError, Result, }; @@ -611,8 +614,6 @@ fn optimize_table_access( best_ordered_plan, } = best_join_order_result; - let joined_tables = table_references.joined_tables_mut(); - // See if best_ordered_plan is better than the overall best_plan if we add a sorting penalty // to the unordered plan's cost. let best_plan = if let Some(best_ordered_plan) = best_ordered_plan { @@ -635,7 +636,7 @@ fn optimize_table_access( let satisfies_order_target = plan_satisfies_order_target( &best_plan, &access_methods_arena, - joined_tables, + table_references.joined_tables_mut(), &order_target, ); if satisfies_order_target { @@ -662,9 +663,9 @@ fn optimize_table_access( let best_join_order: Vec = best_table_numbers .into_iter() .map(|table_number| JoinOrderMember { - table_id: joined_tables[table_number].internal_id, + table_id: table_references.joined_tables_mut()[table_number].internal_id, original_idx: table_number, - is_outer: joined_tables[table_number] + is_outer: table_references.joined_tables_mut()[table_number] .join_info .as_ref() .is_some_and(|join_info| join_info.outer), @@ -675,7 +676,6 @@ fn optimize_table_access( for (i, join_order_member) in best_join_order.iter().enumerate() { let table_idx = join_order_member.original_idx; let access_method = &access_methods_arena.borrow()[best_access_methods[i]]; - match &access_method.params { AccessMethodParams::BTreeTable { iter_dir, @@ -692,10 +692,11 @@ fn optimize_table_access( }; if !try_to_build_ephemeral_index { - joined_tables[table_idx].op = Operation::Scan(Scan::BTreeTable { - iter_dir: *iter_dir, - index: index.clone(), - }); + table_references.joined_tables_mut()[table_idx].op = + Operation::Scan(Scan::BTreeTable { + iter_dir: *iter_dir, + index: index.clone(), + }); continue; } // This branch means we have a full table scan for a non-outermost table. @@ -704,10 +705,11 @@ fn optimize_table_access( .iter() .find(|c| c.table_id == join_order_member.table_id); let Some(table_constraints) = table_constraints else { - joined_tables[table_idx].op = Operation::Scan(Scan::BTreeTable { - iter_dir: *iter_dir, - index: index.clone(), - }); + table_references.joined_tables_mut()[table_idx].op = + Operation::Scan(Scan::BTreeTable { + iter_dir: *iter_dir, + index: index.clone(), + }); continue; }; let usable_constraints = table_constraints @@ -731,26 +733,31 @@ fn optimize_table_access( &best_join_order[..=i], ); if usable_constraint_refs.is_empty() { - joined_tables[table_idx].op = Operation::Scan(Scan::BTreeTable { - iter_dir: *iter_dir, - index: index.clone(), - }); + table_references.joined_tables_mut()[table_idx].op = + Operation::Scan(Scan::BTreeTable { + iter_dir: *iter_dir, + index: index.clone(), + }); continue; } - let ephemeral_index = - ephemeral_index_build(&joined_tables[table_idx], &usable_constraint_refs); + let ephemeral_index = ephemeral_index_build( + &table_references.joined_tables_mut()[table_idx], + &usable_constraint_refs, + ); let ephemeral_index = Arc::new(ephemeral_index); - joined_tables[table_idx].op = Operation::Search(Search::Seek { - index: Some(ephemeral_index), - seek_def: build_seek_def_from_constraints( - &table_constraints.constraints, - &usable_constraint_refs, - *iter_dir, - where_clause, - )?, - }); + table_references.joined_tables_mut()[table_idx].op = + Operation::Search(Search::Seek { + index: Some(ephemeral_index), + seek_def: build_seek_def_from_constraints( + &table_constraints.constraints, + &usable_constraint_refs, + *iter_dir, + where_clause, + Some(table_references), + )?, + }); } else { - let is_outer_join = joined_tables[table_idx] + let is_outer_join = table_references.joined_tables_mut()[table_idx] .join_info .as_ref() .is_some_and(|join_info| join_info.outer); @@ -780,38 +787,42 @@ fn optimize_table_access( } } if let Some(index) = &index { - joined_tables[table_idx].op = Operation::Search(Search::Seek { - index: Some(index.clone()), - seek_def: build_seek_def_from_constraints( - &constraints_per_table[table_idx].constraints, - constraint_refs, - *iter_dir, - where_clause, - )?, - }); + table_references.joined_tables_mut()[table_idx].op = + Operation::Search(Search::Seek { + index: Some(index.clone()), + seek_def: build_seek_def_from_constraints( + &constraints_per_table[table_idx].constraints, + constraint_refs, + *iter_dir, + where_clause, + Some(table_references), + )?, + }); continue; } assert!( constraint_refs.len() == 1, "expected exactly one constraint for rowid seek, got {constraint_refs:?}" ); - joined_tables[table_idx].op = if let Some(eq) = constraint_refs[0].eq { - Operation::Search(Search::RowidEq { - cmp_expr: constraints_per_table[table_idx].constraints[eq] - .get_constraining_expr(where_clause) - .1, - }) - } else { - Operation::Search(Search::Seek { - index: None, - seek_def: build_seek_def_from_constraints( - &constraints_per_table[table_idx].constraints, - constraint_refs, - *iter_dir, - where_clause, - )?, - }) - }; + table_references.joined_tables_mut()[table_idx].op = + if let Some(eq) = constraint_refs[0].eq { + Operation::Search(Search::RowidEq { + cmp_expr: constraints_per_table[table_idx].constraints[eq] + .get_constraining_expr(where_clause, Some(table_references)) + .1, + }) + } else { + Operation::Search(Search::Seek { + index: None, + seek_def: build_seek_def_from_constraints( + &constraints_per_table[table_idx].constraints, + constraint_refs, + *iter_dir, + where_clause, + Some(table_references), + )?, + }) + }; } } AccessMethodParams::VirtualTable { @@ -820,17 +831,19 @@ fn optimize_table_access( constraints, constraint_usages, } => { - joined_tables[table_idx].op = build_vtab_scan_op( + table_references.joined_tables_mut()[table_idx].op = build_vtab_scan_op( where_clause, &constraints_per_table[table_idx], idx_num, idx_str, constraints, constraint_usages, + Some(table_references), )?; } AccessMethodParams::Subquery => { - joined_tables[table_idx].op = Operation::Scan(Scan::Subquery); + table_references.joined_tables_mut()[table_idx].op = + Operation::Scan(Scan::Subquery); } } } @@ -845,6 +858,7 @@ fn build_vtab_scan_op( idx_str: &Option, vtab_constraints: &[ConstraintInfo], constraint_usages: &[ConstraintUsage], + referenced_tables: Option<&TableReferences>, ) -> Result { if constraint_usages.len() != vtab_constraints.len() { return Err(LimboError::ExtensionError(format!( @@ -882,7 +896,7 @@ fn build_vtab_scan_op( if usage.omit { where_clause[constraint.where_clause_pos.0].consumed = true; } - let (_, expr) = constraint.get_constraining_expr(where_clause); + let (_, expr, _) = constraint.get_constraining_expr(where_clause, referenced_tables); constraints[zero_based_argv_index] = Some(expr); arg_count += 1; } @@ -1306,6 +1320,7 @@ pub fn build_seek_def_from_constraints( constraint_refs: &[RangeConstraintRef], iter_dir: IterationDirection, where_clause: &[WhereTerm], + referenced_tables: Option<&TableReferences>, ) -> Result { assert!( !constraint_refs.is_empty(), @@ -1314,7 +1329,7 @@ pub fn build_seek_def_from_constraints( // Extract the key values and operators let key = constraint_refs .iter() - .map(|cref| cref.as_seek_range_constraint(constraints, where_clause)) + .map(|cref| cref.as_seek_range_constraint(constraints, where_clause, referenced_tables)) .collect(); let seek_def = build_seek_def(iter_dir, key)?; @@ -1365,10 +1380,12 @@ fn build_seek_def( start: SeekKey { last_component: SeekKeyComponent::None, op: start_op, + affinity: Affinity::Blob, }, end: SeekKey { last_component: SeekKeyComponent::None, op: end_op, + affinity: Affinity::Blob, }, }); } @@ -1392,46 +1409,52 @@ fn build_seek_def( let start = match last.lower_bound { // Forwards, Asc, GT: (x=10 AND y>20) // Start key: start from the first GT(x:10, y:20) - Some((ast::Operator::Greater, bound)) => SeekKey { + Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, + affinity, }, // Forwards, Asc, GE: (x=10 AND y>=20) // Start key: start from the first GE(x:10, y:20) - Some((ast::Operator::GreaterEquals, bound)) => SeekKey { + Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, + affinity, }, // Forwards, Asc, None, (x=10 AND y<30) // Start key: start from the first GE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GE { eq_only: false }, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.upper_bound { // Forwards, Asc, LT, (x=10 AND y<30) // End key: end at first GE(x:10, y:30) - Some((ast::Operator::Less, bound)) => SeekKey { + Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, + affinity, }, // Forwards, Asc, LE, (x=10 AND y<=30) // End key: end at first GT(x:10, y:30) - Some((ast::Operator::LessEquals, bound)) => SeekKey { + Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, + affinity, }, // Forwards, Asc, None, (x=10 AND y>20) // End key: end at first GT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GT, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; @@ -1441,46 +1464,52 @@ fn build_seek_def( let start = match last.upper_bound { // Forwards, Desc, LT: (x=10 AND y<30) // Start key: start from the first GT(x:10, y:30) - Some((ast::Operator::Less, bound)) => SeekKey { + Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, + affinity, }, // Forwards, Desc, LE: (x=10 AND y<=30) // Start key: start from the first GE(x:10, y:30) - Some((ast::Operator::LessEquals, bound)) => SeekKey { + Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, + affinity, }, // Forwards, Desc, None: (x=10 AND y>20) // Start key: start from the first GE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GE { eq_only: false }, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.lower_bound { // Forwards, Asc, GT, (x=10 AND y>20) // End key: end at first GE(x:10, y:20) - Some((ast::Operator::Greater, bound)) => SeekKey { + Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, + affinity, }, // Forwards, Asc, GE, (x=10 AND y>=20) // End key: end at first GT(x:10, y:20) - Some((ast::Operator::GreaterEquals, bound)) => SeekKey { + Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, + affinity, }, // Forwards, Asc, None, (x=10 AND y<30) // End key: end at first GT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GT, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; @@ -1500,46 +1529,52 @@ fn build_seek_def( let start = match last.upper_bound { // Backwards, Asc, LT: (x=10 AND y<30) // Start key: start from the first LT(x:10, y:30) - Some((ast::Operator::Less, bound)) => SeekKey { + Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, + affinity, }, // Backwards, Asc, LT: (x=10 AND y<=30) // Start key: start from the first LE(x:10, y:30) - Some((ast::Operator::LessEquals, bound)) => SeekKey { + Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, + affinity, }, // Backwards, Asc, None: (x=10 AND y>20) // Start key: start from the first LE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LE { eq_only: false }, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op) } }; let end = match last.lower_bound { // Backwards, Asc, GT, (x=10 AND y>20) // End key: end at first LE(x:10, y:20) - Some((ast::Operator::Greater, bound)) => SeekKey { + Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, + affinity, }, // Backwards, Asc, GT, (x=10 AND y>=20) // End key: end at first LT(x:10, y:20) - Some((ast::Operator::GreaterEquals, bound)) => SeekKey { + Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, + affinity, }, // Backwards, Asc, None, (x=10 AND y<30) // End key: end at first LT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LT, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; @@ -1549,46 +1584,52 @@ fn build_seek_def( let start = match last.lower_bound { // Backwards, Desc, LT: (x=10 AND y>20) // Start key: start from the first LT(x:10, y:20) - Some((ast::Operator::Greater, bound)) => SeekKey { + Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, + affinity, }, // Backwards, Desc, LE: (x=10 AND y>=20) // Start key: start from the first LE(x:10, y:20) - Some((ast::Operator::GreaterEquals, bound)) => SeekKey { + Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, + affinity, }, // Backwards, Desc, LE: (x=10 AND y<30) // Start key: start from the first LE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LE { eq_only: false }, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.upper_bound { // Backwards, Desc, LT, (x=10 AND y<30) // End key: end at first LE(x:10, y:30) - Some((ast::Operator::Less, bound)) => SeekKey { + Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, + affinity, }, // Backwards, Desc, LT, (x=10 AND y<=30) // End key: end at first LT(x:10, y:30) - Some((ast::Operator::LessEquals, bound)) => SeekKey { + Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, + affinity, }, // Backwards, Desc, LT, (x=10 AND y>20) // End key: end at first LT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LT, + affinity: Affinity::Blob, }, - Some((op, _)) => { + Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 7a18b63a6..775bdfa67 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::HashMap, sync::Arc}; +use std::{cmp::Ordering, collections::HashMap, marker::PhantomData, sync::Arc}; use turso_parser::ast::{ self, FrameBound, FrameClause, FrameExclude, FrameMode, SortOrder, SubqueryType, }; @@ -11,6 +11,7 @@ use crate::{ optimizer::constraints::SeekRangeConstraint, }, vdbe::{ + affinity::Affinity, builder::{CursorKey, CursorType, ProgramBuilder}, insn::{IdxInsertFlags, Insn}, BranchOffset, CursorID, @@ -1128,13 +1129,14 @@ pub struct SeekDef { pub iter_dir: IterationDirection, } -pub struct SeekDefKeyIterator<'a> { +pub struct SeekDefKeyIterator<'a, T> { seek_def: &'a SeekDef, seek_key: &'a SeekKey, pos: usize, + _t: PhantomData, } -impl<'a> Iterator for SeekDefKeyIterator<'a> { +impl<'a> Iterator for SeekDefKeyIterator<'a, SeekKeyComponent<&'a ast::Expr>> { type Item = SeekKeyComponent<&'a ast::Expr>; fn next(&mut self) -> Option { @@ -1155,6 +1157,25 @@ impl<'a> Iterator for SeekDefKeyIterator<'a> { } } +impl<'a> Iterator for SeekDefKeyIterator<'a, Affinity> { + type Item = Affinity; + + fn next(&mut self) -> Option { + let result = if self.pos < self.seek_def.prefix.len() { + Some(self.seek_def.prefix[self.pos].eq.as_ref().unwrap().2) + } else if self.pos == self.seek_def.prefix.len() { + match &self.seek_key.last_component { + SeekKeyComponent::Expr(..) => Some(self.seek_key.affinity), + SeekKeyComponent::None => None, + } + } else { + None + }; + self.pos += 1; + result + } +} + impl SeekDef { /// returns amount of values in the given seek key /// - so, for SELECT * FROM t WHERE x = 10 AND y = 20 AND y >= 30 there will be 3 values (10, 20, 30) @@ -1166,11 +1187,25 @@ impl SeekDef { } } /// iterate over value expressions in the given seek key - pub fn iter<'a>(&'a self, key: &'a SeekKey) -> SeekDefKeyIterator<'a> { + pub fn iter<'a>( + &'a self, + key: &'a SeekKey, + ) -> SeekDefKeyIterator<'a, SeekKeyComponent<&'a ast::Expr>> { SeekDefKeyIterator { seek_def: self, seek_key: key, pos: 0, + _t: PhantomData, + } + } + + /// iterate over affinity in the given seek key + pub fn iter_affinity<'a>(&'a self, key: &'a SeekKey) -> SeekDefKeyIterator<'a, Affinity> { + SeekDefKeyIterator { + seek_def: self, + seek_key: key, + pos: 0, + _t: PhantomData, } } } @@ -1196,6 +1231,9 @@ pub struct SeekKey { /// The comparison operator to use when seeking. pub op: SeekOp, + + /// Affinity of the comparison + pub affinity: Affinity, } /// Represents the type of table scan performed during query execution. diff --git a/testing/select.test b/testing/select.test index 27d7089d9..a07032109 100755 --- a/testing/select.test +++ b/testing/select.test @@ -1226,4 +1226,246 @@ do_execsql_test_on_specific_db {:memory:} collate-compound-15 { SELECT 'test' COLLATE NOCASE INTERSECT SELECT 'TEST' COLLATE NOCASE; -} {test} \ No newline at end of file +} {test} + +# Queries that use an idx and have to do an affinity conversion +do_execsql_test_on_specific_db {:memory:} affinity-conversion-1 { + CREATE TABLE t(a TEXT); + INSERT INTO t VALUES ('10'), ('2'), ('02'), ('2a'); + CREATE INDEX idx ON t(a); + SELECT * FROM t WHERE a >= 2; +} {2 +2a} + +do_execsql_test_on_specific_db {:memory:} affinity-conversion-2 { + CREATE TABLE t(a TEXT); + INSERT INTO t VALUES ('10'), ('2'), ('02'), ('2a'); + CREATE INDEX idx ON t(a); + SELECT * FROM t WHERE a == 2; +} {2} + +# Test suite for SQLite affinity conversion in WHERE clauses + +# ============================================ +# TEST 1: TEXT column with INTEGER value +# Should emit OP_Affinity to convert 2 → '2' +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-4 { + CREATE TABLE t1(a TEXT); + INSERT INTO t1 VALUES ('10'), ('2'), ('02'), ('2a'); + CREATE INDEX idx1 ON t1(a); + SELECT * FROM t1 WHERE a >= 2; +} {2 2a} + +# ============================================ +# TEST 2: TEXT column with INTEGER equality +# Should emit OP_Affinity for equality comparison +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-5 { + CREATE TABLE t2(name TEXT); + INSERT INTO t2 VALUES ('100'), ('20'), ('abc'), ('2'); + CREATE INDEX idx2 ON t2(name); + SELECT * FROM t2 WHERE name = 100; +} {100} + +# ============================================ +# TEST 3: INTEGER column with convertible string +# Should emit OP_Affinity with INTEGER affinity +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-6 { + CREATE TABLE t3(value INTEGER); + INSERT INTO t3 VALUES (100), (20), (5), (200); + CREATE INDEX idx3 ON t3(value); + SELECT * FROM t3 WHERE value >= '100'; +} {100 200} + +# ============================================ +# TEST 4: INTEGER column with non-convertible string +# String 'abc' cannot convert to integer +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-7 { + CREATE TABLE t4(value INTEGER); + INSERT INTO t4 VALUES (100), (20), (5); + CREATE INDEX idx4 ON t4(value); + SELECT * FROM t4 WHERE value >= 'abc'; +} {} + +# ============================================ +# TEST 5: NUMERIC column with integer +# Should emit OP_Affinity with NUMERIC affinity +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-8 { + CREATE TABLE t5(score NUMERIC); + INSERT INTO t5 VALUES (100), (20.5), ('30'), (45); + CREATE INDEX idx5 ON t5(score); + SELECT * FROM t5 WHERE score >= 50; +} {100} + +# ============================================ +# TEST 6: REAL column with integer +# Should emit OP_Affinity to convert to REAL +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-9 { + CREATE TABLE t6(price REAL); + INSERT INTO t6 VALUES (99.99), (19.99), (50.00), (25.50); + CREATE INDEX idx6 ON t6(price); + SELECT * FROM t6 WHERE price < 50; +} {19.99 25.5} + +# ============================================ +# TEST 7: TEXT column with REAL value +# Should emit OP_Affinity to convert 20.5 → '20.5' +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-10 { + CREATE TABLE t7(name TEXT); + INSERT INTO t7 VALUES ('100'), ('20.5'), ('abc'), ('30'); + CREATE INDEX idx7 ON t7(name); + SELECT * FROM t7 WHERE name = 20.5; +} {20.5} + +# TODO: Program does not emit correct opcodes to handle this IN query yet +# ============================================ +# TEST 8: TEXT column with IN clause +# Should emit OP_Affinity for batch conversion +# ============================================ +# do_execsql_test_on_specific_db {:memory:} affinity-conversion-11 { +# CREATE TABLE t8(name TEXT); +# INSERT INTO t8 VALUES ('1'), ('2'), ('3'), ('4'), ('abc'); +# CREATE INDEX idx8 ON t8(name); +# SELECT * FROM t8 WHERE name IN (1, 2, 3); +#} {1 2 3} + +# ============================================ +# TEST 9: Compound index with mixed types +# Should emit OP_Affinity with multi-char affinity string +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-12 { + CREATE TABLE t9(a TEXT, b INTEGER, c NUMERIC); + INSERT INTO t9 VALUES ('100', 200, 300); + INSERT INTO t9 VALUES ('50', 100, 150); + INSERT INTO t9 VALUES ('200', 300, 400); + CREATE INDEX idx9 ON t9(a, b, c); + SELECT * FROM t9 WHERE a = 100 AND b = '200' AND c >= 300; +} {100|200|300} + +# ============================================ +# TEST 10: INTEGER PRIMARY KEY range (NO affinity) +# Contrast: should NOT emit OP_Affinity +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-13 { + CREATE TABLE t10(x INTEGER PRIMARY KEY); + INSERT INTO t10 VALUES (1), (2), (100), (200); + SELECT * FROM t10 WHERE x < '100'; +} {1 2} + +# TODO: INDEXED BY not supported yet +# ============================================ +# TEST 11: Same query but forcing index usage +# Should emit OP_Affinity (takes indexed path) +# ============================================ +# do_execsql_test_on_specific_db {:memory:} affinity-conversion-14 { +# CREATE TABLE t11(x INTEGER PRIMARY KEY); +# INSERT INTO t11 VALUES (1), (2), (100), (200); +# CREATE INDEX idx11 ON t11(x); +# SELECT * FROM t11 INDEXED BY idx11 WHERE x < '100'; +# } {1 2} + +# ============================================ +# TEST 12: TEXT column with string that looks numeric +# Should apply TEXT affinity and use lexicographic order +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-15 { + CREATE TABLE t12(name TEXT); + INSERT INTO t12 VALUES ('1'), ('10'), ('2'), ('20'); + CREATE INDEX idx12 ON t12(name); + SELECT * FROM t12 WHERE name >= '2' ORDER BY name; +} {2 20} + +# ============================================ +# TEST 13: INTEGER column with float string +# Should convert '50.5' to 50 (INTEGER affinity) +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-16 { + CREATE TABLE t13(value INTEGER); + INSERT INTO t13 VALUES (50), (51), (100); + CREATE INDEX idx13 ON t13(value); + SELECT * FROM t13 WHERE value >= '50.5'; +} {51 100} + +# ============================================ +# TEST 14: NUMERIC with text that converts +# Should apply NUMERIC affinity +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-17 { + CREATE TABLE t14(score NUMERIC); + INSERT INTO t14 VALUES (10), (20), (30), (40); + CREATE INDEX idx14 ON t14(score); + SELECT * FROM t14 WHERE score BETWEEN '15' AND '35'; +} {20 30} + +# ============================================ +# TEST 15: Multiple columns, only one needs conversion +# Should emit affinity string with mixed affinities +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-18 { + CREATE TABLE t15(a INTEGER, b TEXT); + INSERT INTO t15 VALUES (1, '100'), (2, '200'), (3, '300'); + CREATE INDEX idx15 ON t15(a, b); + SELECT * FROM t15 WHERE a = '2' AND b = 200; +} {2|200} + +# ============================================ +# TEST 16: BLOB column (should not convert) +# BLOB affinity doesn't perform conversions +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-19 { + CREATE TABLE t16(data BLOB); + INSERT INTO t16 VALUES (X'48656c6c6f'), (X'576f726c64'); + CREATE INDEX idx16 ON t16(data); + SELECT typeof(data) FROM t16 WHERE data >= X'48'; +} {blob blob} + +# ============================================ +# TEST 17: Negative numbers with TEXT affinity +# Should convert -5 to '-5' for comparison +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-20 { + CREATE TABLE t17(name TEXT); + INSERT INTO t17 VALUES ('-10'), ('-5'), ('0'), ('5'); + CREATE INDEX idx17 ON t17(name); + SELECT * FROM t17 WHERE name >= -5; +} {-5 0 5} + +# ============================================ +# TEST 18: Zero with different types +# Tests affinity with special value zero +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-21 { + CREATE TABLE t18(value NUMERIC); + INSERT INTO t18 VALUES (0), (0.0), ('0'), (1), (-1); + CREATE INDEX idx18 ON t18(value); + SELECT * FROM t18 WHERE value = 0; +} {0 0 0} + +# ============================================ +# TEST 19: Large numbers requiring conversion +# Tests affinity (B) with large integer values +# ============================================ +do_execsql_test_on_specific_db {:memory:} affinity-conversion-22 { + CREATE TABLE t19(val TEXT); + INSERT INTO t19 VALUES ('1000000'), ('999999'), ('1000001'); + CREATE INDEX idx19 ON t19(val); + SELECT * FROM t19 WHERE val = 1000000; +} {1000000} + +# TODO: cannot use expressions yet in CREATE INDEX +# ============================================ +# TEST 20: Mixed case with expression index +# Expression index with affinity conversion +# ============================================ +# do_execsql_test_on_specific_db {:memory:} affinity-conversion-23 { +# CREATE TABLE t20(name TEXT); +# INSERT INTO t20 VALUES ('ABC'), ('abc'), ('123'); +# CREATE INDEX idx20 ON t20(lower(name)); +# SELECT * FROM t20 WHERE lower(name) = 'abc'; +# } {ABC abc}