From f0aa88a763317b3223746b183b087bc5d0bc3e3b Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 01/10] feat: support uncorrelated quantified subqueries in SELECT Share MarkApply preparation between WHERE and SELECT consumers, bind marker output positions through an expression iterator, and remove placeholder predicates. Reject UPDATE targets that are not base tables and cover view writes. --- src/binder/expr.rs | 12 +++++-- src/binder/parser.rs | 3 ++ src/binder/select.rs | 79 ++++++++++++++++++++++++++------------------ tests/slt/view.slt | 11 ++++++ 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 053dbecd..29df7e87 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -302,9 +302,17 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> let (sub_query, column, correlated) = self.bind_subquery_plan_with_output(Some(&left_ty), arena, build)?; - if !self.context.is_step(&QueryBindStep::Where) { + if !matches!( + self.context.step_now(), + QueryBindStep::Where | QueryBindStep::Project + ) { + return Err(DatabaseError::UnsupportedStmt( + "quantified subqueries require WHERE or SELECT list".into(), + )); + } + if correlated && self.context.is_step(&QueryBindStep::Project) { return Err(DatabaseError::UnsupportedStmt( - "quantified subqueries can only appear in `WHERE`".to_string(), + "correlated quantified subqueries in SELECT are not supported yet".into(), )); } diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 2dbafd27..0f30949c 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -1143,6 +1143,9 @@ where if let TableFactor::Table { name, .. } = &to.relation { let is_joined_update = !to.joins.is_empty(); let table_name = sql_table_name(name.clone())?; + if self.binder.context.table(table_name.clone())?.is_none() { + return Err(DatabaseError::TableNotFound); + } self.binder.with_pk(table_name.clone()); let mut plan = self.binder.bind_table_ref_sql(to, self.arena)?; diff --git a/src/binder/select.rs b/src/binder/select.rs index 4cb64dc1..75375421 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -647,6 +647,15 @@ where &[QueryBindStep::Project, QueryBindStep::Sort], self.arena, )?; + self.binder.context.step(QueryBindStep::Project); + let marker_consumers = self.select_list.iter_mut().chain( + self.orderby + .iter_mut() + .flat_map(|fields| fields.iter_mut().map(|field| &mut field.expr)), + ); + self.plan = self + .binder + .bind_mark_queries(self.plan, marker_consumers, self.arena)?; self.plan = self.binder.bind_window( self.plan, &mut self.select_list, @@ -1434,7 +1443,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b pub(crate) fn bind_where_expr( &mut self, mut children: LogicalPlan, - predicate: ExprRef, + mut predicate: ExprRef, arena: &mut PlanArena, ) -> Result { children = self.bind_scalar_queries(children, &[QueryBindStep::Where], arena)?; @@ -1445,6 +1454,32 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b )); } + if self.context.sub_queries.contains_key(&QueryBindStep::Where) { + children = self.bind_mark_queries(children, std::iter::once(&mut predicate), arena)?; + let passthrough_exprs = children + .output_schema(arena) + .iter() + .copied() + .enumerate() + .map(|(position, column)| { + arena.alloc_expression(ScalarExpression::column_expr(column, position)) + }) + .collect(); + return Ok(Self::build_project_plan( + FilterOperator::build(predicate, children, false), + passthrough_exprs, + )); + } + Ok(FilterOperator::build(predicate, children, false)) + } + + fn bind_mark_queries<'c>( + &mut self, + mut children: LogicalPlan, + exprs: impl Iterator, + arena: &mut PlanArena, + ) -> Result { + let mut output_positions = Vec::new(); if let Some(sub_queries) = self.context.sub_queries_at_now() { for sub_query in sub_queries { match sub_query { @@ -1454,9 +1489,8 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b output_column, } => { let left_schema = children.output_schema(arena).clone(); + output_positions.push((output_column, left_schema.len())); let (plan, predicates) = Self::prepare_mark_apply( - predicate, - &output_column, left_schema.as_ref(), plan, correlated, @@ -1486,9 +1520,8 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b ); } let left_schema = children.output_schema(arena).clone(); + output_positions.push((output_column, left_schema.len())); let (plan, predicates) = Self::prepare_mark_apply( - predicate, - &output_column, left_schema.as_ref(), plan, correlated, @@ -1506,26 +1539,17 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } } } - { - let passthrough_exprs = children - .output_schema(arena) - .iter() - .cloned() - .enumerate() - .map(|(position, column)| { - arena.alloc_expression(ScalarExpression::column_expr(column, position)) - }) - .collect(); - let filter = FilterOperator::build(predicate, children, false); - return Ok(LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: passthrough_exprs, - }), - Childrens::Only(Box::new(filter)), - )); + } + for expr in exprs { + for (output_column, left_len) in &output_positions { + MarkerPositionGlobalizer { + output_column, + left_len: *left_len, + } + .visit(expr, arena)?; } } - Ok(FilterOperator::build(predicate, children, false)) + Ok(children) } fn ensure_mark_apply_right_outputs( @@ -1582,10 +1606,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b Ok(Vec::new()) } - #[allow(clippy::too_many_arguments)] fn prepare_mark_apply( - mut predicate: ExprRef, - output_column: &ColumnRef, left_schema: &Schema, plan: LogicalPlan, correlated: bool, @@ -1594,12 +1615,6 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b arena: &mut PlanArena, ) -> Result<(LogicalPlan, Vec), DatabaseError> { let left_len = left_schema.len(); - MarkerPositionGlobalizer { - output_column, - left_len, - } - .visit(&mut predicate, arena)?; - let (mut plan, correlated_filters) = if correlated { Self::prepare_correlated_subquery_plan(plan, left_schema, preserve_projection, arena)? } else { diff --git a/tests/slt/view.slt b/tests/slt/view.slt index 36597119..c358ceb8 100644 --- a/tests/slt/view.slt +++ b/tests/slt/view.slt @@ -76,6 +76,17 @@ select c1, sum(c0) from v1 group by c1 1 1 2 2 +# Writes must resolve a base table, not silently report success against a view. +statement error table not found +update v_alias set aa = 9; + +query I +select aa from v_alias order by aa; +---- +0 +1 +2 + statement ok drop view v_alias From 38d4fe2a1989e2bb924a54d157fe8ce3921d5cbc Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 02/10] test: strengthen SQL and storage regression coverage Cover scalar identity and spill codecs, persistent statistics, checkpoint recovery, parameter ranges, ORM writes and ALTER failure atomicity. Consolidate scalar-function SLT files and exclude test-only regions from coverage. Fix ORM HAVING binding against the actual aggregate outputs. --- src/bin/shell.rs | 2 + src/binder/aggregate.rs | 19 +- src/binder/create_table.rs | 2 + src/binder/distinct.rs | 2 + src/binder/mod.rs | 2 + src/binder/parser.rs | 59 +++++ src/binder/select.rs | 31 ++- src/catalog/column.rs | 2 + src/catalog/table.rs | 4 + src/db.rs | 70 ++++++ src/db/prepared.rs | 56 +++++ src/execution/dml/analyze.rs | 47 +++- src/execution/dml/copy_from_file.rs | 2 + src/execution/dml/copy_to_file.rs | 2 + src/execution/dql/aggregate/avg.rs | 2 + src/execution/dql/aggregate/hash_agg.rs | 2 + src/execution/dql/aggregate/stream_agg.rs | 2 + .../dql/aggregate/stream_distinct.rs | 2 + src/execution/dql/external_sort.rs | 2 + src/execution/dql/join/hash_join.rs | 2 + src/execution/dql/join/mod.rs | 2 + src/execution/dql/join/nested_loop_join.rs | 2 + src/execution/dql/mark_apply.rs | 2 + src/execution/dql/mod.rs | 2 + src/execution/dql/recursive_cte.rs | 4 + src/execution/dql/scalar_query_init.rs | 2 + src/execution/dql/sort.rs | 2 + src/execution/dql/top_k.rs | 2 + src/execution/mod.rs | 4 + src/execution/spill/codec.rs | 166 +++++++++++++ src/execution/spill/mod.rs | 4 + src/expression/eq_col.rs | 141 +++++++++++ src/expression/evaluator.rs | 4 + src/expression/mod.rs | 2 + src/expression/range_detacher.rs | 70 ++++++ src/optimizer/core/cm_sketch.rs | 2 + src/optimizer/core/histogram.rs | 2 + src/optimizer/core/hll.rs | 2 + src/optimizer/core/kll_sketch.rs | 4 + src/optimizer/core/statistics_meta.rs | 2 + src/optimizer/core/top_n.rs | 2 + src/optimizer/heuristic/optimizer.rs | 2 + src/optimizer/plan_utils.rs | 2 + src/optimizer/rule/implementation/mod.rs | 2 + .../rule/normalization/column_pruning.rs | 2 + .../rule/normalization/combine_operators.rs | 2 + .../normalization/compilation_in_advance.rs | 2 + .../rule/normalization/elimination.rs | 2 + .../rule/normalization/min_max_top_k.rs | 2 + .../rule/normalization/parameterized_index.rs | 2 + .../rule/normalization/pushdown_limit.rs | 2 + .../rule/normalization/pushdown_predicates.rs | 2 + .../rule/normalization/simplification.rs | 2 + src/orm/mod.rs | 233 ++++++++++++++++++ src/parser/mod.rs | 2 + src/planner/arena.rs | 90 +++++++ src/planner/mod.rs | 2 + src/planner/operator/join.rs | 2 + src/planner/operator/mod.rs | 2 + src/planner/operator/visitor.rs | 2 + src/planner/operator/visitor_mut.rs | 4 + src/planner/plan_keeper.rs | 2 + src/planner/scalar_query_ref.rs | 2 + src/python.rs | 2 + src/serdes/char_length_units.rs | 2 + src/serdes/column.rs | 2 + src/serdes/data_value.rs | 2 + src/serdes/evaluator.rs | 2 + src/serdes/function.rs | 2 + src/serdes/mod.rs | 2 + src/serdes/num.rs | 2 + src/serdes/stable_hash.rs | 2 + src/serdes/string.rs | 2 + src/storage/lmdb.rs | 2 + src/storage/memory.rs | 2 + src/storage/mod.rs | 2 + src/storage/rocksdb.rs | 2 + src/storage/table_codec.rs | 2 + tests/macros-test/src/main.rs | 2 + tests/slt/abs.slt | 29 --- tests/slt/aggregation.slt | 18 ++ tests/slt/basic_test.slt | 45 ++++ tests/slt/change_column.slt | 72 ++++++ tests/slt/char.slt | 46 +++- tests/slt/crdb/join.slt | 18 ++ tests/slt/create.slt | 18 ++ tests/slt/joined_dml.slt | 21 ++ tests/slt/limit.slt | 19 ++ tests/slt/string_length.slt | 35 --- tests/slt/subquery.slt | 32 +++ tpcc/src/load.rs | 2 + tpcc/src/main.rs | 2 + 92 files changed, 1419 insertions(+), 70 deletions(-) delete mode 100644 tests/slt/abs.slt delete mode 100644 tests/slt/string_length.slt diff --git a/src/bin/shell.rs b/src/bin/shell.rs index 9742bb48..83945998 100644 --- a/src/bin/shell.rs +++ b/src/bin/shell.rs @@ -419,6 +419,7 @@ Transaction commands: } } + // GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -557,6 +558,7 @@ Transaction commands: ); } } + // GRCOV_EXCL_STOP } #[cfg(not(target_arch = "wasm32"))] diff --git a/src/binder/aggregate.rs b/src/binder/aggregate.rs index 5fb781ab..41e0aa12 100644 --- a/src/binder/aggregate.rs +++ b/src/binder/aggregate.rs @@ -273,8 +273,21 @@ impl> Binder<'_, '_, T, A> { return Ok(()); } - HavingOrderByValidator::new(&self.context.group_by_exprs, &self.context.agg_calls) - .visit(expr, arena) + Self::validate_having_orderby_with_outputs( + expr, + &self.context.agg_calls, + &self.context.group_by_exprs, + arena, + ) + } + + pub(crate) fn validate_having_orderby_with_outputs( + expr: ExprRef, + agg_calls: &[ExprRef], + group_by_exprs: &[ExprRef], + arena: &PlanArena<'_>, + ) -> Result<(), DatabaseError> { + HavingOrderByValidator::new(group_by_exprs, agg_calls).visit(expr, arena) } } @@ -405,6 +418,7 @@ impl ExprVisitorMut for AggregateOutputBinder<'_> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::AggregateOutputBinder; @@ -576,3 +590,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/binder/create_table.rs b/src/binder/create_table.rs index 9de51118..31d22512 100644 --- a/src/binder/create_table.rs +++ b/src/binder/create_table.rs @@ -59,6 +59,7 @@ impl> Binder<'_, '_, T, A> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -125,3 +126,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/binder/distinct.rs b/src/binder/distinct.rs index 19234fbb..305eeb8a 100644 --- a/src/binder/distinct.rs +++ b/src/binder/distinct.rs @@ -124,6 +124,7 @@ impl ExprVisitorMut for DistinctOutputBinder<'_> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::DistinctOutputBinder; @@ -213,3 +214,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/binder/mod.rs b/src/binder/mod.rs index aae02188..6e1619f5 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -691,6 +691,7 @@ pub(crate) fn is_valid_identifier(s: &str) -> bool { && !s.chars().all(|c| c == '_') } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub mod test { use crate::binder::{is_valid_identifier, Binder, BinderContext}; @@ -842,3 +843,4 @@ pub mod test { assert!(!is_valid_identifier("____")); } } +// GRCOV_EXCL_STOP diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 0f30949c..9a57d931 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -3299,6 +3299,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -3491,6 +3492,63 @@ mod tests { Ok(()) } + #[test] + fn sql_type_binding_preserves_unsigned_widths_and_validates_precision( + ) -> Result<(), DatabaseError> { + use sqlparser::ast::{CharacterLength, ExactNumberInfo, TimezoneInfo}; + for (sql, expected) in [ + (DataType::TinyIntUnsigned(None), LogicalType::UTinyint), + (DataType::SmallIntUnsigned(None), LogicalType::USmallint), + (DataType::UnsignedInteger, LogicalType::UInteger), + (DataType::UInt64, LogicalType::UBigint), + ] { + assert_eq!(LogicalType::try_from(sql)?, expected); + } + for sql in [ + DataType::Char(Some(CharacterLength::Max)), + DataType::Varchar(Some(CharacterLength::Max)), + ] { + assert!(matches!( + LogicalType::try_from(sql), + Err(DatabaseError::UnsupportedStmt(_)) + )); + } + #[cfg(feature = "time")] + { + assert_eq!( + LogicalType::try_from(DataType::Time(Some(4), TimezoneInfo::None))?, + LogicalType::Time(Some(4)) + ); + assert_eq!( + LogicalType::try_from(DataType::Timestamp(Some(6), TimezoneInfo::WithTimeZone))?, + LogicalType::TimeStamp(Some(6), true) + ); + for sql in [ + DataType::Time(Some(5), TimezoneInfo::None), + DataType::Time(None, TimezoneInfo::WithTimeZone), + DataType::Timestamp(Some(2), TimezoneInfo::None), + DataType::Datetime(Some(3)), + ] { + assert!(matches!( + LogicalType::try_from(sql), + Err(DatabaseError::UnsupportedStmt(_)) + )); + } + } + #[cfg(feature = "decimal")] + { + assert_eq!( + LogicalType::try_from(DataType::Numeric(ExactNumberInfo::Precision(12)))?, + LogicalType::Decimal(Some(12), None) + ); + assert_eq!( + LogicalType::try_from(DataType::Dec(ExactNumberInfo::PrecisionAndScale(12, 3)))?, + LogicalType::Decimal(Some(12), Some(3)) + ); + } + Ok(()) + } + #[test] fn test_limit_parser_rejects_negative_and_non_constant_values() -> Result<(), DatabaseError> { let tables = build_t1_table()?; @@ -3621,3 +3679,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/binder/select.rs b/src/binder/select.rs index 75375421..8aa225d8 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -373,7 +373,34 @@ where } #[cfg(feature = "orm")] - pub(crate) fn having_expr(mut self, expr: ExprRef) -> Result { + pub(crate) fn having_expr(mut self, mut expr: ExprRef) -> Result { + let mut input = &self.plan; + loop { + match &input.operator { + Operator::Aggregate(op) => { + Binder::::validate_having_orderby_with_outputs( + expr, + &op.agg_calls, + &op.groupby_exprs, + self.arena, + )?; + self.binder.bind_aggregate_output_exprs_with_outputs( + &op.agg_calls, + &op.groupby_exprs, + std::iter::once(&mut expr), + self.arena, + )?; + break; + } + Operator::Filter(_) + | Operator::Sort(_) + | Operator::TopK(_) + | Operator::Limit(_) + | Operator::Window(_) => input = input.childrens.only(), + Operator::ScalarQueryInit(_) => input = input.childrens.twins().0, + _ => break, + } + } self.plan = self.binder.bind_having(self.plan, expr, self.arena)?; Ok(self) } @@ -2124,6 +2151,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{ProjectionOutputBinder, RightSidePositionGlobalizer}; @@ -2416,3 +2444,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/catalog/column.rs b/src/catalog/column.rs index a3448b7c..05e2893e 100644 --- a/src/catalog/column.rs +++ b/src/catalog/column.rs @@ -198,6 +198,7 @@ impl ColumnCatalog { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{ColumnCatalog, ColumnDesc}; @@ -233,6 +234,7 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP /// The descriptor of a column. #[derive(Debug, Clone, PartialEq, Eq, Hash, ReferenceSerialization)] diff --git a/src/catalog/table.rs b/src/catalog/table.rs index ac523df1..25cd77bc 100644 --- a/src/catalog/table.rs +++ b/src/catalog/table.rs @@ -74,10 +74,12 @@ impl TableCatalog { self.columns.get(id).map(|i| self.column_refs[*i]) } + // GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) fn get_column_id_by_name(&self, name: &str) -> Option<&ColumnId> { self.column_idxs.get(name).map(|(id, _)| id) } + // GRCOV_EXCL_STOP pub(crate) fn get_column_by_name(&self, name: &str) -> Option { self.column_idxs @@ -370,6 +372,7 @@ impl TableMeta { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -471,3 +474,4 @@ mod tests { } } } +// GRCOV_EXCL_STOP diff --git a/src/db.rs b/src/db.rs index 74cf4775..520aab02 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1327,6 +1327,7 @@ impl ResultIter for TransactionIter<'_, T, A> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::binder::{Binder, BinderContext}; @@ -2320,6 +2321,74 @@ pub(crate) mod test { Ok(()) } + #[test] + fn optimistic_checkpoint_restores_rows_indexes_and_scalar_views() -> Result<(), DatabaseError> { + let directory = TempDir::new()?; + let live_path = directory.path().join("live"); + let snapshot_path = directory.path().join("snapshot"); + let mut database = DataBaseBuilder::path(&live_path).build_optimistic()?; + database.ddl("create table checkpoint_rows(id int primary key, v int unique)")?; + database + .run("insert into checkpoint_rows values(1,10),(2,20)")? + .done()?; + database.ddl( + "create view checkpoint_view as select (select max(v) from checkpoint_rows) as v", + )?; + database.checkpoint(&snapshot_path)?; + database + .run("update checkpoint_rows set v=30 where id=2")? + .done()?; + database + .run("insert into checkpoint_rows values(3,40)")? + .done()?; + let mut snapshot = DataBaseBuilder::path(&snapshot_path).build_optimistic()?; + snapshot.load(CatalogKind::Table("checkpoint_rows".into()))?; + snapshot.load(CatalogKind::View("checkpoint_view".into()))?; + let mut rows = snapshot.run("select id,v from checkpoint_rows order by id")?; + assert_eq!( + rows.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(1), DataValue::Int32(10)]) + ); + assert_eq!( + rows.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(2), DataValue::Int32(20)]) + ); + assert!(rows.next_tuple(|_, _| ())?.is_none()); + rows.done()?; + assert_eq!( + read_single_i32(snapshot.run("select id from checkpoint_rows where v=20")?)?, + 2 + ); + assert_eq!( + read_single_i32(snapshot.run("select v from checkpoint_view")?)?, + 20 + ); + assert_eq!( + read_single_i32(database.run("select v from checkpoint_view")?)?, + 40 + ); + assert!(matches!( + snapshot + .run("insert into checkpoint_rows values(4,20)")? + .done(), + Err(DatabaseError::DuplicateUniqueValue) + )); + let occupied = directory.path().join("occupied"); + std::fs::write(&occupied, b"keep")?; + assert!( + matches!(database.checkpoint(&occupied), Err(DatabaseError::IO(ref err)) if err.kind() == ErrorKind::AlreadyExists) + ); + assert_eq!(std::fs::read(&occupied)?, b"keep"); + drop(snapshot); + let mut reopened = DataBaseBuilder::path(&snapshot_path).build_optimistic()?; + reopened.load(CatalogKind::Table("checkpoint_rows".into()))?; + assert_eq!( + read_single_i32(reopened.run("select count(*) from checkpoint_rows")?)?, + 2 + ); + Ok(()) + } + #[test] fn test_checkpoint_rejects_non_empty_target_dir() -> Result<(), DatabaseError> { let temp_dir = TempDir::new().expect("unable to create temporary working directory"); @@ -2423,3 +2492,4 @@ pub(crate) mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/db/prepared.rs b/src/db/prepared.rs index 2f61233c..2ece306e 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -1,9 +1,13 @@ use super::*; +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::operator::{PhysicalOption, PlanImpl, SortOption}; +// GRCOV_EXCL_STOP use crate::planner::{ExprRef, ParamArena, PlanInput}; +// GRCOV_EXCL_START #[cfg(test)] use crate::types::index::IndexLookup; +// GRCOV_EXCL_STOP use crate::types::LogicalType; /// A bound and optimized reusable plan. @@ -132,6 +136,7 @@ impl State { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -415,6 +420,56 @@ mod tests { )?; assert!(iter.next_tuple(|_, _| ())?.is_none()); iter.done()?; + + let unions = db.prepare_sql( + "select id from parameter_order where (id=$1 or id=$2) and id>$3 order by id", + &[ + (1, LogicalType::Integer), + (2, LogicalType::Integer), + (3, LogicalType::Integer), + ], + )?; + for (first, second, lower, expected) in [ + ( + DataValue::Int32(1), + DataValue::Int32(4), + DataValue::Int32(2), + vec![DataValue::Int32(4)], + ), + ( + DataValue::Int32(4), + DataValue::Int32(1), + DataValue::Int32(0), + vec![DataValue::Int32(1), DataValue::Int32(4)], + ), + ( + DataValue::Int32(2), + DataValue::Int32(2), + DataValue::Int32(0), + vec![DataValue::Int32(2)], + ), + ( + DataValue::Null, + DataValue::Int32(3), + DataValue::Int32(0), + vec![DataValue::Int32(3)], + ), + ( + DataValue::Int32(1), + DataValue::Int32(4), + DataValue::Null, + vec![], + ), + ] { + let mut iter = db.execute(&unions, [(1, first), (2, second), (3, lower)])?; + let mut rows = Vec::new(); + while iter + .next_tuple(|_, row| rows.push(row.value_at(0).clone()))? + .is_some() + {} + iter.done()?; + assert_eq!(rows, expected); + } Ok(()) } @@ -575,3 +630,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index 5b2bb92d..d3144016 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -189,9 +189,10 @@ impl fmt::Display for AnalyzeOperator { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { - use crate::db::{DataBaseBuilder, Database}; + use crate::db::{CatalogKind, DataBaseBuilder, Database}; use crate::errors::DatabaseError; use crate::execution::dml::analyze::DEFAULT_NUM_OF_BUCKETS; use crate::expression::range_detacher::Range; @@ -261,7 +262,48 @@ mod test { assert_eq!(statistics_meta_p_index.index_id(), 2); assert_eq!(statistics_meta_p_index.histogram().values_len(), 101); assert_eq!(statistics_meta_p_index.histogram().buckets_len(), buckets); - + let expected_frequency = + loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?; + + // Reload the stored parts instead of reusing ANALYZE's in-memory cache. + drop(kite_sql); + let mut reopened = DataBaseBuilder::path(temp_dir.path()).build_rocksdb()?; + reopened.load(CatalogKind::Table(table_name.clone()))?; + let loader = StatisticMetaLoader::new(reopened.state.meta_cache()); + for index in 0..=2 { + let meta = loader.load(&table_name, index)?.unwrap(); + assert_eq!(meta.index_id(), index); + assert_eq!(meta.histogram().values_len(), 101); + assert_eq!(meta.histogram().buckets_len(), buckets); + } + assert_eq!( + loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?, + expected_frequency + ); + let mut iter = reopened.run("select a from t1 where b=7 order by a")?; + let mut rows = Vec::new(); + while iter + .next_tuple(|_, row| rows.push(row.values[0].clone()))? + .is_some() + {} + iter.done()?; + assert_eq!(rows, [7, 27, 47, 67, 87].map(DataValue::Int32)); + + // A missing root must not publish partially reconstructed statistics. + let mut codec = TableCodec::default(); + let mut tx = reopened.storage.transaction()?; + codec.with_statistics_meta( + "t1", + 1, + None, + reopened.state.table_arena().borrow(), + |key, _| tx.remove(key), + )?; + tx.commit()?; + drop(reopened); + let mut reopened = DataBaseBuilder::path(temp_dir.path()).build_rocksdb()?; + assert!(matches!(reopened.load(CatalogKind::Table(table_name)), + Err(DatabaseError::InvalidValue(message)) if message == "statistics meta is incomplete")); Ok(()) } @@ -383,3 +425,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dml/copy_from_file.rs b/src/execution/dml/copy_from_file.rs index 697f00b5..1d842de8 100644 --- a/src/execution/dml/copy_from_file.rs +++ b/src/execution/dml/copy_from_file.rs @@ -106,6 +106,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyFromFile<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -196,3 +197,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dml/copy_to_file.rs b/src/execution/dml/copy_to_file.rs index 31e2ce0f..6fc3bf47 100644 --- a/src/execution/dml/copy_to_file.rs +++ b/src/execution/dml/copy_to_file.rs @@ -111,6 +111,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyToFile<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -187,3 +188,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/aggregate/avg.rs b/src/execution/dql/aggregate/avg.rs index 7e4b2702..04e4e2ab 100644 --- a/src/execution/dql/aggregate/avg.rs +++ b/src/execution/dql/aggregate/avg.rs @@ -81,6 +81,7 @@ impl Accumulator for AvgAccumulator { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -121,3 +122,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 7880d79a..927913dd 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -110,6 +110,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::catalog::{ColumnCatalog, ColumnDesc}; @@ -232,3 +233,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/aggregate/stream_agg.rs b/src/execution/dql/aggregate/stream_agg.rs index a9cb91af..63b58304 100644 --- a/src/execution/dql/aggregate/stream_agg.rs +++ b/src/execution/dql/aggregate/stream_agg.rs @@ -119,6 +119,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::StreamAggExecutor; @@ -242,3 +243,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index 1702fe10..da1250ee 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -80,6 +80,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; @@ -254,3 +255,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index cd47fd57..c8a914c2 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -279,6 +279,7 @@ fn merge_pass<'on_flush>( Ok(target) } +// GRCOV_EXCL_START #[cfg(test)] mod test { use super::{finish_sort, sort_segment, Run, MERGE_FAN_IN}; @@ -507,3 +508,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index d2f42819..b34405e6 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -364,6 +364,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::catalog::{ColumnCatalog, ColumnDesc}; @@ -842,3 +843,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/mod.rs b/src/execution/dql/join/mod.rs index b0165929..c72cae3f 100644 --- a/src/execution/dql/join/mod.rs +++ b/src/execution/dql/join/mod.rs @@ -62,6 +62,7 @@ pub fn joins_nullable(join_type: &JoinType) -> (bool, bool) { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::RowBitmap; @@ -117,3 +118,4 @@ mod tests { bitmap.insert(1); } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 72e4a9b0..e31a2d7e 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -355,6 +355,7 @@ impl<'a> NestedLoopJoin<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use super::*; @@ -1256,3 +1257,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 4d9f6c6d..761c1335 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -364,6 +364,7 @@ impl<'a> MarkApply<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -1032,3 +1033,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/mod.rs b/src/execution/dql/mod.rs index 0bab43cd..461d9523 100644 --- a/src/execution/dql/mod.rs +++ b/src/execution/dql/mod.rs @@ -38,6 +38,7 @@ pub(crate) mod union; pub(crate) mod values; pub(crate) mod window; +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::iter_ext::Itertools; @@ -49,3 +50,4 @@ pub(crate) mod test { .collect_vec() } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/recursive_cte.rs b/src/execution/dql/recursive_cte.rs index 654ec268..eb8c06c2 100644 --- a/src/execution/dql/recursive_cte.rs +++ b/src/execution/dql/recursive_cte.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -320,6 +322,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveScan { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -479,3 +482,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/scalar_query_init.rs b/src/execution/dql/scalar_query_init.rs index 42bd61eb..449b870d 100644 --- a/src/execution/dql/scalar_query_init.rs +++ b/src/execution/dql/scalar_query_init.rs @@ -175,6 +175,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -255,3 +256,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/sort.rs b/src/execution/dql/sort.rs index 402e8088..cec626f6 100644 --- a/src/execution/dql/sort.rs +++ b/src/execution/dql/sort.rs @@ -215,6 +215,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Sort<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::catalog::{ColumnCatalog, ColumnDesc}; @@ -760,3 +761,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/top_k.rs b/src/execution/dql/top_k.rs index 38ec59e5..ac925004 100644 --- a/src/execution/dql/top_k.rs +++ b/src/execution/dql/top_k.rs @@ -194,6 +194,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for TopK<'a> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] #[allow(clippy::mutable_key_type)] mod test { @@ -850,3 +851,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/execution/mod.rs b/src/execution/mod.rs index a43b2421..64c29a74 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -924,6 +926,7 @@ where } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test_utils { use super::*; @@ -1063,3 +1066,4 @@ mod test { assert_eq!(arena.nodes.items.as_ptr(), slot); } } +// GRCOV_EXCL_STOP diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 6dc2b89d..511116d4 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -269,3 +269,169 @@ fn estimated_dynamic_value_size(value: &DataValue) -> usize { _ => 0, } } + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::*; + use crate::types::value::Utf8Type; + use crate::types::{CharLengthUnits, LogicalType}; + use std::fmt::Debug; + + fn round_trip(value: &T) -> Result<(), DatabaseError> { + let mut bytes = Vec::new(); + value.encode(&mut bytes)?; + let mut reader = bytes.as_slice(); + assert_eq!(&T::decode(&mut reader)?, value); + assert!(reader.is_empty()); + assert!(value.estimated_size() >= size_of::()); + Ok(()) + } + + fn values() -> Vec { + let values = vec![ + DataValue::Null, + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Int8(i8::MIN), + DataValue::Int16(i16::MAX), + DataValue::Int32(i32::MIN), + DataValue::Int64(i64::MAX), + DataValue::UInt8(u8::MAX), + DataValue::UInt16(u16::MAX), + DataValue::UInt32(u32::MAX), + DataValue::UInt64(u64::MAX), + DataValue::Float32((-1.25).into()), + DataValue::Float64(2.5.into()), + DataValue::Float64(f64::INFINITY.into()), + DataValue::Utf8 { + value: "text中\0".into(), + ty: Utf8Type::Variable(Some(20)), + unit: CharLengthUnits::Characters, + }, + DataValue::Utf8 { + value: "fixed".into(), + ty: Utf8Type::Fixed(8), + unit: CharLengthUnits::Octets, + }, + DataValue::new_utf8(String::new()), + DataValue::Date32(-1), + DataValue::Date64(1_700_000_000), + DataValue::Time32(12345, 3), + DataValue::Time64(-12345, 6, false), + DataValue::Time64(12345, 6, true), + DataValue::Tuple(Vec::new()), + DataValue::Tuple(vec![ + DataValue::new_utf8("nested".into()), + DataValue::Tuple(vec![DataValue::Null, DataValue::Int32(7)]), + ]), + ]; + #[cfg(feature = "decimal")] + let values = { + let mut values = values; + values.push(DataValue::Decimal(rust_decimal::Decimal::new(-12345, 3))); + values + }; + values + } + + #[test] + fn data_value_codec_covers_all_materialized_variants() -> Result<(), DatabaseError> { + for value in values() { + round_trip(&value)?; + } + assert!(matches!( + DataValue::Parameter { + id: 1, + ty: LogicalType::Integer + } + .encode(&mut Vec::new()), + Err(DatabaseError::InvalidValue(_)) + )); + assert!(matches!( + DataValue::decode(&mut &[255][..]), + Err(DatabaseError::InvalidValue(_)) + )); + assert!(DataValue::decode(&mut &[7][..]).is_err()); + Ok(()) + } + + #[test] + fn composed_codecs_cover_containers_tuples_and_sort_rows() -> Result<(), DatabaseError> { + let values = values(); + round_trip(&Vec::::new())?; + round_trip(&values)?; + round_trip(&Option::::None)?; + round_trip(&Some(DataValue::Null))?; + round_trip(&Some(DataValue::new_utf8("owned".into())))?; + round_trip(&(Some(DataValue::Int32(3)), values.clone()))?; + for is_outer_value in [false, true] { + let reference = ScalarQueryRef { + arena_id: 1, + pos: 7, + is_outer_value, + }; + round_trip(&reference)?; + round_trip(&(reference, DataValue::Null))?; + } + for pk in [None, Some(DataValue::new_utf8("primary".into()))] { + let tuple = Tuple::new(pk, values.clone()); + round_trip(&tuple)?; + let row = SortRow { + sort_values: vec![DataValue::Int32(2), DataValue::Null], + tuple, + outer_values: vec![( + ScalarQueryRef { + arena_id: 0, + pos: 3, + is_outer_value: true, + }, + DataValue::new_utf8("scalar".into()), + )], + }; + let mut bytes = Vec::new(); + row.encode(&mut bytes)?; + let mut reader = bytes.as_slice(); + let decoded = SortRow::decode(&mut reader)?; + assert!(reader.is_empty()); + assert_eq!(decoded.sort_values, row.sort_values); + assert_eq!(decoded.tuple, row.tuple); + assert_eq!(decoded.outer_values, row.outer_values); + assert!(row.estimated_size() >= size_of::()); + } + assert!(matches!( + Option::::decode(&mut &[2][..]), + Err(DatabaseError::InvalidValue(_)) + )); + assert!(Vec::::decode(&mut &[1, 0, 0, 0][..]).is_err()); + assert!(<(DataValue, DataValue)>::decode(&mut &[0][..]).is_err()); + Ok(()) + } + + #[test] + fn scalar_reference_spill_rejects_invalid_and_truncated_markers() -> Result<(), DatabaseError> { + for is_outer_value in [false, true] { + let reference = ScalarQueryRef { + arena_id: 2, + pos: 17, + is_outer_value, + }; + let value = (reference, Some(DataValue::Null)); + let mut bytes = Vec::new(); + value.encode(&mut bytes)?; + assert_eq!( + <(ScalarQueryRef, Option)>::decode(&mut bytes.as_slice())?, + value + ); + assert!(value.estimated_size() >= size_of_val(&value)); + assert!(ScalarQueryRef::decode(&mut &bytes[..16]).is_err()); + bytes[16] = 2; + assert!(matches!( + ScalarQueryRef::decode(&mut bytes.as_slice()), + Err(DatabaseError::InvalidValue(_)) + )); + } + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/spill/mod.rs b/src/execution/spill/mod.rs index 1fbb91eb..1a293485 100644 --- a/src/execution/spill/mod.rs +++ b/src/execution/spill/mod.rs @@ -81,6 +81,7 @@ impl<'on_flush, T: SpillCodec> SpillVec<'on_flush, T> { } } + // GRCOV_EXCL_START #[cfg(test)] pub(crate) fn limit(mut self, max_rows: usize, max_bytes: usize) -> Self { assert!(max_rows > 0, "spill row limit must be positive"); @@ -91,6 +92,7 @@ impl<'on_flush, T: SpillCodec> SpillVec<'on_flush, T> { } self } + // GRCOV_EXCL_STOP pub(crate) fn on_flush(mut self, on_flush: F) -> Self where @@ -408,6 +410,7 @@ fn create_spill_file() -> Result<(File, PathBuf), DatabaseError> { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -767,3 +770,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/expression/eq_col.rs b/src/expression/eq_col.rs index 11fd3be8..6df7123b 100644 --- a/src/expression/eq_col.rs +++ b/src/expression/eq_col.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -544,6 +546,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -593,6 +596,143 @@ mod tests { fn compares_every_scalar_expression_variant() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); + let id = arena.alloc_scalar_query_ref(false); + let other = arena.alloc_scalar_query_ref(false); + assert_case( + &mut arena, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id: other, + ty: LogicalType::Integer, + }, + ); + assert_case( + &mut arena, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id, + ty: LogicalType::Bigint, + }, + ); + assert_case( + &mut arena, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id: other, + ty: LogicalType::Integer, + }, + ); + assert_case( + &mut arena, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Bigint, + }, + ); + assert_case( + &mut arena, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id: other, + ty: LogicalType::Integer, + }, + ); + assert_case( + &mut arena, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Bigint, + }, + ); + assert_case( + &mut arena, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ); + assert_case( + &mut arena, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ); + assert_case( + &mut arena, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterParam { + id, + ty: LogicalType::Integer, + }, + ); let lhs_column = arena.alloc_column(ColumnCatalog::new( "c1".to_string(), @@ -1042,3 +1182,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index 7cffc4e0..9e413fdc 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -413,6 +415,7 @@ fn trim_string(value: &str, trim_what: &str, trim_where: Option) trimmed.to_string() } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -661,3 +664,4 @@ mod tests { assert_eq!(trim_string("hello", "", None), "hello"); } } +// GRCOV_EXCL_STOP diff --git a/src/expression/mod.rs b/src/expression/mod.rs index e84a09a6..af3629c9 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -1084,6 +1084,7 @@ impl fmt::Display for UnaryOperator { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::catalog::{ColumnCatalog, ColumnDesc}; @@ -1574,3 +1575,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 3ac5f5fb..211e1481 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -1255,6 +1255,7 @@ impl fmt::Display for Range { } } +// GRCOV_EXCL_START #[cfg(test)] pub(crate) mod test_support { use super::*; @@ -2748,6 +2749,74 @@ mod test { Ok(()) } + #[test] + fn parameterized_range_union_keeps_residual_until_values_are_bound() -> Result<(), DatabaseError> + { + let parameter = |id| DataValue::Parameter { + id, + ty: LogicalType::Integer, + }; + let left = Range::Scope { + min: Bound::Included(parameter(1)), + max: Bound::Unbounded, + }; + let right = Range::Scope { + min: Bound::Unbounded, + max: Bound::Excluded(parameter(2)), + }; + assert_eq!( + RangeDetacher::::merge_binary( + BinaryOperator::Or, + left.clone(), + right.clone() + ), + Err((left.clone(), right.clone())), + ); + let mut ranges = Range::SortedRanges(vec![left, right]); + let low = DataValue::Int32(8); + let high = DataValue::Int32(3); + ranges.bind_parameters(&|id| match id { + 1 => Some(&low), + 2 => Some(&high), + _ => None, + })?; + assert!(!ranges.has_parameter()); + assert_eq!( + ranges, + Range::SortedRanges(vec![ + Range::Scope { + min: Bound::Included(low), + max: Bound::Unbounded + }, + Range::Scope { + min: Bound::Unbounded, + max: Bound::Excluded(high) + }, + ]) + ); + Ok(()) + } + + #[test] + fn intersection_failure_restores_inputs_after_appending_a_match() { + let scope = |min, max| Range::Scope { + min: Bound::Included(min), + max: Bound::Included(max), + }; + let left = scope(DataValue::Int32(1), DataValue::Int32(9)); + let right = vec![ + Range::Eq(DataValue::Int32(2)), + scope( + DataValue::new_utf8("a".into()), + DataValue::new_utf8("z".into()), + ), + ]; + assert_eq!( + RangeDetacher::::intersect_ranges(left.clone(), right.clone()), + Err((left, Range::SortedRanges(right))), + ); + } + #[test] fn test_range_merge_preserves_open_closed_boundaries() { let gt_one = Range::Scope { @@ -2990,3 +3059,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/cm_sketch.rs b/src/optimizer/core/cm_sketch.rs index 04dd0b71..76ee3a7d 100644 --- a/src/optimizer/core/cm_sketch.rs +++ b/src/optimizer/core/cm_sketch.rs @@ -397,6 +397,7 @@ impl ReferenceSerialization for CountMinSketch { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::expression::range_detacher::Range; @@ -571,3 +572,4 @@ mod tests { assert_eq!(decoded.estimate(&7), 23); } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index d1352a58..ea5badbc 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -687,6 +687,7 @@ impl HistogramMeta { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::errors::DatabaseError; @@ -1565,3 +1566,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/hll.rs b/src/optimizer/core/hll.rs index 66a66498..8dae20ae 100644 --- a/src/optimizer/core/hll.rs +++ b/src/optimizer/core/hll.rs @@ -123,6 +123,7 @@ impl HyperLogLog { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::HyperLogLog; @@ -149,3 +150,4 @@ mod tests { assert!(error < 0.03, "estimate={estimate}, error={error}"); } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/kll_sketch.rs b/src/optimizer/core/kll_sketch.rs index 5a6697fb..4fa6a9ff 100644 --- a/src/optimizer/core/kll_sketch.rs +++ b/src/optimizer/core/kll_sketch.rs @@ -106,6 +106,7 @@ impl KllSketchBuilder { }) } + // GRCOV_EXCL_START #[cfg(test)] fn len(&self) -> usize { self.len @@ -120,6 +121,7 @@ impl KllSketchBuilder { fn retained_len(&self) -> usize { self.levels.iter().map(Vec::len).sum() } + // GRCOV_EXCL_STOP fn update_bounds(&mut self, value: &DataValue) -> Result<(), DatabaseError> { if let Some(min_value) = &self.min_value { @@ -344,6 +346,7 @@ fn compare_values(left: &DataValue, right: &DataValue) -> Ordering { .expect("KLL sketch values must be mutually comparable") } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::KllSketchBuilder; @@ -449,3 +452,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/statistics_meta.rs b/src/optimizer/core/statistics_meta.rs index c3ff7177..d0a3260f 100644 --- a/src/optimizer/core/statistics_meta.rs +++ b/src/optimizer/core/statistics_meta.rs @@ -154,6 +154,7 @@ impl StatisticsMeta { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::errors::DatabaseError; @@ -215,3 +216,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/core/top_n.rs b/src/optimizer/core/top_n.rs index 62773c5e..6d9656ae 100644 --- a/src/optimizer/core/top_n.rs +++ b/src/optimizer/core/top_n.rs @@ -223,6 +223,7 @@ impl ColumnTopN { } } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::ColumnTopN; @@ -271,3 +272,4 @@ mod tests { assert_eq!(top_n.get(&DataValue::Null), None); } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index b0b455c9..8b853407 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -617,6 +617,7 @@ impl ImplementationRuleIndex { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::{Binder, BinderContext}; @@ -755,6 +756,7 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP #[derive(Clone, Default)] pub struct HepOptimizerPipeline { diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index e2aa752b..94c9f6c4 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -116,6 +116,7 @@ fn take_childrens(plan: &mut LogicalPlan) -> Childrens { mem::replace(&mut *plan.childrens, Childrens::None) } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -217,3 +218,4 @@ mod tests { assert!(!wrap_child_with(&mut parent, 2, Operator::ShowView)); } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index f31ae4cc..c1a5879f 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -307,6 +307,7 @@ impl ImplementationRule for ImplementationRuleImpl { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; @@ -653,3 +654,4 @@ mod tests { ); } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 6653cbf9..85fb9fa8 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -847,6 +847,7 @@ impl NormalizationRule for ColumnPruning { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; @@ -1117,3 +1118,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index 91d1bf13..b45b8903 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -327,6 +327,7 @@ impl NormalizationRule for CollapseGroupByAgg { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; @@ -563,3 +564,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/compilation_in_advance.rs b/src/optimizer/rule/normalization/compilation_in_advance.rs index 1fe28b46..e26a2111 100644 --- a/src/optimizer/rule/normalization/compilation_in_advance.rs +++ b/src/optimizer/rule/normalization/compilation_in_advance.rs @@ -67,6 +67,7 @@ impl NormalizationRule for EvaluatorBind { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -250,3 +251,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index e37e5a2d..2e483454 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -490,6 +490,7 @@ pub(crate) fn covers( } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{ @@ -1278,3 +1279,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/min_max_top_k.rs b/src/optimizer/rule/normalization/min_max_top_k.rs index 47a174c9..28bfae1c 100644 --- a/src/optimizer/rule/normalization/min_max_top_k.rs +++ b/src/optimizer/rule/normalization/min_max_top_k.rs @@ -80,6 +80,7 @@ impl NormalizationRule for MinMaxToTopK { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::MinMaxToTopK; @@ -179,3 +180,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index bc726d29..8ee20085 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -573,6 +573,7 @@ mod tests { } // GRCOV_EXCL_STOP +// GRCOV_EXCL_START #[cfg(test)] mod inner_join_tests { use super::*; @@ -884,3 +885,4 @@ mod inner_join_tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index 1bff3ee0..e33dc91c 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -129,6 +129,7 @@ impl NormalizationRule for PushLimitIntoScan { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; @@ -233,3 +234,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 16fd7c0a..6bdef695 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -572,6 +572,7 @@ impl NormalizationRule for PushJoinPredicateIntoScan { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { @@ -1349,3 +1350,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 052dc486..9de97e4e 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -103,6 +103,7 @@ impl NormalizationRule for SimplifyFilter { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::binder::test::build_t1_table; @@ -687,3 +688,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/orm/mod.rs b/src/orm/mod.rs index dc1d9a80..d7dcc50a 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -3425,6 +3425,7 @@ fn orm_list<'a, E: BindSource<'a>, M: Model>( #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; + use crate::expression::BinaryOperator; use crate::types::tuple::{Schema, SchemaView}; use std::sync::Arc; @@ -4063,6 +4064,238 @@ mod tests { Ok(()) } + #[test] + fn chained_range_and_function_helpers_preserve_values_and_null_rows( + ) -> Result<(), DatabaseError> { + let database = build_orm_unit_database()?; + let mut iter = database + .bind(|ctx| { + ctx.from::()? + .filter(|e| e.column(OrmUnitUser::age())?.between(18, 20))? + .project_tuple(|e| { + Ok(vec![ + e.column(OrmUnitUser::id())?, + e.column(OrmUnitUser::name())? + .function("upper", std::iter::empty::())?, + ]) + })? + .order_by_expr(|e| Ok(e.column(OrmUnitUser::id())?.asc()))? + .finish() + })? + .project_tuple::<(i32, String)>(); + assert_eq!(iter.next().transpose()?, Some((1, "ALICE".into()))); + assert_eq!(iter.next().transpose()?, Some((2, "BOB".into()))); + assert!(iter.next().transpose()?.is_none()); + iter.done()?; + let mut iter = database + .bind(|ctx| { + ctx.from::()? + .filter(|e| { + let age = e.column(OrmUnitUser::age())?; + age.clone().not_between(18, 20)?.or(age.is_null()) + })? + .project_scalar(OrmUnitUser::id())? + .finish() + })? + .project_value::(); + assert_eq!(iter.next().transpose()?, Some(3)); + assert!(iter.next().transpose()?.is_none()); + iter.done()?; + Ok(()) + } + + #[test] + fn insert_select_maps_columns_and_rejects_invalid_targets() -> Result<(), DatabaseError> { + let database = build_orm_unit_database()?; + database + .bind(|ctx| { + ctx.insert_select::(["name", "id", "age"], |ctx| { + ctx.from::()? + .filter(|e| e.column(OrmUnitUser::id())?.lte(2))? + .project_tuple(|e| { + Ok(vec![ + e.column(OrmUnitUser::name())?, + e.binary(e.column(OrmUnitUser::id())?, BinaryOperator::Plus, 10)?, + e.column(OrmUnitUser::age())?, + ]) + })? + .finish() + }) + })? + .done()?; + database + .bind(|ctx| { + ctx.insert_select::(std::iter::empty::(), |ctx| { + ctx.from::()? + .filter(|e| e.column(OrmUnitUser::id())?.eq(3))? + .project_tuple(|e| { + Ok(vec![ + e.binary(e.column(OrmUnitUser::id())?, BinaryOperator::Plus, 20)?, + e.column(OrmUnitUser::name())?, + e.column(OrmUnitUser::age())?, + ]) + })? + .finish() + }) + })? + .done()?; + assert!(matches!( + database.bind( + |ctx| ctx.insert_select::(["id", "name"], |ctx| { + ctx.from::()? + .project_scalar(OrmUnitUser::id())? + .finish() + }) + ), + Err(DatabaseError::ValuesLenMismatch(2, 1)) + )); + assert!(database + .bind( + |ctx| ctx.insert_select::(["missing"], |ctx| { + ctx.from::()? + .project_scalar(OrmUnitUser::id())? + .finish() + }) + ) + .is_err()); + let mut iter = + database.run("select id,name,age from orm_unit_users where id>3 order by id")?; + let mut rows = Vec::new(); + while iter + .next_tuple(|_, row| rows.push(row.values.clone()))? + .is_some() + {} + iter.done()?; + assert_eq!( + rows, + vec![ + vec![ + DataValue::Int32(11), + DataValue::new_utf8("Alice".into()), + DataValue::Int32(18) + ], + vec![ + DataValue::Int32(12), + DataValue::new_utf8("Bob".into()), + DataValue::Int32(20) + ], + vec![ + DataValue::Int32(23), + DataValue::new_utf8("Cara".into()), + DataValue::Null + ], + ] + ); + Ok(()) + } + + #[test] + fn case_helpers_infer_else_types_and_preserve_null_fallbacks() -> Result<(), DatabaseError> { + let database = build_orm_unit_database()?; + let mut rows = database + .bind(|ctx| { + ctx.from::()? + .project_tuple(|e| { + Ok(vec![ + e.column(OrmUnitUser::id())?, + e.case_when(std::iter::empty::<(bool, i32)>(), Some(7)), + e.case_value( + e.column(OrmUnitUser::id())?, + std::iter::empty::<(i32, String)>(), + Some("fallback".to_string()), + ), + e.case_when([(e.column(OrmUnitUser::age())?.gt(18)?, 1)], None::), + ]) + })? + .order_by(OrmUnitUser::id().asc())? + .finish() + })? + .project_tuple::<(i32, i32, String, Option)>(); + for expected in [ + (1, 7, "fallback".into(), None), + (2, 7, "fallback".into(), Some(1)), + (3, 7, "fallback".into(), None), + ] { + assert_eq!(rows.next().transpose()?, Some(expected)); + } + assert!(rows.next().transpose()?.is_none()); + rows.done()?; + Ok(()) + } + + #[test] + fn grouped_orm_having_reads_the_aggregate_output() -> Result<(), DatabaseError> { + let database = build_orm_unit_database()?; + let mut rows = database + .bind(|ctx| { + ctx.from::()? + .project_tuple(|e| { + Ok(vec![e.column(OrmUnitOrder::user_id())?, e.count_all()?]) + })? + .group_by(|e| e.column(OrmUnitOrder::user_id()))? + .having(|e| e.count_all()?.gt(1))? + .having(|e| e.column(OrmUnitOrder::user_id())?.eq(1))? + .order_by(OrmUnitOrder::user_id().asc())? + .finish() + })? + .project_tuple::<(i32, i32)>(); + assert_eq!(rows.next().transpose()?, Some((1, 2))); + assert!(rows.next().transpose()?.is_none()); + rows.done()?; + assert!(matches!( + database.bind(|ctx| { + ctx.from::()? + .project_tuple(|e| { + Ok(vec![e.column(OrmUnitOrder::user_id())?, e.count_all()?]) + })? + .group_by(|e| e.column(OrmUnitOrder::user_id()))? + .having(|e| e.column(OrmUnitOrder::amount())?.gt(100))? + .finish() + }), + Err(DatabaseError::AggMiss(_)) + )); + Ok(()) + } + + #[test] + fn update_assignments_preserve_unmatched_rows() -> Result<(), DatabaseError> { + let database = build_orm_unit_database()?; + assert!(matches!( + database.bind(|ctx| ctx.mutate::()?.update(|_| Ok(()))), + Err(DatabaseError::ColumnsEmpty) + )); + database + .bind(|ctx| { + ctx.mutate::()? + .filter(|e| e.column(OrmUnitUser::id())?.eq(1))? + .update(|u| { + u.set(OrmUnitUser::name(), "Alicia")?; + u.set_bound_expr( + OrmUnitUser::age(), + ScalarExpression::Constant(DataValue::Int32(25)), + ) + }) + })? + .done()?; + let mut rows = database + .bind(|ctx| { + ctx.from::()? + .project_scalars((OrmUnitUser::id(), OrmUnitUser::name(), OrmUnitUser::age()))? + .order_by(OrmUnitUser::id().asc())? + .finish() + })? + .project_tuple::<(i32, String, Option)>(); + assert_eq!( + rows.next().transpose()?, + Some((1, "Alicia".into(), Some(25))) + ); + assert_eq!(rows.next().transpose()?, Some((2, "Bob".into(), Some(20)))); + assert_eq!(rows.next().transpose()?, Some((3, "Cara".into(), None))); + assert!(rows.next().transpose()?.is_none()); + rows.done()?; + Ok(()) + } + #[test] fn query_builder_wrappers_cover_group_having_and_join_variants() -> Result<(), DatabaseError> { let database = build_orm_unit_database()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1ed6d108..e985bf93 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -33,6 +33,7 @@ pub fn parse_sql>(sql: S) -> Result, ParserError> { Parser::parse_sql(&DIALECT, sql.as_ref()) } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::parse_sql; @@ -54,3 +55,4 @@ mod tests { ); } } +// GRCOV_EXCL_STOP diff --git a/src/planner/arena.rs b/src/planner/arena.rs index cf6716a7..8df83e70 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -1235,6 +1235,89 @@ mod tests { assert_eq!(arena.init_value(result), Some(&DataValue::Null)); } + #[test] + fn materializing_scalar_references_preserves_shared_catalog_ids() -> Result<(), DatabaseError> { + use crate::planner::operator::project::ProjectOperator; + use crate::planner::Childrens; + let catalog = TableArenaCell::default(); + let persistent = catalog.borrow_mut().alloc_scalar_query_ref(false); + let mut arena = PlanArena::new(&catalog); + let param = arena.alloc_scalar_query_ref(false); + let result = arena.alloc_scalar_query_ref(true); + let constant = arena.alloc_scalar_query_ref(false); + let exprs = [ + ScalarExpression::OuterParam { + id: param, + ty: LogicalType::Integer, + }, + ScalarExpression::OuterValue { + id: result, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id: constant, + ty: LogicalType::Integer, + }, + ScalarExpression::InitValue { + id: persistent, + ty: LogicalType::Integer, + }, + ] + .into_iter() + .map(|expr| arena.alloc_expression(expr)) + .collect::>(); + let input = LogicalPlan::new( + Operator::Project(ProjectOperator { + exprs: exprs.clone(), + }), + Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), + ); + let source = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(5))); + let mut plan = ScalarQueryInitOperator::build( + input, + LogicalPlan::new(Operator::Dummy, Childrens::None), + exprs[1], + vec![(param, source)], + ); + arena.materialize_scalar_queries(&mut plan)?; + let expected_param = ScalarQueryRef { + arena_id: 0, + pos: 1, + is_outer_value: false, + }; + let expected_result = ScalarQueryRef { + arena_id: 0, + pos: 2, + is_outer_value: true, + }; + assert!( + matches!(arena.expression(exprs[0]), ScalarExpression::OuterParam { id, .. } if *id == expected_param) + ); + assert!( + matches!(arena.expression(exprs[1]), ScalarExpression::OuterValue { id, .. } if *id == expected_result) + ); + assert!( + matches!(arena.expression(exprs[2]), ScalarExpression::InitValue { id, .. } if id.arena_id == 0 && id.pos == 3) + ); + assert!( + matches!(arena.expression(exprs[3]), ScalarExpression::InitValue { id, .. } if *id == persistent) + ); + let Operator::ScalarQueryInit(op) = &plan.operator else { + unreachable!() + }; + assert_eq!(op.param_bindings[0].0, expected_param); + arena.reserve_scalar_query_ref(expected_result); + arena.reserve_scalar_query_ref(constant); + let mut view = ExecMetaArena::new(arena); + let allocated = view.alloc_scalar_query_ref(true); + view.reserve_scalar_query_ref(allocated); + view.set_init_value(allocated, DataValue::Null); + let view = view.boxed(); + assert_eq!(view.init_value(allocated), Some(&DataValue::Null)); + assert_eq!(view.arena_id(), 1); + Ok(()) + } + #[test] fn scalar_query_namespaces_follow_arena_parent() -> Result<(), DatabaseError> { let root = TableArenaCell::default(); @@ -1261,6 +1344,8 @@ mod tests { let mut params = ParamArena::new(&plan, &[], &[])?; assert_eq!(params.arena_id(), 2); let param_local = params.alloc_scalar_query_ref(false); + params.reserve_scalar_query_ref(param_local); + params.reserve_scalar_query_ref(local); assert_eq!( param_local, ScalarQueryRef { @@ -1280,6 +1365,11 @@ mod tests { view.set_init_value(local, DataValue::Int32(33)); assert_eq!(view.init_value(catalog), Some(&DataValue::Int32(11))); assert_eq!(view.init_value(local), Some(&DataValue::Int32(33))); + let mut boxed = view.boxed(); + let reference = boxed.alloc_scalar_query_ref(true); + boxed.reserve_scalar_query_ref(reference); + assert_eq!(reference.arena_id, 2); + assert_eq!(reference.pos, param_local.pos + 1); Ok(()) } diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 855277e2..612bcba5 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -677,6 +677,7 @@ mod tests { } // GRCOV_EXCL_STOP +// GRCOV_EXCL_START #[cfg(test)] pub(crate) mod test { use crate::expression::ScalarExpression; @@ -718,3 +719,4 @@ pub(crate) mod test { } } } +// GRCOV_EXCL_STOP diff --git a/src/planner/operator/join.rs b/src/planner/operator/join.rs index b2e52293..7ae642cc 100644 --- a/src/planner/operator/join.rs +++ b/src/planner/operator/join.rs @@ -272,6 +272,7 @@ pub(crate) fn extract_join_keys( Ok(()) } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::*; @@ -298,3 +299,4 @@ mod tests { assert_eq!(operator.plan_impl(), PlanImpl::NestLoopJoin); } } +// GRCOV_EXCL_STOP diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index cb8830e9..27d696ef 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index b1e3b1ad..668f6768 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -394,6 +394,7 @@ pub fn walk_operator<'a, V: OperatorVisitor<'a>>( } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod tests { use super::*; @@ -677,3 +678,4 @@ pub(crate) mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index 0255a5c2..10dee0e5 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -1,5 +1,7 @@ +// GRCOV_EXCL_START #[cfg(test)] use crate::planner::PlanArena; +// GRCOV_EXCL_STOP // Copyright 2024 KipData/KiteSQL // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -456,6 +458,7 @@ pub fn walk_mut_operator<'a, V: OperatorVisitorMut<'a>>( } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -539,3 +542,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/planner/plan_keeper.rs b/src/planner/plan_keeper.rs index 60e0dc2c..294f69e6 100644 --- a/src/planner/plan_keeper.rs +++ b/src/planner/plan_keeper.rs @@ -32,6 +32,7 @@ pub(crate) struct PlanKeeper<'p> { } impl<'p> PlanKeeper<'p> { + // GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) fn empty() -> Self { Self { @@ -39,6 +40,7 @@ impl<'p> PlanKeeper<'p> { borrowed: None, } } + // GRCOV_EXCL_STOP pub(crate) fn new(input: PlanInput<'p>) -> Self { match input { diff --git a/src/planner/scalar_query_ref.rs b/src/planner/scalar_query_ref.rs index 6b12f531..51e13769 100644 --- a/src/planner/scalar_query_ref.rs +++ b/src/planner/scalar_query_ref.rs @@ -61,6 +61,7 @@ impl ReferenceSerialization for ScalarQueryRef { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; @@ -102,3 +103,4 @@ mod tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/python.rs b/src/python.rs index cca38f74..1cd641a6 100644 --- a/src/python.rs +++ b/src/python.rs @@ -353,6 +353,7 @@ fn kite_sql(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::kite_sql; @@ -582,3 +583,4 @@ except ValueError as exc: }) } } +// GRCOV_EXCL_STOP diff --git a/src/serdes/char_length_units.rs b/src/serdes/char_length_units.rs index 4d499b71..b5b7b19a 100644 --- a/src/serdes/char_length_units.rs +++ b/src/serdes/char_length_units.rs @@ -53,6 +53,7 @@ impl ReferenceSerialization for CharLengthUnits { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::errors::DatabaseError; @@ -94,3 +95,4 @@ pub(crate) mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/serdes/column.rs b/src/serdes/column.rs index 94d786c9..071c5522 100644 --- a/src/serdes/column.rs +++ b/src/serdes/column.rs @@ -128,6 +128,7 @@ impl ReferenceSerialization for ColumnRelation { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::catalog::{ @@ -336,3 +337,4 @@ pub(crate) mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/serdes/data_value.rs b/src/serdes/data_value.rs index e1c2bcde..df0e8eb9 100644 --- a/src/serdes/data_value.rs +++ b/src/serdes/data_value.rs @@ -400,6 +400,7 @@ fn read_char_length_units(reader: &mut R) -> Result u64 { value ^ (value >> 31) } +// GRCOV_EXCL_START #[cfg(test)] mod tests { use super::StableHasher; @@ -154,3 +155,4 @@ mod tests { assert_eq!(chunked.finish(), single.finish()); } } +// GRCOV_EXCL_STOP diff --git a/src/serdes/string.rs b/src/serdes/string.rs index 378f950f..0110b277 100644 --- a/src/serdes/string.rs +++ b/src/serdes/string.rs @@ -74,6 +74,7 @@ impl ReferenceSerialization for Arc { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::errors::DatabaseError; @@ -104,3 +105,4 @@ pub(crate) mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/storage/lmdb.rs b/src/storage/lmdb.rs index 6cfa872c..8e922d4a 100644 --- a/src/storage/lmdb.rs +++ b/src/storage/lmdb.rs @@ -365,6 +365,7 @@ impl Transaction for LmdbTransaction<'_> { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{LmdbConfig, LmdbStorage}; @@ -481,3 +482,4 @@ mod tests { kite_sql.ddl("create table t1 (a int primary key)").unwrap(); } } +// GRCOV_EXCL_STOP diff --git a/src/storage/memory.rs b/src/storage/memory.rs index ede8f0e0..d159c9c0 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -211,6 +211,7 @@ impl Transaction for MemoryTransaction { } } +// GRCOV_EXCL_START #[cfg(all(test, target_arch = "wasm32", feature = "wasm"))] mod wasm_tests { use super::*; @@ -521,3 +522,4 @@ mod native_tests { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 5b756bc0..e2eca4da 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2209,6 +2209,7 @@ impl ViewIter<'_, T> { } } +// GRCOV_EXCL_START #[cfg(test)] pub(crate) fn check_range_rev_matches_range( tx: &mut T, @@ -3207,3 +3208,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index 7be2dba1..10458f9a 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -854,6 +854,7 @@ fn next<'a, D: rocksdb::DBAccess>( } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use crate::catalog::{ColumnCatalog, ColumnDesc, TableName}; @@ -1419,3 +1420,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 07d29733..e3c1290c 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -1058,6 +1058,7 @@ impl TableCodec { } } +// GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; @@ -1866,3 +1867,4 @@ mod tests { assert_eq!(vec[2], &op("V0")); } } +// GRCOV_EXCL_STOP diff --git a/tests/macros-test/src/main.rs b/tests/macros-test/src/main.rs index 5aff4964..fb508310 100644 --- a/tests/macros-test/src/main.rs +++ b/tests/macros-test/src/main.rs @@ -14,6 +14,7 @@ fn main() {} +// GRCOV_EXCL_START #[cfg(test)] mod test { use kite_sql::catalog::column::{ColumnCatalog, ColumnDesc}; @@ -3122,3 +3123,4 @@ mod test { Ok(()) } } +// GRCOV_EXCL_STOP diff --git a/tests/slt/abs.slt b/tests/slt/abs.slt deleted file mode 100644 index 30d5ef97..00000000 --- a/tests/slt/abs.slt +++ /dev/null @@ -1,29 +0,0 @@ -statement ok -create table t_abs(id int primary key, i int, b bigint, s smallint, d double, f float); - -statement ok -insert into t_abs values (0, -3, -9000000000, -7, -1.5, -2.5), (1, 4, 5, 6, 0.25, 1.5), (2, null, null, null, null, null); - -query IIIRR rowsort -select abs(i), abs(b), abs(s), abs(d), abs(f) from t_abs; ----- -3 9000000000 7 1.5 2.5 -4 5 6 0.25 1.5 -NULL NULL NULL NULL NULL - -query I rowsort -select abs(i - b) from t_abs where id < 2; ----- -1 -8999999997 - -query I -select id from t_abs where abs(i) > 3; ----- -1 - -statement error -select abs(-2147483648); - -statement ok -drop table t_abs; diff --git a/tests/slt/aggregation.slt b/tests/slt/aggregation.slt index 70bd2efa..7744ad70 100644 --- a/tests/slt/aggregation.slt +++ b/tests/slt/aggregation.slt @@ -121,5 +121,23 @@ select count(*) from sn where a > 0; ---- 2 +# Pruning unused aggregate outputs must retain group cardinality, including an empty group. +query I rowsort +select 7 from (select a,sum(a) as total from sn group by a) q; +---- +7 +7 + +query I +select 7 from (select sum(a) as total from sn where false) q; +---- +7 + +query I rowsort +select a from (select a,sum(a) as total from sn group by a) q; +---- +1 +2 + statement ok drop table sn diff --git a/tests/slt/basic_test.slt b/tests/slt/basic_test.slt index 48ef1f7e..5fecbd8d 100644 --- a/tests/slt/basic_test.slt +++ b/tests/slt/basic_test.slt @@ -158,3 +158,48 @@ select c1 from t2; statement ok drop table t2 + +# Absolute value: numeric types, NULL and overflow. +statement ok +create table t_abs(id int primary key, i int, b bigint, s smallint, d double, f float); + +statement ok +insert into t_abs values (0, -3, -9000000000, -7, -1.5, -2.5), (1, 4, 5, 6, 0.25, 1.5), (2, null, null, null, null, null); + +query IIIRR rowsort +select abs(i), abs(b), abs(s), abs(d), abs(f) from t_abs; +---- +3 9000000000 7 1.5 2.5 +4 5 6 0.25 1.5 +NULL NULL NULL NULL NULL + +query I rowsort +select abs(i - b) from t_abs where id < 2; +---- +1 +8999999997 + +query I +select id from t_abs where abs(i) > 3; +---- +1 + +statement error +select abs(-2147483648); + +query I +select abs(cast(-7 as tinyint)); +---- +7 + +statement error over flow +select abs(cast(-128 as tinyint)); + +statement error over flow +select abs(cast(-32768 as smallint)); + +statement error over flow +select abs(cast('-9223372036854775808' as bigint)); + +statement ok +drop table t_abs; diff --git a/tests/slt/change_column.slt b/tests/slt/change_column.slt index aad116af..7ba12ce4 100644 --- a/tests/slt/change_column.slt +++ b/tests/slt/change_column.slt @@ -197,3 +197,75 @@ value2 Integer 4 false EMPTY 808 statement ok drop table alter_users + +# Failed conversions must preserve both metadata and all original rows. +statement ok +create table alter_atomic(id int primary key, v bigint unique, label varchar(12)); + +statement ok +insert into alter_atomic values(1,10,'short'),(2,2147483648,'long-value'); + +statement error cannot alter type of indexed column +alter table alter_atomic alter column v type int; + +query TTTTTT +describe alter_atomic; +---- +id Integer 4 false PRIMARY NULL +v Bigint 8 true UNIQUE NULL +label Varchar(Some(12), CHARACTERS) variable true EMPTY NULL + +query IT +select id,label from alter_atomic where v=2147483648; +---- +2 long-value + +query II +select id,v from alter_atomic order by id; +---- +1 10 +2 2147483648 + +statement ok +drop index alter_atomic.uk_v_index; + +statement error out of range integral type conversion +alter table alter_atomic alter column v type int; + +query II +select id,v from alter_atomic order by id; +---- +1 10 +2 2147483648 + +statement error too long +alter table alter_atomic alter column label type varchar(5); + +query IT +select id,label from alter_atomic order by id; +---- +1 short +2 long-value + +statement ok +insert into alter_atomic values(3,30,'still-long'); + +statement ok +update alter_atomic set v=20 where id=2; + +statement ok +alter table alter_atomic alter column v type int; + +query II +select id,v from alter_atomic where v=20; +---- +2 20 + +statement ok +create unique index alter_v on alter_atomic(v); + +statement error the column has been declared unique +insert into alter_atomic values(4,20,'duplicate'); + +statement ok +drop table alter_atomic; diff --git a/tests/slt/char.slt b/tests/slt/char.slt index 12bbe883..766e42d8 100644 --- a/tests/slt/char.slt +++ b/tests/slt/char.slt @@ -65,8 +65,52 @@ select id from t2 where +v1 is null; statement error select -v1 from t2; +# Eliminating unary + must not silently accept non-numeric/non-text types. +statement error unsupported unary operator +select +true; + +statement error unsupported unary operator +select +cast('2024-01-01' as date); + statement ok drop table t1; statement ok -drop table t2; \ No newline at end of file +drop table t2; + +# String length functions. +# char_length / octet_length: integer result, NULL in -> NULL out. + +statement ok +create table s(id int primary key, c varchar); + +statement ok +insert into s values (1, 'ab'), (2, 'abcd'), (3, null); + +query R +select avg(char_length(c)) from s; +---- +3.0 + +query I +select sum(octet_length(c)) from s; +---- +6 + +query I rowsort +select id from s where char_length(c) = 2; +---- +1 + +query I rowsort +select id from s where char_length(c) + 1 > 3; +---- +2 + +query I +select char_length(c) from s where id = 3; +---- +NULL + +statement ok +drop table s diff --git a/tests/slt/crdb/join.slt b/tests/slt/crdb/join.slt index b867d43e..ee22890c 100644 --- a/tests/slt/crdb/join.slt +++ b/tests/slt/crdb/join.slt @@ -1224,6 +1224,21 @@ select a.id,b.id from ij_a a cross join ij_b b where a.k=b.k and a.v+b.v>320 ord ---- 4 3 +# Expression equalities remain residual predicates, not hash keys. +query II +select a.id,b.id from ij_a a inner join ij_b b on a.k=b.k and a.v+75=b.v*1 order by a.id,b.id; +---- +4 1 + +query II +select a.id,b.id from ij_a a inner join ij_b b on a.k=b.k and a.v+10 NULL out. - -statement ok -create table s(id int primary key, c varchar); - -statement ok -insert into s values (1, 'ab'), (2, 'abcd'), (3, null); - -query R -select avg(char_length(c)) from s; ----- -3.0 - -query I -select sum(octet_length(c)) from s; ----- -6 - -query I rowsort -select id from s where char_length(c) = 2; ----- -1 - -query I rowsort -select id from s where char_length(c) + 1 > 3; ----- -2 - -query I -select char_length(c) from s where id = 3; ----- -NULL - -statement ok -drop table s diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index c2ed1b5c..48dda063 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1036,6 +1036,10 @@ select id from corr_rows where b=(select max(x.b) from corr_rows x where x.idall(select b from corr_rows),5 Result<(), DatabaseError> { @@ -1187,3 +1188,4 @@ fn tpcc_db_path() -> PathBuf { .unwrap() .join("kite_sql_tpcc") } +// GRCOV_EXCL_STOP From 808e24dd9eb38e976f6b628b1c2c64b590b79aa9 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 03/10] feat: configure parser depth and unify numeric string comparisons --- fuzz/fuzz_targets/sql_exec.rs | 4 +- src/binder/mod.rs | 4 +- src/binder/parser.rs | 88 ++++++++++++------ src/db.rs | 135 ++++++++++++++++++++++++++-- src/db/prepared.rs | 4 +- src/parser/mod.rs | 13 ++- src/types/mod.rs | 26 ++++++ tests/slt/subquery.slt | 24 +++++ tpcc/src/backend/kitesql_rocksdb.rs | 6 +- 9 files changed, 256 insertions(+), 48 deletions(-) diff --git a/fuzz/fuzz_targets/sql_exec.rs b/fuzz/fuzz_targets/sql_exec.rs index 71be8fe3..af0342fc 100644 --- a/fuzz/fuzz_targets/sql_exec.rs +++ b/fuzz/fuzz_targets/sql_exec.rs @@ -34,7 +34,7 @@ #![no_main] use kite_sql::binder::{command_type, CommandType}; -use kite_sql::db::{prepare_all, DataBaseBuilder, Database, Statement}; +use kite_sql::db::{DataBaseBuilder, Database, Statement}; use kite_sql::storage::lmdb::LmdbStorage; use kite_sql::types::value::DataValue; use libfuzzer_sys::fuzz_target; @@ -135,7 +135,7 @@ fn strip_slt(script: &str) -> String { } fn run_one(db: &mut Database, sql: &str) { - let Ok(statements) = prepare_all(sql) else { + let Ok(statements) = db.prepare_all(sql) else { return; }; // `split(';')` leaves one statement per chunk except for `;` inside diff --git a/src/binder/mod.rs b/src/binder/mod.rs index 6e1619f5..18e13ef8 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -52,9 +52,7 @@ mod update; mod window; #[cfg(feature = "parser")] -pub(crate) use parser::parse_statement; -#[cfg(feature = "parser")] -pub use parser::{command_type, prepare_all, CommandType, Statement}; +pub use parser::{command_type, CommandType, Statement}; #[cfg(feature = "orm")] pub use select::{BindPlanFrom, BindPlanSelectList}; #[cfg(feature = "orm")] diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 9a57d931..05025d2a 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -20,7 +20,7 @@ use super::{is_valid_identifier, Binder, CteBinding, QueryBindStep, SetOperatorK #[cfg(feature = "copy")] use crate::binder::copy::{ExtSource, FileFormat}; use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef, TableName}; -use crate::db::{BindSource, DBTransaction, Database, DatabaseIter, TransactionIter}; +use crate::db::{BindSource, DBTransaction, Database, DatabaseIter, State, TransactionIter}; use crate::errors::{DatabaseError, SqlErrorSpan}; use crate::expression; use crate::expression::agg::AggKind; @@ -28,7 +28,7 @@ use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::window::WindowFunctionKind; use crate::expression::{AliasType, ScalarExpression, TypeCast}; use crate::iter_ext::Itertools; -use crate::parser::parse_sql; +use crate::parser::parse_sql_with_recursion_limit; use crate::planner::operator::alter_table::change_column::{DefaultChange, NotNullChange}; use crate::planner::operator::join::{JoinCondition, JoinOperator as LJoinOperator, JoinType}; use crate::planner::operator::mark_apply::MarkApplyQuantifier; @@ -137,19 +137,23 @@ pub fn command_type(stmt: &Statement) -> Result { } } -/// Parses a single SQL statement into a reusable [`Statement`]. -pub(crate) fn parse_statement>(sql: T) -> Result { - let mut stmts = prepare_all(sql)?; - stmts.pop().ok_or(DatabaseError::EmptyStatement) -} +impl State { + pub(crate) fn prepare_all>( + &self, + sql: T, + ) -> Result, DatabaseError> { + let stmts = parse_sql_with_recursion_limit(sql, self.parser_recursion_limit())?; + if stmts.is_empty() { + return Err(DatabaseError::EmptyStatement); + } + Ok(stmts) + } -/// Parses one or more SQL statements into a vector of [`Statement`] values. -pub fn prepare_all>(sql: T) -> Result, DatabaseError> { - let stmts = parse_sql(sql)?; - if stmts.is_empty() { - return Err(DatabaseError::EmptyStatement); + pub(crate) fn parse_statement(&self, sql: &str) -> Result { + self.prepare_all(sql)? + .pop() + .ok_or(DatabaseError::EmptyStatement) } - Ok(stmts) } fn statement_mutates_catalog_or_statistics(statement: &Statement) -> Result { @@ -160,9 +164,17 @@ fn statement_mutates_catalog_or_statistics(statement: &Statement) -> Result Database { + /// Parses SQL statements using this database's configured parser recursion limit. + pub fn prepare_all>(&self, sql: T) -> Result, DatabaseError> { + self.state.prepare_all(sql) + } + pub fn ddl>(&mut self, sql: T) -> Result<(), DatabaseError> { let sql = sql.as_ref(); - let statements = prepare_all(sql).map_err(|err| err.with_sql_context(sql))?; + let statements = self + .state + .prepare_all(sql) + .map_err(|err| err.with_sql_context(sql))?; for statement in &statements { if !matches!(command_type(statement)?, CommandType::DDL) { @@ -186,7 +198,10 @@ impl Database { ) -> Result, { let sql = sql.as_ref(); - let statements = prepare_all(sql).map_err(|err| err.with_sql_context(sql))?; + let statements = self + .state + .prepare_all(sql) + .map_err(|err| err.with_sql_context(sql))?; self.run_mut_statements(sql, statements, consume) } @@ -228,7 +243,10 @@ impl Database { /// ``` pub fn run>(&self, sql: T) -> Result, DatabaseError> { let sql = sql.as_ref(); - let statements = prepare_all(sql).map_err(|err| err.with_sql_context(sql))?; + let statements = self + .state + .prepare_all(sql) + .map_err(|err| err.with_sql_context(sql))?; let has_catalog_mutation = statements .iter() .try_fold(false, |has_mutation, stmt| { @@ -297,7 +315,10 @@ impl<'txn, S: Storage> DBTransaction<'txn, S> { sql: T, ) -> Result>, DatabaseError> { let sql = sql.as_ref(); - let mut statements = prepare_all(sql).map_err(|err| err.with_sql_context(sql))?; + let mut statements = self + .state + .prepare_all(sql) + .map_err(|err| err.with_sql_context(sql))?; for statement in &statements { if statement_mutates_catalog_or_statistics(statement)? { return Err(DatabaseError::UnsupportedStmt( @@ -3307,6 +3328,7 @@ mod tests { use crate::binder::BinderContext; use crate::db::DataBaseBuilder; use crate::errors::SqlErrorSpan; + use crate::parser::parse_sql; use crate::storage::Storage; use sqlparser::tokenizer::Location; @@ -3341,40 +3363,52 @@ mod tests { #[test] fn test_prepare_and_command_type_classification() -> Result<(), DatabaseError> { + let database = DataBaseBuilder::path(".").build_in_memory()?; assert!(matches!( - prepare_all(""), + database.prepare_all(""), Err(DatabaseError::EmptyStatement) )); assert_eq!( - command_type(&parse_statement("select 1")?)?, + command_type(&database.prepare_all("select 1")?.pop().unwrap())?, CommandType::DQL ); assert_eq!( - command_type(&parse_statement("create table t (id int primary key)")?)?, + command_type( + &database + .prepare_all("create table t (id int primary key)")? + .pop() + .unwrap() + )?, CommandType::DDL ); assert_eq!( - command_type(&parse_statement("analyze table t")?)?, + command_type(&database.prepare_all("analyze table t")?.pop().unwrap())?, CommandType::Analyze ); assert_eq!( - command_type(&parse_statement("insert into t values (1)")?)?, + command_type( + &database + .prepare_all("insert into t values (1)")? + .pop() + .unwrap() + )?, CommandType::DML ); assert_eq!( - command_type(&parse_statement("update t set id = 1")?)?, + command_type(&database.prepare_all("update t set id = 1")?.pop().unwrap())?, CommandType::DML ); assert_eq!( - command_type(&parse_statement("delete from t")?)?, + command_type(&database.prepare_all("delete from t")?.pop().unwrap())?, CommandType::DML ); assert_eq!( - command_type(&parse_statement("truncate table t")?)?, + command_type(&database.prepare_all("truncate table t")?.pop().unwrap())?, CommandType::DML ); - let err = command_type(&parse_statement("start transaction")?).unwrap_err(); + let err = + command_type(&database.prepare_all("start transaction")?.pop().unwrap()).unwrap_err(); assert_unsupported(err, "START TRANSACTION"); Ok(()) @@ -3452,7 +3486,7 @@ mod tests { "only a single ALTER TABLE operation", ); - let mut stmt = parse_statement("alter table t1 drop column c1")?; + let mut stmt = parse_sql("alter table t1 drop column c1")?.pop().unwrap(); let Statement::AlterTable(alter) = &mut stmt else { unreachable!("expected alter table statement") }; diff --git a/src/db.rs b/src/db.rs index 520aab02..cef5b10c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -15,7 +15,7 @@ #[cfg(any(feature = "parser", feature = "orm"))] mod prepared; #[cfg(feature = "parser")] -pub use crate::binder::{prepare_all, Statement}; +pub use crate::binder::Statement; use crate::binder::{Binder, BinderContext}; use crate::catalog::TableName; use crate::errors::DatabaseError; @@ -112,6 +112,7 @@ pub struct DataBaseBuilder { ))] path: PathBuf, histogram_buckets: Option, + parser_recursion_limit: Option, transaction_isolation: Option, #[cfg(all(not(target_arch = "wasm32"), feature = "rocksdb"))] storage_config: StorageConfig, @@ -143,6 +144,7 @@ impl DataBaseBuilder { ))] path, histogram_buckets: None, + parser_recursion_limit: None, transaction_isolation: None, #[cfg(all(not(target_arch = "wasm32"), feature = "rocksdb"))] storage_config: Default::default(), @@ -157,6 +159,14 @@ impl DataBaseBuilder { self } + /// Sets the SQL parser recursion limit. Unset uses the parser's default limit. + /// Higher limits may require a larger thread stack for deeply nested SQL. + #[cfg(feature = "parser")] + pub fn parser_recursion_limit(mut self, limit: usize) -> Self { + self.parser_recursion_limit = Some(limit); + self + } + /// Sets the transaction isolation level used by database-created transactions. pub fn transaction_isolation(mut self, isolation: TransactionIsolationLevel) -> Self { self.transaction_isolation = Some(isolation); @@ -219,7 +229,12 @@ impl DataBaseBuilder { /// Builds a database using a custom storage implementation. pub fn build_with_storage(self, storage: T) -> Result, DatabaseError> { - Self::_build::(storage, self.histogram_buckets, self.transaction_isolation) + Self::_build::( + storage, + self.histogram_buckets, + self.parser_recursion_limit, + self.transaction_isolation, + ) } /// Builds a database for the current target platform. @@ -227,7 +242,12 @@ impl DataBaseBuilder { pub fn build(self) -> Result, DatabaseError> { let storage = MemoryStorage::new(); - Self::_build::(storage, self.histogram_buckets, self.transaction_isolation) + Self::_build::( + storage, + self.histogram_buckets, + self.parser_recursion_limit, + self.transaction_isolation, + ) } /// Builds a RocksDB-backed database. @@ -235,7 +255,12 @@ impl DataBaseBuilder { pub fn build_rocksdb(self) -> Result, DatabaseError> { let storage = RocksStorage::with_config(self.path, self.storage_config)?; - Self::_build::(storage, self.histogram_buckets, self.transaction_isolation) + Self::_build::( + storage, + self.histogram_buckets, + self.parser_recursion_limit, + self.transaction_isolation, + ) } /// Builds an in-memory database. @@ -244,7 +269,12 @@ impl DataBaseBuilder { pub fn build_in_memory(self) -> Result, DatabaseError> { let storage = MemoryStorage::new(); - Self::_build::(storage, self.histogram_buckets, self.transaction_isolation) + Self::_build::( + storage, + self.histogram_buckets, + self.parser_recursion_limit, + self.transaction_isolation, + ) } /// Builds a LMDB-backed database. @@ -252,7 +282,12 @@ impl DataBaseBuilder { pub fn build_lmdb(self) -> Result, DatabaseError> { let storage = LmdbStorage::with_config(self.path, self.lmdb_config)?; - Self::_build::(storage, self.histogram_buckets, self.transaction_isolation) + Self::_build::( + storage, + self.histogram_buckets, + self.parser_recursion_limit, + self.transaction_isolation, + ) } #[cfg(all(not(target_arch = "wasm32"), feature = "rocksdb"))] @@ -264,6 +299,7 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.parser_recursion_limit, self.transaction_isolation, ) } @@ -271,6 +307,7 @@ impl DataBaseBuilder { fn _build( storage: T, histogram_buckets: Option, + parser_recursion_limit: Option, transaction_isolation: Option, ) -> Result, DatabaseError> { if matches!(histogram_buckets, Some(0)) { @@ -278,6 +315,11 @@ impl DataBaseBuilder { "histogram buckets must be >= 1".to_string(), )); } + if matches!(parser_recursion_limit, Some(0)) { + return Err(DatabaseError::InvalidValue( + "parser recursion limit must be >= 1".to_string(), + )); + } let transaction_isolation = transaction_isolation.unwrap_or_else(|| storage.default_transaction_isolation()); storage.validate_transaction_isolation(transaction_isolation)?; @@ -295,6 +337,7 @@ impl DataBaseBuilder { table_arena, optimizer_pipeline: optimizer_pipeline(), histogram_buckets, + parser_recursion_limit, _p: Default::default(), }; @@ -467,10 +510,17 @@ pub(crate) struct State { table_arena: TableArenaCell, optimizer_pipeline: HepOptimizerPipeline, histogram_buckets: Option, + #[cfg_attr(not(feature = "parser"), allow(dead_code))] + parser_recursion_limit: Option, _p: PhantomData, } impl State { + #[cfg(feature = "parser")] + pub(crate) fn parser_recursion_limit(&self) -> Option { + self.parser_recursion_limit + } + fn scala_functions(&self) -> &ScalaFunctions { &self.scala_functions } @@ -1569,7 +1619,7 @@ pub(crate) mod test { kite_sql.ddl("CREATE TABLE onecolumn (id INT PRIMARY KEY, x INT NULL)")?; kite_sql.ddl("CREATE TABLE empty (e_id INT PRIMARY KEY, x INT)")?; - let stmt = crate::binder::parse_statement( + let stmt = kite_sql.state.parse_statement( "SELECT * FROM onecolumn AS a(aid, x) JOIN empty AS b(bid, y) ON a.x = b.y", )?; let transaction = kite_sql.storage.transaction()?; @@ -1652,7 +1702,7 @@ pub(crate) mod test { kite_sql.ddl("CREATE TABLE onecolumn (id INT PRIMARY KEY, x INT NULL)")?; kite_sql.ddl("CREATE TABLE twocolumn (t_id INT PRIMARY KEY, x INT NULL, y INT NULL)")?; - let stmt = crate::binder::parse_statement( + let stmt = kite_sql.state.parse_statement( "SELECT o.x, t.y FROM onecolumn o INNER JOIN twocolumn t ON (o.x=t.x AND t.y=53)", )?; let transaction = kite_sql.storage.transaction()?; @@ -1762,7 +1812,7 @@ pub(crate) mod test { )? .done()?; - let stmt = crate::binder::parse_statement( + let stmt = kite_sql.state.parse_statement( "SELECT o.x, t.y FROM onecolumn o INNER JOIN twocolumn t ON (o.x=t.x AND t.y=53)", )?; let transaction = kite_sql.storage.transaction()?; @@ -2270,6 +2320,73 @@ pub(crate) mod test { )); } + #[test] + fn parser_recursion_limit_applies_to_sql_frontends() -> Result<(), DatabaseError> { + let expression = format!("{}7{}", "(".repeat(8), ")".repeat(8)); + let sql = format!("select {expression}"); + let default = DataBaseBuilder::path(".").build_in_memory()?; + assert_eq!(read_single_i32(default.run(&sql)?)?, 7); + assert_eq!(default.prepare_all(&sql)?.len(), 1); + + let mut db = DataBaseBuilder::path(".") + .parser_recursion_limit(256) + .build_in_memory()?; + assert_eq!(db.prepare_all(&sql)?.len(), 1); + assert_eq!(read_single_i32(db.run(&sql)?)?, 7); + db.ddl(format!("create view deep_view as {sql}"))?; + assert_eq!(read_single_i32(db.run("select * from deep_view")?)?, 7); + db.run_mut(&sql, |iter| { + assert_eq!( + iter.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(7)]) + ); + Ok(()) + })?; + let prepared = db.prepare_sql(&sql, &[])?; + let mut iter = db.execute(&prepared, [])?; + assert_eq!( + iter.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(7)]) + ); + iter.done()?; + let mut tx = db.new_transaction()?; + let mut iter = tx.run(&sql)?; + assert_eq!( + iter.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(7)]) + ); + iter.done()?; + let prepared = tx.prepare_sql(&sql, &[])?; + let mut iter = tx.execute(&prepared, [])?; + assert_eq!( + iter.next_tuple(|_, row| row.values.clone())?, + Some(vec![DataValue::Int32(7)]) + ); + iter.done()?; + tx.commit()?; + + let low = DataBaseBuilder::path(".") + .parser_recursion_limit(4) + .build_in_memory()?; + assert!(low.run("select 1").is_ok()); + assert!(low + .prepare_all(&sql) + .unwrap_err() + .to_string() + .contains("recursion limit exceeded")); + assert!(low + .run(&sql) + .err() + .unwrap() + .to_string() + .contains("recursion limit exceeded")); + assert!(matches!( + DataBaseBuilder::path(".").parser_recursion_limit(0).build_in_memory(), + Err(DatabaseError::InvalidValue(message)) if message == "parser recursion limit must be >= 1" + )); + Ok(()) + } + #[cfg(feature = "lmdb")] #[test] fn test_lmdb_rejects_read_committed_isolation() { diff --git a/src/db/prepared.rs b/src/db/prepared.rs index 2ece306e..405639bd 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -53,7 +53,7 @@ impl Database { sql: &str, params: &[(usize, LogicalType)], ) -> Result, DatabaseError> { - let statement = crate::binder::parse_statement(sql)?; + let statement = self.state.parse_statement(sql)?; let transaction = self .storage .transaction_with_isolation(self.transaction_isolation)?; @@ -85,7 +85,7 @@ impl<'db, S: Storage> DBTransaction<'db, S> { sql: &str, params: &[(usize, LogicalType)], ) -> Result, DatabaseError> { - let statement = crate::binder::parse_statement(sql)?; + let statement = self.state.parse_statement(sql)?; self.state.prepare_plan(&statement, params, &self.inner) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e985bf93..98c76d3c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -30,7 +30,18 @@ const DIALECT: GenericDialect = GenericDialect {}; /// println!("{:?}", ast); /// ``` pub fn parse_sql>(sql: S) -> Result, ParserError> { - Parser::parse_sql(&DIALECT, sql.as_ref()) + parse_sql_with_recursion_limit(sql, None) +} + +pub(crate) fn parse_sql_with_recursion_limit>( + sql: S, + recursion_limit: Option, +) -> Result, ParserError> { + let mut parser = Parser::new(&DIALECT); + if let Some(limit) = recursion_limit { + parser = parser.with_recursion_limit(limit); + } + parser.try_with_sql(sql.as_ref())?.parse_statements() } // GRCOV_EXCL_START diff --git a/src/types/mod.rs b/src/types/mod.rs index bcb0e7ea..32e54759 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -258,6 +258,15 @@ impl LogicalType { return Ok(Cow::Owned(LogicalType::DateTime)); } } + if (left.is_numeric() && matches!(right, LogicalType::Char(..) | LogicalType::Varchar(..))) + || (right.is_numeric() + && matches!(left, LogicalType::Char(..) | LogicalType::Varchar(..))) + { + return Ok(Cow::Owned(LogicalType::Varchar( + None, + CharLengthUnits::Characters, + ))); + } if let (LogicalType::Char(..), LogicalType::Varchar(..)) | (LogicalType::Varchar(..), LogicalType::Char(..)) | (LogicalType::Char(..), LogicalType::Char(..)) @@ -735,6 +744,23 @@ pub(crate) mod test { )?, Cow::Owned(LogicalType::Varchar(None, CharLengthUnits::Characters)) ); + for numeric in [ + LogicalType::Integer, + LogicalType::Bigint, + LogicalType::Double, + ] { + for string in [ + LogicalType::Char(2, CharLengthUnits::Characters), + LogicalType::Varchar(Some(8), CharLengthUnits::Octets), + ] { + for (left, right) in [(&numeric, &string), (&string, &numeric)] { + assert_eq!( + LogicalType::max_logical_type(left, right)?, + Cow::Owned(LogicalType::Varchar(None, CharLengthUnits::Characters)) + ); + } + } + } #[cfg(feature = "time")] { assert_eq!( diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 48dda063..0e3628a8 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1083,6 +1083,30 @@ select id,b in (select b from corr_rows where id=2) as matched,b not in (select statement error correlated quantified subqueries in SELECT are not supported yet select b in (select x.b from corr_rows x where x.id( database: &mut Database, sql: &str, ) -> Result<(), TpccError> { - let statements = prepare_all(sql)?; + let statements = database.prepare_all(sql)?; let all_ddl = statements.iter().try_fold(true, |all_ddl, statement| { Ok::<_, kite_sql::errors::DatabaseError>( all_ddl && matches!(command_type(statement)?, CommandType::DDL), From f5ccd1f99a43cc2d04641976f8499ff8f237dd1c Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 04/10] feat: support variable-length binary values --- src/binder/parser.rs | 39 ++++++++++-- src/execution/spill/codec.rs | 6 ++ src/orm/mod.rs | 45 ++++++++++++- src/python.rs | 9 ++- src/serdes/data_value.rs | 33 +++++++++- src/serdes/evaluator.rs | 9 +++ src/storage/table_codec.rs | 1 + src/types/evaluator/binary.rs | 35 ++++++++++ src/types/evaluator/bytes.rs | 37 +++++++++++ src/types/evaluator/cast.rs | 12 ++++ src/types/evaluator/mod.rs | 4 ++ src/types/mod.rs | 26 +++++++- src/types/serialize.rs | 50 +++++++++++++++ src/types/value.rs | 104 ++++++++++++++++++++++++++++++ src/wasm.rs | 3 +- tests/slt/binary.slt | 116 ++++++++++++++++++++++++++++++++++ tpcc/src/backend/sqlite.rs | 1 + 17 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 src/types/evaluator/bytes.rs create mode 100644 tests/slt/binary.slt diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 05025d2a..c42cf04c 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -42,11 +42,11 @@ use crate::storage::{Storage, Transaction}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, ColumnId, LogicalType}; pub(super) use sqlparser::ast::{ - AlterColumnOperation, AlterTableOperation, Assignment, AssignmentTarget, BinaryOperator, - ColumnDef, ColumnOption, CreateView, DataType, DescribeAlias, Distinct, DuplicateTreatment, - Expr, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArguments, GroupByExpr, Ident, - IndexColumn, Join, JoinConstraint, JoinOperator, LimitClause, ObjectName, ObjectNamePart, - ObjectType, OrderByExpr, OrderByKind, Query, Select, SelectInto, SelectItem, + AlterColumnOperation, AlterTableOperation, Assignment, AssignmentTarget, BinaryLength, + BinaryOperator, ColumnDef, ColumnOption, CreateView, DataType, DescribeAlias, Distinct, + DuplicateTreatment, Expr, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArguments, + GroupByExpr, Ident, IndexColumn, Join, JoinConstraint, JoinOperator, LimitClause, ObjectName, + ObjectNamePart, ObjectType, OrderByExpr, OrderByKind, Query, Select, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Spanned, TableAlias, TableConstraint, TableFactor, TableObject, TableWithJoins, TypedString, UnaryOperator, Value, ValueWithSpan, WindowType, @@ -1667,6 +1667,24 @@ impl TryFrom<&Value> for DataValue { } } Value::SingleQuotedString(s) | Value::DoubleQuotedString(s) => s.clone().into(), + Value::HexStringLiteral(hex) => { + if hex.len() % 2 != 0 { + return Err(DatabaseError::InvalidValue( + "hex literal must contain pairs of digits".to_string(), + )); + } + let mut bytes = Vec::with_capacity(hex.len() / 2); + for pair in hex.as_bytes().chunks_exact(2) { + let Some(high) = (pair[0] as char).to_digit(16) else { + return Err(DatabaseError::InvalidValue("invalid hex digit".to_string())); + }; + let Some(low) = (pair[1] as char).to_digit(16) else { + return Err(DatabaseError::InvalidValue("invalid hex digit".to_string())); + }; + bytes.push((high * 16 + low) as u8); + } + bytes.into() + } Value::Boolean(b) => (*b).into(), Value::Null => Self::Null, v => return Err(DatabaseError::UnsupportedStmt(format!("{v:?}"))), @@ -1727,6 +1745,17 @@ impl TryFrom for LogicalType { .unwrap_or(CharLengthUnits::Characters), )) } + DataType::Varbinary(length) => { + let len = match length { + Some(BinaryLength::IntegerLength { length }) => Some(u32::try_from(length)?), + None | Some(BinaryLength::Max) => None, + }; + Ok(Self::Varbinary(len)) + } + DataType::Blob(length) | DataType::Bytes(length) => { + Ok(Self::Varbinary(length.map(u32::try_from).transpose()?)) + } + DataType::Bytea => Ok(Self::Varbinary(None)), DataType::String(_) | DataType::Text => { Ok(Self::Varchar(None, CharLengthUnits::Characters)) } diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 511116d4..37ec0e17 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -257,6 +257,7 @@ impl SpillCodec for Tuple { fn estimated_dynamic_value_size(value: &DataValue) -> usize { match value { DataValue::Utf8 { value, .. } => value.capacity(), + DataValue::Binary { value, .. } => value.capacity(), DataValue::Tuple(values) => values .capacity() .saturating_mul(size_of::()) @@ -290,6 +291,11 @@ mod tests { fn values() -> Vec { let values = vec![ + DataValue::Binary { + value: vec![0, 255, 128], + len: Some(8), + }, + DataValue::from(Vec::::new()), DataValue::Null, DataValue::Boolean(false), DataValue::Boolean(true), diff --git a/src/orm/mod.rs b/src/orm/mod.rs index d7dcc50a..52c6e582 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -3057,7 +3057,21 @@ impl_from_data_value_by_method!(f64, double); #[cfg(feature = "decimal")] impl_from_data_value_by_method!(Decimal, decimal); -impl_to_data_value_by_clone!(bool, i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, String); +impl_to_data_value_by_clone!( + bool, + i8, + i16, + i32, + i64, + u8, + u16, + u32, + u64, + f32, + f64, + String, + Vec +); #[cfg(feature = "decimal")] impl_to_data_value_by_clone!(Decimal); @@ -3073,6 +3087,7 @@ macro_rules! impl_model_column_type { }; } +impl_model_column_type!(LogicalType::Varbinary(None); Vec); impl_model_column_type!(LogicalType::Boolean; bool); impl_model_column_type!(LogicalType::Tinyint; i8); impl_model_column_type!(LogicalType::Smallint; i16); @@ -3127,6 +3142,20 @@ mod chrono_orm { } } +impl FromDataValue for Vec { + fn logical_type() -> Option { + LogicalType::type_trans::() + } + + fn from_data_value(value: DataValue) -> Result { + if let DataValue::Binary { value, .. } = value { + Ok(value) + } else { + Err(invalid_from_data_value::(&value)) + } + } +} + impl FromDataValue for String { fn logical_type() -> Option { LogicalType::type_trans::() @@ -3740,6 +3769,20 @@ mod tests { Some(9) ); + assert_eq!( + Vec::::from_data_value(DataValue::from(vec![0, 255]))?, + vec![0, 255] + ); + assert_eq!( + vec![0u8, 255].to_data_value(), + DataValue::from(vec![0, 255]) + ); + assert_eq!( + as ModelColumnType>::logical_type(), + LogicalType::Varbinary(None) + ); + assert_eq!(Option::>::from_data_value(DataValue::Null)?, None); + assert!(Vec::::from_data_value(DataValue::Int32(1)).is_err()); assert_eq!(true.to_data_value(), DataValue::Boolean(true)); assert_eq!("name".to_data_value(), DataValue::from("name".to_string())); assert_eq!(Option::::None.to_data_value(), DataValue::Null); diff --git a/src/python.rs b/src/python.rs index 1cd641a6..91da11a5 100644 --- a/src/python.rs +++ b/src/python.rs @@ -25,7 +25,7 @@ use crate::types::tuple::{SchemaView, Tuple}; use crate::types::value::DataValue; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PyModule}; +use pyo3::types::{PyBytes, PyDict, PyList, PyModule}; fn to_py_err(err: impl ToString) -> PyErr { PyRuntimeError::new_err(err.to_string()) @@ -51,6 +51,7 @@ fn data_value_to_py(py: Python<'_>, value: &DataValue) -> PyResult { DataValue::UInt16(value) => value.into_py(py), DataValue::UInt32(value) => value.into_py(py), DataValue::UInt64(value) => value.into_py(py), + DataValue::Binary { value, .. } => PyBytes::new(py, value).into_any().unbind(), DataValue::Utf8 { value, .. } => value.clone().into_py(py), DataValue::Date32(_) | DataValue::Date64(_) @@ -450,6 +451,12 @@ stream.finish() assert streamed == [[0, 0], [1, 11]] db.ddl("drop table my_struct") +db.ddl("create table binary_rows (id int primary key, b varbinary(4))") +db.execute("insert into binary_rows values (1,x'00ff'),(2,x''),(3,null)") +assert [row["values"] for row in db.run("select b from binary_rows order by id").rows()] == [ + [b'\x00\xff'], [b''], [None], +] +db.ddl("drop table binary_rows") "# ), )?; diff --git a/src/serdes/data_value.rs b/src/serdes/data_value.rs index df0e8eb9..718897f5 100644 --- a/src/serdes/data_value.rs +++ b/src/serdes/data_value.rs @@ -43,6 +43,7 @@ const TAG_TIME32: u8 = 15; const TAG_TIME64: u8 = 16; const TAG_DECIMAL: u8 = 17; const TAG_TUPLE: u8 = 18; +const TAG_BINARY: u8 = 19; impl ReferenceSerialization for Utf8Type { fn encode( @@ -144,6 +145,16 @@ impl DataValue { write_u8(writer, TAG_UINT64)?; write_u64(writer, *value) } + DataValue::Binary { value, len } => { + write_u8(writer, TAG_BINARY)?; + write_len(writer, value.len())?; + writer.write_all(value)?; + write_bool(writer, len.is_some())?; + if let Some(len) = len { + write_u32(writer, *len)?; + } + Ok(()) + } DataValue::Utf8 { value, ty, unit } => { write_u8(writer, TAG_UTF8)?; write_string(writer, value)?; @@ -200,6 +211,17 @@ impl DataValue { TAG_UINT16 => Ok(DataValue::UInt16(read_u16(reader)?)), TAG_UINT32 => Ok(DataValue::UInt32(read_u32(reader)?)), TAG_UINT64 => Ok(DataValue::UInt64(read_u64(reader)?)), + TAG_BINARY => { + let size = read_len(reader)?; + let mut value = vec![0; size]; + reader.read_exact(&mut value)?; + let len = if read_bool(reader)? { + Some(read_u32(reader)?) + } else { + None + }; + Ok(DataValue::Binary { value, len }) + } TAG_UTF8 => Ok(DataValue::Utf8 { value: read_string(reader)?, ty: read_utf8_type(reader)?, @@ -403,7 +425,7 @@ fn read_char_length_units(reader: &mut R) -> Result Result<(), DatabaseError> { let sources = vec![ + DataValue::Binary { + value: vec![0, 255, 128], + len: Some(8), + }, + DataValue::from(Vec::::new()), DataValue::Null, DataValue::Boolean(true), DataValue::Float32(OrderedFloat(32.5)), @@ -497,6 +524,10 @@ pub(crate) mod test { assert_invalid_value(&[u8::MAX], "invalid data value tag"); assert_invalid_value(&[TAG_BOOLEAN, 2], "invalid bool value"); assert_invalid_value(&[TAG_INT64, 1], "failed to fill whole buffer"); + assert_invalid_value( + &[TAG_BINARY, 2, 0, 0, 0, 255], + "failed to fill whole buffer", + ); let mut utf8 = vec![TAG_UTF8]; utf8.extend(0u32.to_le_bytes()); diff --git a/src/serdes/evaluator.rs b/src/serdes/evaluator.rs index 43d62bc2..4edd37ff 100644 --- a/src/serdes/evaluator.rs +++ b/src/serdes/evaluator.rs @@ -88,6 +88,10 @@ impl ReferenceSerialization for CastEvaluatorParams { precision.encode(writer, is_direct, reference_tables, arena)?; zone.encode(writer, is_direct, reference_tables, arena) } + CastEvaluatorParams::Binary { len } => { + 7u8.encode(writer, is_direct, reference_tables, arena)?; + len.encode(writer, is_direct, reference_tables, arena) + } CastEvaluatorParams::Tuple { evaluators } => { 6u8.encode(writer, is_direct, reference_tables, arena)?; evaluators.encode(writer, is_direct, reference_tables, arena) @@ -134,6 +138,9 @@ impl ReferenceSerialization for CastEvaluatorParams { precision: Option::::decode(reader, context, reference_tables, arena)?, zone: bool::decode(reader, context, reference_tables, arena)?, }, + 7 => CastEvaluatorParams::Binary { + len: Option::::decode(reader, context, reference_tables, arena)?, + }, 6 => CastEvaluatorParams::Tuple { evaluators: Vec::::decode( reader, @@ -286,6 +293,8 @@ mod tests { fn cast_evaluator_params_serialization_roundtrips_all_variants() -> Result<(), DatabaseError> { roundtrip_cast_params(CastEvaluatorParams::Identity, 0)?; roundtrip_cast_params(CastEvaluatorParams::Unit, 1)?; + roundtrip_cast_params(CastEvaluatorParams::Binary { len: Some(8) }, 7)?; + roundtrip_cast_params(CastEvaluatorParams::Binary { len: None }, 7)?; roundtrip_cast_params( CastEvaluatorParams::String { len: Some(12), diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index e3c1290c..4d9672e4 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -212,6 +212,7 @@ impl TableCodec { | LogicalType::UBigint | LogicalType::Char(..) | LogicalType::Varchar(..) + | LogicalType::Varbinary(..) | LogicalType::Date | LogicalType::DateTime | LogicalType::Time(..) diff --git a/src/types/evaluator/binary.rs b/src/types/evaluator/binary.rs index 26cd8252..a6b45493 100644 --- a/src/types/evaluator/binary.rs +++ b/src/types/evaluator/binary.rs @@ -15,6 +15,7 @@ use crate::errors::DatabaseError; use crate::expression::BinaryOperator; use crate::types::evaluator::boolean::*; +use crate::types::evaluator::bytes::*; #[cfg(feature = "time")] use crate::types::evaluator::date::*; #[cfg(feature = "time")] @@ -133,6 +134,7 @@ const BINARY_BOOLEAN_BASE: u16 = BINARY_TIME64_BASE + TIMESTAMP_OPS_LEN; const BINARY_UTF8_BASE: u16 = BINARY_BOOLEAN_BASE + BOOLEAN_OPS_LEN; const BINARY_SQL_NULL: u16 = BINARY_UTF8_BASE + UTF8_OPS_LEN; const BINARY_TUPLE_BASE: u16 = BINARY_SQL_NULL + SQL_NULL_OPS_LEN; +const BINARY_BYTES_BASE: u16 = BINARY_TUPLE_BASE + TUPLE_LT_EQ_OFFSET + 1; // Evaluator positions are serialized ABI. Do not reorder or reuse existing // positions; only append new positions at the end of the current layout. @@ -181,6 +183,21 @@ pub fn binary_create( ) -> Result { let ty = ty.as_ref(); match ty { + LogicalType::Varbinary(_) => match op { + BinaryOperator::Eq => unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_EQ_OFFSET)), + BinaryOperator::NotEq => { + unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_NOT_EQ_OFFSET)) + } + BinaryOperator::Gt => unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_GT_OFFSET)), + BinaryOperator::GtEq => { + unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_GT_EQ_OFFSET)) + } + BinaryOperator::Lt => unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_LT_OFFSET)), + BinaryOperator::LtEq => { + unit_binary_ref(binary_pos(BINARY_BYTES_BASE, TUPLE_LT_EQ_OFFSET)) + } + _ => Err(DatabaseError::UnsupportedBinaryOperator(ty.clone(), op)), + }, LogicalType::Tinyint => numeric_binary_ref(BINARY_INT8_BASE, ty, op), LogicalType::Smallint => numeric_binary_ref(BINARY_INT16_BASE, ty, op), LogicalType::Integer => numeric_binary_ref(BINARY_INT32_BASE, ty, op), @@ -512,6 +529,24 @@ pub(crate) fn eval_binary( x if x == binary_pos(BINARY_TUPLE_BASE, TUPLE_LT_EQ_OFFSET) => { tuple_lt_eq_binary_eval(left, right) } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_EQ_OFFSET) => { + bytes_eq_binary_eval(left, right) + } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_NOT_EQ_OFFSET) => { + bytes_not_eq_binary_eval(left, right) + } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_GT_OFFSET) => { + bytes_gt_binary_eval(left, right) + } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_GT_EQ_OFFSET) => { + bytes_gt_eq_binary_eval(left, right) + } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_LT_OFFSET) => { + bytes_lt_binary_eval(left, right) + } + x if x == binary_pos(BINARY_BYTES_BASE, TUPLE_LT_EQ_OFFSET) => { + bytes_lt_eq_binary_eval(left, right) + } _ => unreachable!("unknown binary evaluator position {pos}"), } } diff --git a/src/types/evaluator/bytes.rs b/src/types/evaluator/bytes.rs new file mode 100644 index 00000000..b11f50be --- /dev/null +++ b/src/types/evaluator/bytes.rs @@ -0,0 +1,37 @@ +// Copyright 2026 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::types::value::DataValue; + +macro_rules! bytes_binary { + ($name:ident, $op:tt) => { + pub fn $name(left: &DataValue, right: &DataValue) -> Result { + Ok(match (left, right) { + (DataValue::Binary { value: left, .. }, DataValue::Binary { value: right, .. }) => { + DataValue::Boolean(left $op right) + } + (DataValue::Null, _) | (_, DataValue::Null) => DataValue::Null, + _ => unreachable!(), + }) + } + }; +} + +bytes_binary!(bytes_eq_binary_eval, ==); +bytes_binary!(bytes_not_eq_binary_eval, !=); +bytes_binary!(bytes_gt_binary_eval, >); +bytes_binary!(bytes_gt_eq_binary_eval, >=); +bytes_binary!(bytes_lt_binary_eval, <); +bytes_binary!(bytes_lt_eq_binary_eval, <=); diff --git a/src/types/evaluator/cast.rs b/src/types/evaluator/cast.rs index c7bd0262..da2c2396 100644 --- a/src/types/evaluator/cast.rs +++ b/src/types/evaluator/cast.rs @@ -73,6 +73,7 @@ const CAST_TIMESTAMP: u16 = 17; #[cfg(feature = "decimal")] const CAST_DECIMAL: u16 = 18; const CAST_TUPLE: u16 = 19; +const CAST_BINARY: u16 = 20; // Cast positions are serialized ABI. Type codes above must never be reordered // or reused; new cast families should append a new code and keep old positions. @@ -101,6 +102,7 @@ fn cast_type_code(ty: &LogicalType) -> u16 { #[cfg(not(feature = "decimal"))] LogicalType::Decimal(_, _) => unreachable!("DECIMAL requires the `decimal` feature"), LogicalType::Tuple(_) => CAST_TUPLE, + LogicalType::Varbinary(_) => CAST_BINARY, } } @@ -551,6 +553,10 @@ pub fn cast_create( } match (from, to) { + (LogicalType::Varbinary(_), LogicalType::Varbinary(len)) => Ok(CastEvaluatorRef::new( + cast_pos(from, to), + CastEvaluatorParams::Binary { len: *len }, + )), (LogicalType::SqlNull, _) => cast_ref!(from, to), (_, LogicalType::SqlNull) => cast_ref!(from, to), (LogicalType::Boolean, LogicalType::Tinyint) => { @@ -927,6 +933,12 @@ impl CastEvaluatorRef { } match (from_code, to_code) { + (CAST_BINARY, CAST_BINARY) => { + let CastEvaluatorParams::Binary { len } = params else { + unreachable!() + }; + value.clone().cast(&LogicalType::Varbinary(*len)) + } (CAST_SQL_NULL, _) => run!(null_cast_eval), (_, CAST_SQL_NULL) => run!(to_sql_null_cast_eval), (CAST_BOOLEAN, CAST_TINYINT) => run!(boolean_to_tinyint_cast_eval), diff --git a/src/types/evaluator/mod.rs b/src/types/evaluator/mod.rs index 9b9e41a6..10d1f3da 100644 --- a/src/types/evaluator/mod.rs +++ b/src/types/evaluator/mod.rs @@ -14,6 +14,7 @@ pub mod binary; pub mod boolean; +pub mod bytes; pub mod cast; #[cfg(feature = "time")] pub mod date; @@ -118,6 +119,9 @@ pub enum CastEvaluatorParams { Tuple { evaluators: Vec, }, + Binary { + len: Option, + }, } impl CastEvaluatorRef { diff --git a/src/types/mod.rs b/src/types/mod.rs index 32e54759..78fcdf4b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -70,6 +70,7 @@ pub enum LogicalType { // decimal (precision, scale) Decimal(Option, Option), Tuple(Vec), + Varbinary(Option), } impl LogicalType { @@ -103,6 +104,8 @@ impl LogicalType { Some(LogicalType::Float) } else if type_id == TypeId::of::() { Some(LogicalType::Double) + } else if type_id == TypeId::of::>() { + Some(LogicalType::Varbinary(None)) } else if type_id == TypeId::of::() { Some(LogicalType::Varchar(None, CharLengthUnits::Characters)) } else { @@ -137,7 +140,7 @@ impl LogicalType { LogicalType::Float => Some(4), LogicalType::Double => Some(8), /// Note: The non-fixed length type's raw_len is None e.g. Varchar - LogicalType::Varchar(_, _) => None, + LogicalType::Varchar(_, _) | LogicalType::Varbinary(_) => None, LogicalType::Char(len, unit) => match unit { CharLengthUnits::Characters => None, CharLengthUnits::Octets => Some(*len as usize), @@ -258,6 +261,9 @@ impl LogicalType { return Ok(Cow::Owned(LogicalType::DateTime)); } } + if let (LogicalType::Varbinary(_), LogicalType::Varbinary(_)) = (left, right) { + return Ok(Cow::Owned(LogicalType::Varbinary(None))); + } if (left.is_numeric() && matches!(right, LogicalType::Char(..) | LogicalType::Varchar(..))) || (right.is_numeric() && matches!(left, LogicalType::Char(..) | LogicalType::Varchar(..))) @@ -407,7 +413,7 @@ impl LogicalType { LogicalType::Float => matches!(to, LogicalType::Double | LogicalType::Decimal(_, _)), LogicalType::Double => matches!(to, LogicalType::Decimal(_, _)), LogicalType::Char(..) => false, - LogicalType::Varchar(..) => false, + LogicalType::Varchar(..) | LogicalType::Varbinary(..) => false, LogicalType::Date => matches!( to, LogicalType::DateTime | LogicalType::Varchar(..) | LogicalType::Char(..) @@ -444,6 +450,7 @@ impl std::fmt::Display for LogicalType { LogicalType::Double => write!(f, "Double")?, LogicalType::Char(len, units) => write!(f, "Char({len}, {units})")?, LogicalType::Varchar(len, units) => write!(f, "Varchar({len:?}, {units})")?, + LogicalType::Varbinary(len) => write!(f, "Varbinary({len:?})")?, LogicalType::Date => write!(f, "Date")?, LogicalType::DateTime => write!(f, "DateTime")?, LogicalType::TimeStamp(precision, zone) => { @@ -521,7 +528,10 @@ pub(crate) mod test { LogicalType::type_trans::(), Some(LogicalType::Varchar(None, CharLengthUnits::Characters)) ); - assert_eq!(LogicalType::type_trans::>(), None); + assert_eq!( + LogicalType::type_trans::>(), + Some(LogicalType::Varbinary(None)) + ); #[cfg(feature = "decimal")] assert_eq!( @@ -975,6 +985,16 @@ pub(crate) mod test { &mut reference_tables, LogicalType::Varchar(None, CharLengthUnits::Octets), )?; + fn_assert( + &mut cursor, + &mut reference_tables, + LogicalType::Varbinary(Some(8)), + )?; + fn_assert( + &mut cursor, + &mut reference_tables, + LogicalType::Varbinary(None), + )?; fn_assert(&mut cursor, &mut reference_tables, LogicalType::Date)?; fn_assert(&mut cursor, &mut reference_tables, LogicalType::DateTime)?; fn_assert( diff --git a/src/types/serialize.rs b/src/types/serialize.rs index 18aa64ef..1357895b 100644 --- a/src/types/serialize.rs +++ b/src/types/serialize.rs @@ -92,11 +92,17 @@ pub enum TupleValueSerializableImpl { Decimal, SkipFixed(usize), SkipVariable, + Varbinary { + len: Option, + }, } impl TupleValueSerializable for TupleValueSerializableImpl { fn to_raw(&self, value: &DataValue, writer: &mut W) -> Result<(), DatabaseError> { match self { + TupleValueSerializableImpl::Varbinary { len } => { + VarbinarySerializable { len: *len }.to_raw(value, writer) + } TupleValueSerializableImpl::Boolean => BooleanSerializable.to_raw(value, writer), TupleValueSerializableImpl::Int8 => Int8Serializable.to_raw(value, writer), TupleValueSerializableImpl::Int16 => Int16Serializable.to_raw(value, writer), @@ -137,6 +143,9 @@ impl TupleValueSerializable for TupleValueSerializableImpl { fn from_raw(&self, reader: &mut Cursor<&[u8]>) -> Result { match self { + TupleValueSerializableImpl::Varbinary { len } => { + VarbinarySerializable { len: *len }.from_raw(reader) + } TupleValueSerializableImpl::Boolean => BooleanSerializable.from_raw(reader), TupleValueSerializableImpl::Int8 => Int8Serializable.from_raw(reader), TupleValueSerializableImpl::Int16 => Int16Serializable.from_raw(reader), @@ -181,6 +190,9 @@ impl TupleValueSerializable for TupleValueSerializableImpl { values: &mut std::vec::Vec, ) -> Result<(), DatabaseError> { match self { + TupleValueSerializableImpl::Varbinary { len } => { + VarbinarySerializable { len: *len }.filling_value(reader, values) + } TupleValueSerializableImpl::Boolean => { BooleanSerializable.filling_value(reader, values) } @@ -269,6 +281,11 @@ struct VarcharSerializable { unit: CharLengthUnits, } +#[derive(Debug)] +struct VarbinarySerializable { + len: Option, +} + #[derive(Debug)] struct DateSerializable; #[derive(Debug)] @@ -488,6 +505,27 @@ impl TupleValueSerializable for VarcharSerializable { } } +impl TupleValueSerializable for VarbinarySerializable { + fn to_raw(&self, value: &DataValue, writer: &mut W) -> Result<(), DatabaseError> { + let DataValue::Binary { value, .. } = value else { + unsafe { std::hint::unreachable_unchecked() } + }; + write_u32_le(writer, value.len() as u32)?; + writer.write_all(value)?; + Ok(()) + } + + fn from_raw(&self, reader: &mut Cursor<&[u8]>) -> Result { + let size = read_u32_le(reader)? as usize; + let mut value = vec![0; size]; + reader.read_exact(&mut value)?; + Ok(DataValue::Binary { + value, + len: self.len, + }) + } +} + impl_tuple_value_serializable!( DateSerializable, Date32, @@ -613,6 +651,7 @@ impl LogicalType { pub fn serializable(&self) -> TupleValueSerializableImpl { match self { + LogicalType::Varbinary(len) => TupleValueSerializableImpl::Varbinary { len: *len }, LogicalType::Boolean => TupleValueSerializableImpl::Boolean, LogicalType::Tinyint => TupleValueSerializableImpl::Int8, LogicalType::UTinyint => TupleValueSerializableImpl::UInt8, @@ -669,6 +708,17 @@ mod tests { fn test_tuple_value_serializable_impl_roundtrips_all_value_variants( ) -> Result<(), DatabaseError> { let cases = vec![ + ( + TupleValueSerializableImpl::Varbinary { len: Some(8) }, + DataValue::Binary { + value: vec![0, 255, 128], + len: Some(8), + }, + ), + ( + TupleValueSerializableImpl::Varbinary { len: None }, + DataValue::from(Vec::::new()), + ), ( TupleValueSerializableImpl::Boolean, DataValue::Boolean(true), diff --git a/src/types/value.rs b/src/types/value.rs index 7564783c..b23e37ee 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -163,6 +163,10 @@ pub enum DataValue { #[cfg(feature = "decimal")] Decimal(Decimal), Tuple(Vec), + Binary { + value: Vec, + len: Option, + }, } /// A read-only view of a value, including an unmaterialized tuple slice. @@ -381,6 +385,8 @@ impl PartialEq for DataValue { (UInt32(_), _) => false, (UInt64(v1), UInt64(v2)) => v1.eq(v2), (UInt64(_), _) => false, + (Binary { value: v1, .. }, Binary { value: v2, .. }) => v1.eq(v2), + (Binary { .. }, _) => false, (Utf8 { value: v1, .. }, Utf8 { value: v2, .. }) => v1.eq(v2), (Utf8 { .. }, _) => false, (Null, Null) => true, @@ -473,6 +479,8 @@ impl PartialOrd for DataValue { (UInt32(_), _) => None, (UInt64(v1), UInt64(v2)) => v1.partial_cmp(v2), (UInt64(_), _) => None, + (Binary { value: v1, .. }, Binary { value: v2, .. }) => v1.partial_cmp(v2), + (Binary { .. }, _) => None, (Utf8 { value: v1, .. }, Utf8 { value: v2, .. }) => v1.partial_cmp(v2), (Utf8 { .. }, _) => None, (Null, Null) => Some(Ordering::Equal), @@ -565,6 +573,10 @@ impl Hash for DataValue { 11u8.hash(state); v.hash(state); } + Binary { value, .. } => { + 19u8.hash(state); + value.hash(state); + } Utf8 { value: v, .. } => { 12u8.hash(state); v.hash(state); @@ -709,6 +721,7 @@ impl DataValue { | DataValue::Float64(_) | DataValue::Date64(_) | DataValue::Time64(_, _, _) => 8, + DataValue::Binary { value, .. } => std::mem::size_of::() + value.len(), DataValue::Utf8 { value, ty, unit } => match (ty, unit) { (Utf8Type::Variable(_), _) => std::mem::size_of::() + value.len(), (Utf8Type::Fixed(len), CharLengthUnits::Characters) => { @@ -747,6 +760,14 @@ impl DataValue { } } + pub fn binary(&self) -> Option<&[u8]> { + if let DataValue::Binary { value, .. } = self { + Some(value) + } else { + None + } + } + pub fn new_utf8(string: String) -> Self { DataValue::Utf8 { ty: Utf8Type::Fixed(string.len() as u32), @@ -766,6 +787,9 @@ impl DataValue { #[inline] pub(crate) fn check_len(&self, logic_type: &LogicalType) -> Result<(), DatabaseError> { let is_over_len = match (logic_type, self) { + (LogicalType::Varbinary(Some(len)), DataValue::Binary { value, .. }) => { + value.len() > *len as usize + } (LogicalType::Varchar(None, _), _) => false, ( LogicalType::Varchar(Some(len), CharLengthUnits::Characters), @@ -885,6 +909,10 @@ impl DataValue { ty: Utf8Type::Variable(*len), unit: *unit, }, + LogicalType::Varbinary(len) => DataValue::Binary { + value: Vec::new(), + len: *len, + }, LogicalType::Date => { #[cfg(feature = "time")] { @@ -979,6 +1007,7 @@ impl DataValue { unit, .. } => LogicalType::Char(*len, *unit), + DataValue::Binary { len, .. } => LogicalType::Varbinary(*len), DataValue::Date32(_) => LogicalType::Date, DataValue::Date64(_) => LogicalType::DateTime, DataValue::Time32(..) => LogicalType::Time(None), @@ -1115,6 +1144,7 @@ impl DataValue { DataValue::UInt32(v) => encode_u!(b, v), DataValue::Time32(v, ..) => encode_u!(b, v), DataValue::UInt64(v) => encode_u!(b, v), + DataValue::Binary { value, .. } => Self::encode_string(b, value), DataValue::Utf8 { value: v, .. } => Self::encode_string(b, v.as_bytes()), DataValue::Boolean(v) => b.push_byte(if *v { b'1' } else { b'0' }), DataValue::Float32(f) => { @@ -1236,6 +1266,10 @@ impl DataValue { reader.read_exact(&mut b)?; Ok(DataValue::Boolean(b[0] == b'1')) } + LogicalType::Varbinary(len) => Ok(DataValue::Binary { + value: Self::decode_string(reader)?, + len: *len, + }), LogicalType::Varchar(len, unit) => Ok(DataValue::Utf8 { value: String::from_utf8(Self::decode_string(reader)?)?, ty: Utf8Type::Variable(*len), @@ -1468,6 +1502,11 @@ impl DataValue { Ok(DataValue::Parameter { id, ty: ty.clone() }) } (DataValue::Null, _) => Ok(DataValue::Null), + (DataValue::Binary { value, .. }, LogicalType::Varbinary(len)) => { + let result = DataValue::Binary { value, len: *len }; + result.check_len(to)?; + Ok(result) + } (DataValue::Utf8 { value, .. }, LogicalType::Char(len, unit)) => { to_char(value, *len, *unit) } @@ -1688,6 +1727,12 @@ impl From> for DataValue { } } +impl From> for DataValue { + fn from(value: Vec) -> Self { + Self::Binary { value, len: None } + } +} + impl From for DataValue { fn from(value: String) -> Self { DataValue::Utf8 { @@ -1800,6 +1845,13 @@ impl fmt::Display for DataValue { DataValue::UInt16(e) => write!(f, "{e}")?, DataValue::UInt32(e) => write!(f, "{e}")?, DataValue::UInt64(e) => write!(f, "{e}")?, + DataValue::Binary { value, .. } => { + write!(f, "X'")?; + for byte in value { + write!(f, "{byte:02X}")?; + } + write!(f, "'")?; + } DataValue::Utf8 { value: e, .. } => write!(f, "{e}")?, DataValue::Null => write!(f, "NULL")?, DataValue::Date32(e) => { @@ -1870,6 +1922,7 @@ impl fmt::Debug for DataValue { DataValue::UInt16(_) => write!(f, "UInt16({self})"), DataValue::UInt32(_) => write!(f, "UInt32({self})"), DataValue::UInt64(_) => write!(f, "UInt64({self})"), + DataValue::Binary { .. } => write!(f, "Binary({self})"), DataValue::Utf8 { .. } => write!(f, "Utf8(\"{self}\")"), DataValue::Null => write!(f, "null"), DataValue::Date32(_) => write!(f, "Date32({self})"), @@ -1926,6 +1979,57 @@ mod test { assert_eq!(left.partial_cmp(&DataValueRef::Value(&scalar)), None); } + #[test] + fn binary_values_preserve_bytes_and_index_order() -> Result<(), DatabaseError> { + let values: Vec = [ + vec![], + vec![0], + vec![0, 0], + vec![0, 255], + vec![1], + vec![255; 8], + vec![255; 9], + ] + .into_iter() + .map(DataValue::from) + .collect(); + let mut previous = None; + for value in &values { + let mut encoded = Vec::new(); + value.memcomparable_encode(&mut encoded)?; + assert_eq!( + DataValue::memcomparable_decode( + &mut Cursor::new(&encoded), + &LogicalType::Varbinary(None) + )?, + *value + ); + if let Some(previous) = previous { + assert!(previous < encoded); + } + previous = Some(encoded); + } + let bytes = DataValue::from(vec![0, 255]); + assert_eq!(bytes.binary(), Some([0, 255].as_slice())); + assert_eq!(values[0].binary(), Some([].as_slice())); + assert_eq!(DataValue::Null.binary(), None); + assert_eq!(DataValue::from("bytes".to_string()).binary(), None); + assert_eq!(bytes.to_string(), "X'00FF'"); + assert_eq!(bytes.logical_type(), LogicalType::Varbinary(None)); + assert_eq!(bytes.serialized_len_hint(), 6); + let bounded = bytes.clone().cast(&LogicalType::Varbinary(Some(2)))?; + assert_eq!(bounded, bytes); + assert!(matches!( + bytes.clone().cast(&LogicalType::Varbinary(Some(1))), + Err(DatabaseError::TooLong) + )); + assert!(matches!( + bytes.cast(&LogicalType::Bigint), + Err(DatabaseError::CastFail { .. }) + )); + Ok(()) + } + fn utf8(value: &str) -> DataValue { DataValue::Utf8 { value: value.to_string(), diff --git a/src/wasm.rs b/src/wasm.rs index 9d786f24..c8986d12 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -19,7 +19,7 @@ use crate::storage::memory::MemoryStorage; use crate::types::tuple::Tuple; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; -use js_sys::{Array, Object, Reflect}; +use js_sys::{Array, Object, Reflect, Uint8Array}; use wasm_bindgen::prelude::*; fn to_js_err(err: impl ToString) -> JsValue { @@ -48,6 +48,7 @@ fn data_value_to_js(value: &DataValue) -> Result { DataValue::UInt16(value) => Ok(JsValue::from_f64(*value as f64)), DataValue::UInt32(value) => Ok(JsValue::from_f64(*value as f64)), DataValue::UInt64(value) => Ok(JsValue::from_f64(*value as f64)), + DataValue::Binary { value, .. } => Ok(Uint8Array::from(value.as_slice()).into()), DataValue::Utf8 { value, ty, unit } => { let object = Object::new(); set_prop(&object, "value", JsValue::from_str(value))?; diff --git a/tests/slt/binary.slt b/tests/slt/binary.slt new file mode 100644 index 00000000..50ec7c3d --- /dev/null +++ b/tests/slt/binary.slt @@ -0,0 +1,116 @@ +# Binary values preserve arbitrary bytes, including zero bytes and non-UTF8. +statement ok +create table binary_values (id int primary key, b varbinary(8) unique); + +statement ok +insert into binary_values values (1,x''),(2,x'00'),(3,x'0000'),(4,x'00ff'),(5,x'80'),(6,x'ff'),(7,null); + +query IT +select id,b from binary_values order by b; +---- +1 X'' +2 X'00' +3 X'0000' +4 X'00FF' +5 X'80' +6 X'FF' +7 NULL + +query I +select id from binary_values where b=x'00ff'; +---- +4 + +query I +select id from binary_values where b>=x'00' and bx'80',x'ab'=x'AB',x'00'<>x'01',x'00'<=x'00',x'01'>=x'00'; +---- +true true true true true true + +query TTTTT +select x'00ff' in (select b from binary_values),x'01' in (select b from binary_values),x'01' not in (select b from binary_values),null=x'00',x'00' in (select b from binary_values where false); +---- +true NULL NULL NULL false + +query TT +select cast(x'00ff' as varbinary(2)),cast(null as blob); +---- +X'00FF' NULL + +query T +select distinct b from binary_values where b in (x'00',x'ff') order by b desc; +---- +X'FF' +X'00' + +query I +select count(*) from binary_values a join binary_values b on a.b=b.b; +---- +6 + +statement error +insert into binary_values values (8,x'00ff'); + +statement error +insert into binary_values values (8,x'000102030405060708'); + +statement error +select cast(x'0001' as varbinary(1)); + +statement error +select x'0'; + +statement error +select x'GG'; + +statement error +select x'00'+x'01'; + +statement error +select cast(x'00' as bigint); + +query I +select count(*) from binary_values; +---- +7 + +statement ok +update binary_values set b=x'fe' where id=6; + +query I +select id from binary_values where b=x'fe'; +---- +6 + +statement ok +delete from binary_values where b=x'0000'; + +query I +select count(*) from binary_values where b=x'0000'; +---- +0 + +statement ok +create table binary_pk (b bytea primary key); + +statement ok +insert into binary_pk values (x''),(x'00'),(x'0000'),(x'ff'); + +query T +select b from binary_pk where b>=x'00' order by b; +---- +X'00' +X'0000' +X'FF' + +statement ok +drop table binary_pk; + +statement ok +drop table binary_values; diff --git a/tpcc/src/backend/sqlite.rs b/tpcc/src/backend/sqlite.rs index 5e4dd25a..3de33ef7 100644 --- a/tpcc/src/backend/sqlite.rs +++ b/tpcc/src/backend/sqlite.rs @@ -229,6 +229,7 @@ fn convert_value(value: &DataValue) -> Result { DataValue::UInt16(v) => Value::Integer(*v as i64), DataValue::UInt32(v) => Value::Integer(*v as i64), DataValue::UInt64(v) => Value::Integer(*v as i64), + DataValue::Binary { value, .. } => Value::Binary(value.clone()), DataValue::Utf8 { value, .. } => Value::String(value.clone()), DataValue::Date32(v) => Value::Integer(*v as i64), DataValue::Date64(v) => Value::String( From 90c6bb2705a0975fe77a3c94c160ac1fe496df46 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 05/10] fix: bind index DDL options explicitly --- src/binder/parser.rs | 76 ++++++++++++++++++++++++++++---------- tests/slt/create_index.slt | 58 +++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index c42cf04c..ed0a9895 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -43,13 +43,13 @@ use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, ColumnId, LogicalType}; pub(super) use sqlparser::ast::{ AlterColumnOperation, AlterTableOperation, Assignment, AssignmentTarget, BinaryLength, - BinaryOperator, ColumnDef, ColumnOption, CreateView, DataType, DescribeAlias, Distinct, - DuplicateTreatment, Expr, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArguments, - GroupByExpr, Ident, IndexColumn, Join, JoinConstraint, JoinOperator, LimitClause, ObjectName, - ObjectNamePart, ObjectType, OrderByExpr, OrderByKind, Query, Select, SelectInto, SelectItem, - SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Spanned, TableAlias, - TableConstraint, TableFactor, TableObject, TableWithJoins, TypedString, UnaryOperator, Value, - ValueWithSpan, WindowType, + BinaryOperator, ColumnDef, ColumnOption, CreateIndex, CreateView, DataType, DescribeAlias, + Distinct, DuplicateTreatment, Expr, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArguments, GroupByExpr, Ident, IndexColumn, IndexOption, IndexType, Join, + JoinConstraint, JoinOperator, LimitClause, ObjectName, ObjectNamePart, ObjectType, OrderByExpr, + OrderByKind, Query, Select, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, + SetOperator, SetQuantifier, Spanned, TableAlias, TableConstraint, TableFactor, TableObject, + TableWithJoins, TypedString, UnaryOperator, Value, ValueWithSpan, WindowType, }; #[cfg(feature = "copy")] pub(super) use sqlparser::ast::{CopyOption, CopySource, CopyTarget}; @@ -402,8 +402,9 @@ where object_type, names, if_exists, + table, .. - } => self.drop_object(*object_type, names.clone(), *if_exists)?, + } => self.drop_object(*object_type, names.clone(), *if_exists, table.clone())?, Statement::Insert(insert) => self.insert(insert)?, Statement::Update(update) => self.update(update)?, Statement::Delete(delete) => self.delete(delete)?, @@ -860,10 +861,26 @@ where Ok(()) } - fn create_index( - self, - create: sqlparser::ast::CreateIndex, - ) -> Result { + fn create_index(self, create: CreateIndex) -> Result { + if create.concurrently + || !create.include.is_empty() + || create.nulls_distinct.is_some() + || !create.with.is_empty() + || create.predicate.is_some() + || !create.alter_options.is_empty() + || create + .using + .as_ref() + .is_some_and(|ty| !matches!(ty, IndexType::BTree)) + || create + .index_options + .iter() + .any(|option| !matches!(option, IndexOption::Using(IndexType::BTree))) + { + return Err(DatabaseError::UnsupportedStmt( + "CREATE INDEX options".to_string(), + )); + } let table_name = sql_table_name(create.table_name)?; let index_name = create .name @@ -875,6 +892,15 @@ where let mut columns = Vec::with_capacity(create.columns.len()); for index_column in create.columns { + if index_column.operator_class.is_some() + || index_column.column.options.asc == Some(false) + || index_column.column.options.nulls_first.is_some() + || index_column.column.with_fill.is_some() + { + return Err(DatabaseError::UnsupportedStmt( + "CREATE INDEX column options".to_string(), + )); + } let expr = self .binder .bind_expr(&index_column.column.expr, self.arena)?; @@ -929,10 +955,16 @@ where object_type: ObjectType, mut names: Vec, if_exists: bool, + table: Option, ) -> Result { - if names.len() > 1 { + if names.len() != 1 { + return Err(DatabaseError::UnsupportedStmt( + "only Drop a single `Table`, `View` or `Index` is allowed".to_string(), + )); + } + if table.is_some() && object_type != ObjectType::Index { return Err(DatabaseError::UnsupportedStmt( - "only Drop a single `Table` or `View` is allowed".to_string(), + "DROP ON is only supported for INDEX".to_string(), )); } @@ -944,12 +976,17 @@ where .binder .bind_drop_view(sql_table_name(names.remove(0))?, if_exists), ObjectType::Index => { - let (table_name, index_name) = sql_index_name(names.remove(0))?; + let name = names.remove(0); + let (table_name, index_name) = if let Some(table) = table { + (sql_table_name(table)?, sql_object_name(name)?) + } else { + sql_index_name(name)? + }; self.binder .bind_drop_index(table_name, index_name, if_exists) } _ => Err(DatabaseError::UnsupportedStmt( - "only `Table` and `View` are allowed to be Dropped".to_string(), + "only `Table`, `View` and `Index` are allowed to be Dropped".to_string(), )), } } @@ -1946,10 +1983,9 @@ fn single_ident_from_object_name(name: &ObjectName) -> Result Result<(TableName, String), DatabaseError> { - let table_name = name.0.first().ok_or_else(|| { - attach_span_if_absent(DatabaseError::invalid_table(name.to_string()), &name) - })?; - let index_name = name.0.get(1).ok_or(DatabaseError::InvalidIndex)?; + let [table_name, index_name] = name.0.as_slice() else { + return Err(DatabaseError::InvalidIndex); + }; Ok(( lower_name_part(table_name)?.into(), diff --git a/tests/slt/create_index.slt b/tests/slt/create_index.slt index 9a748f50..46667a43 100644 --- a/tests/slt/create_index.slt +++ b/tests/slt/create_index.slt @@ -27,6 +27,64 @@ select * from t; ---- 0 0 0 0 +# New parser table fields must reach DROP INDEX binding. +statement ok +create index "Index_On" on t using btree (v1 asc); + +statement ok +drop index "Index_On" on "T"; + +statement ok +drop index if exists "Index_On" on t; + +statement error +drop index index_on on t; + +statement error +drop index t.index_2.extra; + +statement error +drop index index_2 on missing_table; + +# Unsupported options must not silently create a different index. +statement error CREATE INDEX options +create index option_index on t (v1) where v1 > 0; + +statement error CREATE INDEX options +create index option_index on t (v1) include (v2); + +statement error CREATE INDEX options +create index option_index on t using hash (v1); + +statement error CREATE INDEX options +create index concurrently option_index on t (v1); + +statement error CREATE INDEX options +create unique index option_index on t (v1) nulls not distinct; + +statement error CREATE INDEX options +create index option_index on t (v1) with (fillfactor=70); + +statement error CREATE INDEX column options +create index option_index on t (v1 desc); + +statement error CREATE INDEX column options +create index option_index on t (v1 nulls first); + +statement error CREATE INDEX column options +create index option_index on t (v1 int4_ops); + +statement ok +create index option_index on t (v1); + +statement ok +drop index option_index on t; + +query IIII +select * from t; +---- +0 0 0 0 + statement ok drop index t.index_1 From 356fd1507b785e91204e73f7e1eff05831b5987f Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 06/10] refactor: consume cast values and preserve string concat ownership --- src/execution/dql/aggregate/avg.rs | 2 +- src/execution/dql/aggregate/min_max.rs | 4 +- src/execution/dql/aggregate/sum.rs | 4 +- src/expression/evaluator.rs | 12 +- src/expression/simplify.rs | 8 +- src/optimizer/core/histogram.rs | 8 +- src/types/evaluator/binary.rs | 37 ++- src/types/evaluator/boolean.rs | 55 ++-- src/types/evaluator/bytes.rs | 6 + src/types/evaluator/cast.rs | 399 ++++++++++++++++++++----- src/types/evaluator/date.rs | 32 +- src/types/evaluator/datetime.rs | 42 ++- src/types/evaluator/decimal.rs | 51 ++-- src/types/evaluator/float32.rs | 27 +- src/types/evaluator/float64.rs | 39 +-- src/types/evaluator/int16.rs | 29 +- src/types/evaluator/int32.rs | 29 +- src/types/evaluator/int64.rs | 29 +- src/types/evaluator/int8.rs | 28 +- src/types/evaluator/mod.rs | 3 +- src/types/evaluator/null.rs | 8 +- src/types/evaluator/time32.rs | 37 ++- src/types/evaluator/time64.rs | 63 ++-- src/types/evaluator/tuple.rs | 8 +- src/types/evaluator/uint16.rs | 29 +- src/types/evaluator/uint32.rs | 29 +- src/types/evaluator/uint64.rs | 29 +- src/types/evaluator/uint8.rs | 29 +- src/types/evaluator/utf8.rs | 189 +++++++----- src/types/mod.rs | 4 +- src/types/value.rs | 25 +- tests/macros-test/src/main.rs | 3 +- tests/slt/binary.slt | 64 ++++ 33 files changed, 905 insertions(+), 456 deletions(-) diff --git a/src/execution/dql/aggregate/avg.rs b/src/execution/dql/aggregate/avg.rs index 04e4e2ab..a1873aa5 100644 --- a/src/execution/dql/aggregate/avg.rs +++ b/src/execution/dql/aggregate/avg.rs @@ -68,7 +68,7 @@ impl Accumulator for AvgAccumulator { let value = acc.result().clone().cast(&LogicalType::Double)?; let quantity = DataValue::Float64((self.count as f64).into()); let evaluator = binary_create(Cow::Owned(LogicalType::Double), BinaryOperator::Divide)?; - self.result = evaluator.binary_eval(&value, &quantity)?; + self.result = evaluator.binary_eval(Cow::Borrowed(&value), &quantity)?; Ok(()) } diff --git a/src/execution/dql/aggregate/min_max.rs b/src/execution/dql/aggregate/min_max.rs index 85de9240..bc88ffdb 100644 --- a/src/execution/dql/aggregate/min_max.rs +++ b/src/execution/dql/aggregate/min_max.rs @@ -44,7 +44,9 @@ impl Accumulator for MinMaxAccumulator { if !value.is_null() { if !self.result.is_null() { let evaluator = binary_create(Cow::Owned(value.logical_type()), self.op)?; - if let DataValue::Boolean(result) = evaluator.binary_eval(&self.result, value)? { + if let DataValue::Boolean(result) = + evaluator.binary_eval(Cow::Borrowed(&self.result), value)? + { result } else { return Err(DatabaseError::InvalidType); diff --git a/src/execution/dql/aggregate/sum.rs b/src/execution/dql/aggregate/sum.rs index 9c5a9f9c..33150e7a 100644 --- a/src/execution/dql/aggregate/sum.rs +++ b/src/execution/dql/aggregate/sum.rs @@ -45,7 +45,9 @@ impl Accumulator for SumAccumulator { if self.result.is_null() { self.result = value.clone(); } else { - self.result = self.evaluator.binary_eval(&self.result, value)?; + self.result = self + .evaluator + .binary_eval(Cow::Borrowed(&self.result), value)?; } } diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index 9e413fdc..51212489 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -84,7 +84,7 @@ impl ScalarExpression { } => { let value = arena.expression(*expr).eval(arena, tuple)?; if let Some(evaluator) = evaluator { - evaluator.eval(&value).map(Cow::Owned) + evaluator.eval(value.into_owned()).map(Cow::Owned) } else { Ok(value) } @@ -109,7 +109,7 @@ impl ScalarExpression { evaluator .as_ref() .ok_or(DatabaseError::EvaluatorNotFound)? - .binary_eval(&left, &right) + .binary_eval(left, &right) .map(Cow::Owned) } ScalarExpression::IsNull { expr, negated } => { @@ -134,7 +134,7 @@ impl ScalarExpression { let mut saw_null = false; for arg in args { let arg_value = arena.expression(*arg).eval(arena, tuple)?; - match eq.binary_eval(&value, &arg_value)? { + match eq.binary_eval(Cow::Borrowed(&value), &arg_value)? { DataValue::Boolean(true) => { return Ok(Cow::Owned(DataValue::Boolean(!negated))) } @@ -179,11 +179,11 @@ impl ScalarExpression { let ge = left_evaluator .as_ref() .ok_or(DatabaseError::EvaluatorNotFound)? - .binary_eval(&value, &left)?; + .binary_eval(Cow::Borrowed(&value), &left)?; let le = right_evaluator .as_ref() .ok_or(DatabaseError::EvaluatorNotFound)? - .binary_eval(&value, &right)?; + .binary_eval(Cow::Borrowed(&value), &right)?; let is_between = match (ge, le) { (DataValue::Boolean(false), _) | (_, DataValue::Boolean(false)) => false, (DataValue::Boolean(true), DataValue::Boolean(true)) => true, @@ -354,7 +354,7 @@ impl ScalarExpression { when_value = cast_cow(when_value, &ty)?; let evaluator = binary_create(Cow::Owned(ty), BinaryOperator::Eq)?; evaluator - .binary_eval(operand_value, &when_value)? + .binary_eval(Cow::Borrowed(operand_value), &when_value)? .is_true()? } else { when_value.is_true()? diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index ee395f7b..59aa9d9f 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -83,7 +83,7 @@ impl ExprVisitorMut for ConstantCalculator { let evaluator = binary_create(Cow::Borrowed(&ty), *op)?; let left_val = left_val.clone().cast(&ty)?; let right_val = right_val.clone().cast(&ty)?; - let value = evaluator.binary_eval(&left_val, &right_val)?; + let value = evaluator.binary_eval(Cow::Owned(left_val), &right_val)?; *expr = ScalarExpression::Constant(value); } } @@ -400,7 +400,7 @@ impl Simplify { let right = right.cast(ty).ok()?; binary_create(Cow::Borrowed(ty), op) .ok()? - .binary_eval(&left, &right) + .binary_eval(Cow::Owned(left), &right) .ok() } @@ -545,11 +545,11 @@ impl ExprRef { return None; } if let Some(evaluator) = evaluator { - evaluator.binary_eval(&left, &right) + evaluator.binary_eval(Cow::Owned(left), &right) } else { binary_create(Cow::Borrowed(ty), *op) .ok()? - .binary_eval(&left, &right) + .binary_eval(Cow::Owned(left), &right) } .ok() } diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index ea5badbc..a2f28dcb 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -213,28 +213,28 @@ impl BoundComparator { fn lt(&self, value: &DataValue, target: &DataValue) -> Result { Ok(matches!( - self.lt.binary_eval(value, target)?, + self.lt.binary_eval(Cow::Borrowed(value), target)?, DataValue::Boolean(true) )) } fn lte(&self, value: &DataValue, target: &DataValue) -> Result { Ok(matches!( - self.lte.binary_eval(value, target)?, + self.lte.binary_eval(Cow::Borrowed(value), target)?, DataValue::Boolean(true) )) } fn gt(&self, value: &DataValue, target: &DataValue) -> Result { Ok(matches!( - self.gt.binary_eval(value, target)?, + self.gt.binary_eval(Cow::Borrowed(value), target)?, DataValue::Boolean(true) )) } fn gte(&self, value: &DataValue, target: &DataValue) -> Result { Ok(matches!( - self.gte.binary_eval(value, target)?, + self.gte.binary_eval(Cow::Borrowed(value), target)?, DataValue::Boolean(true) )) } diff --git a/src/types/evaluator/binary.rs b/src/types/evaluator/binary.rs index a6b45493..abb410fd 100644 --- a/src/types/evaluator/binary.rs +++ b/src/types/evaluator/binary.rs @@ -355,9 +355,13 @@ macro_rules! eval_numeric_binary { pub(crate) fn eval_binary( pos: u16, params: &BinaryEvaluatorParams, - left: &crate::types::value::DataValue, + left: Cow<'_, crate::types::value::DataValue>, right: &crate::types::value::DataValue, ) -> Result { + if pos == binary_pos(BINARY_UTF8_BASE, UTF8_STRING_CONCAT_OFFSET) { + return utf8_string_concat_binary_eval(left.into_owned(), right); + } + let left = left.as_ref(); match pos { BINARY_INT8_BASE..BINARY_INT16_BASE => { eval_numeric_binary!(pos, BINARY_INT8_BASE, Int8, left, right) @@ -495,9 +499,6 @@ pub(crate) fn eval_binary( x if x == binary_pos(BINARY_UTF8_BASE, UTF8_NOT_EQ_OFFSET) => { utf8_not_eq_binary_eval(left, right) } - x if x == binary_pos(BINARY_UTF8_BASE, UTF8_STRING_CONCAT_OFFSET) => { - utf8_string_concat_binary_eval(left, right) - } x if x == binary_pos(BINARY_UTF8_BASE, UTF8_LIKE_OFFSET) => { let BinaryEvaluatorParams::Like { escape_char } = params else { unreachable!() @@ -725,7 +726,7 @@ mod test { left: &DataValue, right: &DataValue, ) -> Result { - create(ty, op)?.binary_eval(left, right) + create(ty, op)?.binary_eval(Cow::Borrowed(left), right) } fn assert_panics(f: impl FnOnce() + std::panic::UnwindSafe) { @@ -735,7 +736,7 @@ mod test { fn assert_binary_ref_panics(pos: u16, params: BinaryEvaluatorParams, value: &DataValue) { assert_panics(|| { let evaluator = BinaryEvaluatorRef::new(pos, params); - let _ = evaluator.binary_eval(value, value).unwrap(); + let _ = evaluator.binary_eval(Cow::Borrowed(value), value).unwrap(); }); } @@ -768,21 +769,29 @@ mod test { let divide = binary_create(Cow::Owned($ty), BinaryOperator::Divide)?; for (left, right, expected) in [(5, 2, 2), (-5, 2, -2), (5, -2, -2), (-5, -2, 2)] { assert_eq!( - divide - .binary_eval(&DataValue::$variant(left), &DataValue::$variant(right))?, + divide.binary_eval( + Cow::Borrowed(&DataValue::$variant(left)), + &DataValue::$variant(right) + )?, DataValue::$variant(expected) ); } assert!(matches!( - divide.binary_eval(&DataValue::$variant(<$int>::MIN), &DataValue::$variant(-1)), + divide.binary_eval( + Cow::Borrowed(&DataValue::$variant(<$int>::MIN)), + &DataValue::$variant(-1) + ), Err(DatabaseError::OverFlow) )); assert!(matches!( - divide.binary_eval(&DataValue::$variant(1), &DataValue::$variant(0)), + divide.binary_eval( + Cow::Borrowed(&DataValue::$variant(1)), + &DataValue::$variant(0) + ), Err(DatabaseError::InvalidValue(_)) )); assert_eq!( - divide.binary_eval(&DataValue::Null, &DataValue::$variant(0))?, + divide.binary_eval(Cow::Borrowed(&DataValue::Null), &DataValue::$variant(0))?, DataValue::Null ); }}; @@ -818,9 +827,9 @@ mod test { ), ] { let divide = binary_create(Cow::Owned(ty), BinaryOperator::Divide)?; - assert_eq!(divide.binary_eval(&left, &right)?, expected); + assert_eq!(divide.binary_eval(Cow::Borrowed(&left), &right)?, expected); assert_eq!( - divide.binary_eval(&left, &DataValue::Null)?, + divide.binary_eval(Cow::Borrowed(&left), &DataValue::Null)?, DataValue::Null ); } @@ -1021,7 +1030,7 @@ mod test { assert_panics(|| { let evaluator = BinaryEvaluatorRef::new(u16::MAX, BinaryEvaluatorParams::Unit); let _ = evaluator - .binary_eval(&DataValue::Int32(1), &DataValue::Int32(1)) + .binary_eval(Cow::Borrowed(&DataValue::Int32(1)), &DataValue::Int32(1)) .unwrap(); }); } diff --git a/src/types/evaluator/boolean.rs b/src/types/evaluator/boolean.rs index 2cdba135..3d4e8fd9 100644 --- a/src/types/evaluator/boolean.rs +++ b/src/types/evaluator/boolean.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::types::evaluator::cast::{to_char, to_varchar}; +use crate::types::evaluator::cast::{to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; use ordered_float::OrderedFloat; @@ -67,34 +67,34 @@ pub fn boolean_eq_binary_eval( } crate::define_cast_evaluator!(boolean_to_tinyint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Int8(if *value { 1 } else { 0 })) + Ok(DataValue::Int8(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_utinyint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::UInt8(if *value { 1 } else { 0 })) + Ok(DataValue::UInt8(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_smallint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Int16(if *value { 1 } else { 0 })) + Ok(DataValue::Int16(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_usmallint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::UInt16(if *value { 1 } else { 0 })) + Ok(DataValue::UInt16(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_integer_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Int32(if *value { 1 } else { 0 })) + Ok(DataValue::Int32(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_uinteger_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::UInt32(if *value { 1 } else { 0 })) + Ok(DataValue::UInt32(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_bigint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Int64(if *value { 1 } else { 0 })) + Ok(DataValue::Int64(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_ubigint_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::UInt64(if *value { 1 } else { 0 })) + Ok(DataValue::UInt64(if value { 1 } else { 0 })) }); crate::define_cast_evaluator!(boolean_to_float_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Float32(OrderedFloat(if *value { 1.0 } else { 0.0 }))) + Ok(DataValue::Float32(OrderedFloat(if value { 1.0 } else { 0.0 }))) }); crate::define_cast_evaluator!(boolean_to_double_cast_eval, DataValue::Boolean(value) => { - Ok(DataValue::Float64(OrderedFloat(if *value { 1.0 } else { 0.0 }))) + Ok(DataValue::Float64(OrderedFloat(if value { 1.0 } else { 0.0 }))) }); crate::define_cast_evaluator!( boolean_to_char_cast_eval { @@ -110,6 +110,10 @@ crate::define_cast_evaluator!( }, DataValue::Boolean(value) => |this| to_varchar(value.to_string(), this.len, this.unit) ); +crate::define_cast_evaluator!( + boolean_to_varbinary_cast_eval { len: Option }, + DataValue::Boolean(value) => |this| to_varbinary(value.to_string().into_bytes(), this.len) +); pub fn boolean_not_eq_binary_eval( left: &DataValue, right: &DataValue, @@ -184,47 +188,47 @@ mod test { let value = DataValue::Boolean(true); assert_eq!( - boolean_to_tinyint_cast_eval(&value).unwrap(), + boolean_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - boolean_to_utinyint_cast_eval(&value).unwrap(), + boolean_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - boolean_to_smallint_cast_eval(&value).unwrap(), + boolean_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - boolean_to_usmallint_cast_eval(&value).unwrap(), + boolean_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - boolean_to_integer_cast_eval(&value).unwrap(), + boolean_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - boolean_to_uinteger_cast_eval(&value).unwrap(), + boolean_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - boolean_to_bigint_cast_eval(&value).unwrap(), + boolean_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - boolean_to_ubigint_cast_eval(&value).unwrap(), + boolean_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - boolean_to_float_cast_eval(&value).unwrap(), + boolean_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - boolean_to_double_cast_eval(&value).unwrap(), + boolean_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - boolean_to_char_cast_eval(4, CharLengthUnits::Characters, &value).unwrap(), + boolean_to_char_cast_eval(4, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "true".to_string(), ty: Utf8Type::Fixed(4), @@ -232,7 +236,8 @@ mod test { } ); assert_eq!( - boolean_to_varchar_cast_eval(Some(4), CharLengthUnits::Characters, &value).unwrap(), + boolean_to_varchar_cast_eval(Some(4), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "true".to_string(), ty: Utf8Type::Variable(Some(4)), @@ -240,14 +245,14 @@ mod test { } ); assert_eq!( - boolean_to_double_cast_eval(&DataValue::Boolean(false)).unwrap(), + boolean_to_double_cast_eval(DataValue::Boolean(false)).unwrap(), DataValue::Float64(OrderedFloat(0.0)) ); assert_eq!( boolean_to_varchar_cast_eval( None, CharLengthUnits::Characters, - &DataValue::Boolean(false) + DataValue::Boolean(false) ) .unwrap(), DataValue::Utf8 { diff --git a/src/types/evaluator/bytes.rs b/src/types/evaluator/bytes.rs index b11f50be..94ca6348 100644 --- a/src/types/evaluator/bytes.rs +++ b/src/types/evaluator/bytes.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; +use crate::types::evaluator::cast::to_varbinary; use crate::types::value::DataValue; macro_rules! bytes_binary { @@ -29,6 +30,11 @@ macro_rules! bytes_binary { }; } +crate::define_cast_evaluator!( + bytes_to_varbinary_cast_eval { len: Option }, + DataValue::Binary { value, .. } => |this| to_varbinary(value, this.len) +); + bytes_binary!(bytes_eq_binary_eval, ==); bytes_binary!(bytes_not_eq_binary_eval, !=); bytes_binary!(bytes_gt_binary_eval, >); diff --git a/src/types/evaluator/cast.rs b/src/types/evaluator/cast.rs index da2c2396..834077a5 100644 --- a/src/types/evaluator/cast.rs +++ b/src/types/evaluator/cast.rs @@ -14,6 +14,7 @@ use crate::errors::DatabaseError; use crate::types::evaluator::boolean::*; +use crate::types::evaluator::bytes::*; #[cfg(feature = "time")] use crate::types::evaluator::date::*; #[cfg(feature = "time")] @@ -142,6 +143,12 @@ pub(crate) fn to_char( }) } +pub(crate) fn to_varbinary(value: Vec, len: Option) -> Result { + let result = DataValue::Binary { value, len }; + result.check_len(&LogicalType::Varbinary(len))?; + Ok(result) +} + pub(crate) fn to_varchar( value: String, len: Option, @@ -224,7 +231,7 @@ macro_rules! decimal_to_int_cast { macro_rules! define_cast_evaluator { ($name:ident, $pattern:pat => $body:block) => { pub fn $name( - value: &$crate::types::value::DataValue, + value: $crate::types::value::DataValue, ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { match value { $crate::types::value::DataValue::Null => Ok($crate::types::value::DataValue::Null), @@ -235,7 +242,7 @@ macro_rules! define_cast_evaluator { }; ($name:ident, $pattern:pat => |$this:ident| $body:expr) => { pub fn $name( - value: &$crate::types::value::DataValue, + value: $crate::types::value::DataValue, ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { match value { $crate::types::value::DataValue::Null => Ok($crate::types::value::DataValue::Null), @@ -249,7 +256,7 @@ macro_rules! define_cast_evaluator { }; ($name:ident, $pattern:pat => |$this:ident| $body:block) => { pub fn $name( - value: &$crate::types::value::DataValue, + value: $crate::types::value::DataValue, ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { match value { $crate::types::value::DataValue::Null => Ok($crate::types::value::DataValue::Null), @@ -264,7 +271,7 @@ macro_rules! define_cast_evaluator { ($name:ident { $($field:ident : $field_ty:ty),+ $(,)? }, $pattern:pat => |$this:ident| $body:expr) => { pub fn $name( $($field: $field_ty,)+ - value: &$crate::types::value::DataValue, + value: $crate::types::value::DataValue, ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { struct This { $($field: $field_ty),+ @@ -280,7 +287,7 @@ macro_rules! define_cast_evaluator { ($name:ident { $($field:ident : $field_ty:ty),+ $(,)? }, $pattern:pat => $body:block) => { pub fn $name( $($field: $field_ty,)+ - value: &$crate::types::value::DataValue, + value: $crate::types::value::DataValue, ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { match value { $crate::types::value::DataValue::Null => Ok($crate::types::value::DataValue::Null), @@ -296,37 +303,37 @@ macro_rules! define_integer_cast_evaluators { ($prefix:ident, $variant:ident, $src_ty:ty, $from_ty:expr) => { paste::paste! { $crate::define_cast_evaluator!([<$prefix:snake _to_boolean_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - $crate::numeric_to_boolean_cast!(*value, $from_ty) + $crate::numeric_to_boolean_cast!(value, $from_ty) }); $crate::define_cast_evaluator!([<$prefix:snake _to_tinyint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Int8(i8::try_from(*value)?)) + Ok($crate::types::value::DataValue::Int8(i8::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_utinyint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::UInt8(u8::try_from(*value)?)) + Ok($crate::types::value::DataValue::UInt8(u8::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_smallint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Int16(i16::try_from(*value)?)) + Ok($crate::types::value::DataValue::Int16(i16::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_usmallint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::UInt16(u16::try_from(*value)?)) + Ok($crate::types::value::DataValue::UInt16(u16::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_integer_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Int32(i32::try_from(*value)?)) + Ok($crate::types::value::DataValue::Int32(i32::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_uinteger_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::UInt32(u32::try_from(*value)?)) + Ok($crate::types::value::DataValue::UInt32(u32::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_bigint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Int64(i64::try_from(*value)?)) + Ok($crate::types::value::DataValue::Int64(i64::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_ubigint_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::UInt64(u64::try_from(*value)?)) + Ok($crate::types::value::DataValue::UInt64(u64::try_from(value)?)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_float_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Float32(ordered_float::OrderedFloat(*value as f32))) + Ok($crate::types::value::DataValue::Float32(ordered_float::OrderedFloat(value as f32))) }); $crate::define_cast_evaluator!([<$prefix:snake _to_double_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::Float64(ordered_float::OrderedFloat(*value as f64))) + Ok($crate::types::value::DataValue::Float64(ordered_float::OrderedFloat(value as f64))) }); $crate::define_cast_evaluator!( [<$prefix:snake _to_char_cast_eval>] { @@ -346,13 +353,19 @@ macro_rules! define_integer_cast_evaluators { $crate::types::evaluator::cast::to_varchar(value.to_string(), this.len, this.unit) } ); + $crate::define_cast_evaluator!( + [<$prefix:snake _to_varbinary_cast_eval>] { len: Option }, + $crate::types::value::DataValue::$variant(value) => |this| { + $crate::types::evaluator::cast::to_varbinary(value.to_string().into_bytes(), this.len) + } + ); #[cfg(feature = "decimal")] $crate::define_cast_evaluator!( [<$prefix:snake _to_decimal_cast_eval>] { scale: Option }, $crate::types::value::DataValue::$variant(value) => |this| { - let mut decimal = rust_decimal::Decimal::from(*value); + let mut decimal = rust_decimal::Decimal::from(value); $crate::types::value::DataValue::decimal_round_i(&this.scale, &mut decimal); Ok($crate::types::value::DataValue::Decimal(decimal)) } @@ -366,7 +379,7 @@ macro_rules! define_float_cast_evaluators { ($prefix:ident, $variant:ident, $src_ty:ty, $from_ty:expr, $into_decimal:ident) => { paste::paste! { $crate::define_cast_evaluator!([<$prefix:snake _to_float_cast_eval>], $crate::types::value::DataValue::$variant(value) => { - Ok($crate::types::value::DataValue::$variant(*value)) + Ok($crate::types::value::DataValue::$variant(value)) }); $crate::define_cast_evaluator!([<$prefix:snake _to_double_cast_eval>], $crate::types::value::DataValue::$variant(value) => { Ok($crate::types::value::DataValue::Float64(ordered_float::OrderedFloat(value.0 as f64))) @@ -413,6 +426,12 @@ macro_rules! define_float_cast_evaluators { $crate::types::evaluator::cast::to_varchar(value.to_string(), this.len, this.unit) } ); + $crate::define_cast_evaluator!( + [<$prefix:snake _to_varbinary_cast_eval>] { len: Option }, + $crate::types::value::DataValue::$variant(value) => |this| { + $crate::types::evaluator::cast::to_varbinary(value.to_string().into_bytes(), this.len) + } + ); #[cfg(feature = "decimal")] $crate::define_cast_evaluator!( [<$prefix:snake _to_decimal_cast_eval>] { @@ -433,10 +452,6 @@ macro_rules! define_float_cast_evaluators { } }; } -pub fn identity_cast_eval(value: &DataValue) -> Result { - Ok(value.clone()) -} - macro_rules! cast_ref { ($from:expr, $to:expr) => {{ Ok(CastEvaluatorRef::new( @@ -553,11 +568,14 @@ pub fn cast_create( } match (from, to) { - (LogicalType::Varbinary(_), LogicalType::Varbinary(len)) => Ok(CastEvaluatorRef::new( + (LogicalType::Tuple(_), LogicalType::Varbinary(_)) => { + Err(cast_fail(from.clone(), to.clone())) + } + (LogicalType::SqlNull, _) => cast_ref!(from, to), + (_, LogicalType::Varbinary(len)) => Ok(CastEvaluatorRef::new( cast_pos(from, to), CastEvaluatorParams::Binary { len: *len }, )), - (LogicalType::SqlNull, _) => cast_ref!(from, to), (_, LogicalType::SqlNull) => cast_ref!(from, to), (LogicalType::Boolean, LogicalType::Tinyint) => { cast_ref!(from, to) @@ -836,6 +854,13 @@ fn varchar_params(ty: &LogicalType) -> (Option, CharLengthUnits) { (*len, *unit) } +fn binary_param(params: &CastEvaluatorParams) -> Option { + let CastEvaluatorParams::Binary { len } = params else { + unreachable!("cast evaluator must have binary parameters") + }; + *len +} + fn string_param(params: &CastEvaluatorParams) -> (Option, CharLengthUnits) { let CastEvaluatorParams::String { len, unit } = params else { unreachable!("cast evaluator must have string parameters") @@ -893,6 +918,7 @@ macro_rules! eval_integer_cast_by_pos { let (len, unit) = string_param(params); [<$prefix:snake _to_varchar_cast_eval>](len, unit, $value) } + CAST_BINARY => [<$prefix:snake _to_varbinary_cast_eval>](binary_param(params), $value), #[cfg(feature = "decimal")] CAST_DECIMAL => { let (_, scale) = decimal_param(params); @@ -905,7 +931,7 @@ macro_rules! eval_integer_cast_by_pos { } impl CastEvaluatorRef { - pub fn eval(&self, value: &DataValue) -> Result { + pub fn eval(&self, value: DataValue) -> Result { let params = &self.params; let from_code = self.pos / CAST_TYPE_STRIDE; let to_code = self.pos % CAST_TYPE_STRIDE; @@ -923,7 +949,7 @@ impl CastEvaluatorRef { } if matches!(params, CastEvaluatorParams::Identity) { - return run!(identity_cast_eval); + return Ok(value); } #[cfg(not(feature = "time"))] if is_chrono_type_code(from_code) || is_chrono_type_code(to_code) { @@ -933,12 +959,41 @@ impl CastEvaluatorRef { } match (from_code, to_code) { - (CAST_BINARY, CAST_BINARY) => { - let CastEvaluatorParams::Binary { len } = params else { - unreachable!() - }; - value.clone().cast(&LogicalType::Varbinary(*len)) - } + (CAST_BINARY, CAST_BINARY) => run!(bytes_to_varbinary_cast_eval { + len: binary_param(params) + }), + (CAST_BOOLEAN, CAST_BINARY) => run!(boolean_to_varbinary_cast_eval { + len: binary_param(params) + }), + (CAST_FLOAT, CAST_BINARY) => run!(float32_to_varbinary_cast_eval { + len: binary_param(params) + }), + (CAST_DOUBLE, CAST_BINARY) => run!(float64_to_varbinary_cast_eval { + len: binary_param(params) + }), + (CAST_CHAR | CAST_VARCHAR, CAST_BINARY) => run!(utf8_to_varbinary_cast_eval { + len: binary_param(params) + }), + #[cfg(feature = "time")] + (CAST_DATE, CAST_BINARY) => run!(date32_to_varbinary_cast_eval { + len: binary_param(params) + }), + #[cfg(feature = "time")] + (CAST_DATETIME, CAST_BINARY) => run!(date64_to_varbinary_cast_eval { + len: binary_param(params) + }), + #[cfg(feature = "time")] + (CAST_TIME, CAST_BINARY) => run!(time32_to_varbinary_cast_eval { + len: binary_param(params) + }), + #[cfg(feature = "time")] + (CAST_TIMESTAMP, CAST_BINARY) => run!(time64_to_varbinary_cast_eval { + len: binary_param(params) + }), + #[cfg(feature = "decimal")] + (CAST_DECIMAL, CAST_BINARY) => run!(decimal_to_varbinary_cast_eval { + len: binary_param(params) + }), (CAST_SQL_NULL, _) => run!(null_cast_eval), (_, CAST_SQL_NULL) => run!(to_sql_null_cast_eval), (CAST_BOOLEAN, CAST_TINYINT) => run!(boolean_to_tinyint_cast_eval), @@ -1195,7 +1250,7 @@ mod test { (LogicalType::Integer, DataValue::Int32(-12)), (LogicalType::Bigint, DataValue::Int64(-12)), ] { - assert_eq!(cast_eval(from.clone(), to, &value)?, expected); + assert_eq!(cast_eval(from.clone(), to, value.clone())?, expected); } } for (from, zero) in [ @@ -1203,11 +1258,11 @@ mod test { (LogicalType::Double, DataValue::Float64(OrderedFloat(-0.0))), ] { assert_eq!( - cast_eval(from.clone(), LogicalType::Integer, &zero)?, + cast_eval(from.clone(), LogicalType::Integer, zero.clone())?, DataValue::Int32(0) ); assert_eq!( - cast_eval(from, LogicalType::UInteger, &zero)?, + cast_eval(from, LogicalType::UInteger, zero.clone())?, DataValue::UInt32(0) ); } @@ -1270,7 +1325,7 @@ mod test { fn cast_eval( from: LogicalType, to: LogicalType, - value: &DataValue, + value: DataValue, ) -> Result { create(from, to)?.eval(value) } @@ -1282,7 +1337,7 @@ mod test { fn assert_cast_ref_panics(pos: u16, params: CastEvaluatorParams, value: DataValue) { assert_panics(|| { let evaluator = CastEvaluatorRef::new(pos, params); - let _ = evaluator.eval(&value).unwrap(); + let _ = evaluator.eval(value).unwrap(); }); } @@ -1411,8 +1466,27 @@ mod test { ]; for (to, expected) in cases { - assert_eq!(cast_eval(LogicalType::Boolean, to, &value)?, expected); + assert_eq!( + cast_eval(LogicalType::Boolean, to, value.clone())?, + expected + ); } + assert_eq!( + cast_eval( + LogicalType::Boolean, + LogicalType::Varbinary(None), + value.clone() + )?, + DataValue::from(b"true".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::Boolean, + LogicalType::Varbinary(None), + DataValue::Null + )?, + DataValue::Null + ); Ok(()) } @@ -1453,7 +1527,7 @@ mod test { ), ]; for (to, expected) in float_cases { - assert_eq!(cast_eval(LogicalType::Float, to, &float)?, expected); + assert_eq!(cast_eval(LogicalType::Float, to, float.clone())?, expected); } let double = DataValue::Float64(OrderedFloat(1.5)); @@ -1490,8 +1564,27 @@ mod test { ), ]; for (to, expected) in double_cases { - assert_eq!(cast_eval(LogicalType::Double, to, &double)?, expected); + assert_eq!( + cast_eval(LogicalType::Double, to, double.clone())?, + expected + ); } + assert_eq!( + cast_eval( + LogicalType::Float, + LogicalType::Varbinary(None), + float.clone() + )?, + DataValue::from(b"1.5".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::Double, + LogicalType::Varbinary(None), + double.clone() + )?, + DataValue::from(b"1.5".to_vec()) + ); Ok(()) } @@ -1542,14 +1635,41 @@ mod test { ]; for (from, value, expected) in cases { - assert_eq!(cast_eval(from, LogicalType::Double, &value)?, expected); + assert_eq!( + cast_eval(from.clone(), LogicalType::Varbinary(None), value.clone())?, + DataValue::from(value.to_string().into_bytes()) + ); + assert_eq!( + cast_eval(from, LogicalType::Double, value.clone())?, + expected + ); } + assert_eq!( + cast_eval( + LogicalType::Bigint, + LogicalType::Varbinary(None), + DataValue::Int64(-123) + )?, + DataValue::from(b"-123".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::UBigint, + LogicalType::Varbinary(None), + DataValue::UInt64(u64::MAX) + )?, + DataValue::from(b"18446744073709551615".to_vec()) + ); + assert!(matches!( + DataValue::Int64(123).cast(&LogicalType::Varbinary(Some(2))), + Err(DatabaseError::TooLong) + )); assert_eq!( cast_eval( LogicalType::Integer, LogicalType::Char(1, CharLengthUnits::Characters), - &DataValue::Int32(9), + DataValue::Int32(9), )?, DataValue::Utf8 { value: "9".to_string(), @@ -1561,7 +1681,7 @@ mod test { cast_eval( LogicalType::UInteger, LogicalType::Varchar(Some(2), CharLengthUnits::Characters), - &DataValue::UInt32(10), + DataValue::UInt32(10), )?, DataValue::Utf8 { value: "10".to_string(), @@ -1574,7 +1694,7 @@ mod test { cast_eval( LogicalType::Smallint, LogicalType::Decimal(None, Some(1)), - &DataValue::Int16(12), + DataValue::Int16(12), )?, DataValue::Decimal(Decimal::new(120, 1)) ); @@ -1584,11 +1704,48 @@ mod test { #[test] fn test_cast_create_dispatches_utf8_casts() -> Result<(), DatabaseError> { + let from = LogicalType::Varchar(None, CharLengthUnits::Characters); + for to in [ + from.clone(), + LogicalType::Char(4, CharLengthUnits::Octets), + LogicalType::Varchar(Some(4), CharLengthUnits::Octets), + LogicalType::Varbinary(Some(4)), + ] { + let text = "中\0".to_string(); + let buffer = text.as_ptr(); + let result = create(from.clone(), to)?.eval(DataValue::from(text))?; + match result { + DataValue::Utf8 { value, .. } => { + assert_eq!(value, "中\0"); + assert_eq!(value.as_ptr(), buffer); + } + DataValue::Binary { value, .. } => { + assert_eq!(value, "中\0".as_bytes()); + assert_eq!(value.as_ptr(), buffer); + } + _ => unreachable!(), + } + } + assert_eq!( + cast_eval( + LogicalType::Varchar(None, CharLengthUnits::Characters), + LogicalType::Varbinary(Some(4)), + utf8("中\0") + )?, + DataValue::Binary { + value: "中\0".as_bytes().to_vec(), + len: Some(4) + } + ); + assert!(matches!( + utf8("中").cast(&LogicalType::Varbinary(Some(2))), + Err(DatabaseError::TooLong) + )); assert_eq!( cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Boolean, - &utf8("true") + utf8("true") )?, DataValue::Boolean(true) ); @@ -1596,7 +1753,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Integer, - &utf8("12") + utf8("12") )?, DataValue::Int32(12) ); @@ -1604,7 +1761,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Tinyint, - &utf8("12") + utf8("12") )?, DataValue::Int8(12) ); @@ -1612,7 +1769,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::UTinyint, - &utf8("12") + utf8("12") )?, DataValue::UInt8(12) ); @@ -1620,7 +1777,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Smallint, - &utf8("12") + utf8("12") )?, DataValue::Int16(12) ); @@ -1628,7 +1785,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::USmallint, - &utf8("12") + utf8("12") )?, DataValue::UInt16(12) ); @@ -1636,7 +1793,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::UInteger, - &utf8("12") + utf8("12") )?, DataValue::UInt32(12) ); @@ -1644,7 +1801,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Bigint, - &utf8("12") + utf8("12") )?, DataValue::Int64(12) ); @@ -1652,7 +1809,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::UBigint, - &utf8("12") + utf8("12") )?, DataValue::UInt64(12) ); @@ -1660,7 +1817,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Float, - &utf8("1.5") + utf8("1.5") )?, DataValue::Float32(OrderedFloat(1.5)) ); @@ -1668,7 +1825,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Double, - &utf8("1.5") + utf8("1.5") )?, DataValue::Float64(OrderedFloat(1.5)) ); @@ -1676,7 +1833,7 @@ mod test { cast_eval( LogicalType::Char(2, CharLengthUnits::Characters), LogicalType::Varchar(Some(2), CharLengthUnits::Characters), - &DataValue::Utf8 { + DataValue::Utf8 { value: "ab".to_string(), ty: Utf8Type::Fixed(2), unit: CharLengthUnits::Characters, @@ -1692,7 +1849,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Char(2, CharLengthUnits::Characters), - &utf8("ab"), + utf8("ab"), )?, DataValue::Utf8 { value: "ab".to_string(), @@ -1704,7 +1861,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Date, - &utf8("2024-01-02") + utf8("2024-01-02") )?, DataValue::Date32( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) @@ -1716,7 +1873,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::DateTime, - &utf8("2024-01-02 03:04:05"), + utf8("2024-01-02 03:04:05"), )?, DataValue::Date64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) @@ -1731,7 +1888,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Time(Some(0)), - &utf8("03:04:05"), + utf8("03:04:05"), )?, DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0) ); @@ -1739,7 +1896,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::TimeStamp(Some(0), true), - &utf8("2024-01-02 03:04:05+00:00"), + utf8("2024-01-02 03:04:05+00:00"), )?, DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) @@ -1758,7 +1915,7 @@ mod test { cast_eval( LogicalType::Varchar(None, CharLengthUnits::Characters), LogicalType::Decimal(None, None), - &utf8("12.34"), + utf8("12.34"), )?, DataValue::Decimal(Decimal::new(1234, 2)) ); @@ -1777,7 +1934,7 @@ mod test { cast_eval( LogicalType::Date, LogicalType::Char(10, CharLengthUnits::Characters), - &date, + date.clone(), )?, DataValue::Utf8 { value: "2024-01-02".to_string(), @@ -1789,7 +1946,7 @@ mod test { cast_eval( LogicalType::Date, LogicalType::Varchar(Some(10), CharLengthUnits::Characters), - &date, + date.clone(), )?, DataValue::Utf8 { value: "2024-01-02".to_string(), @@ -1798,7 +1955,7 @@ mod test { } ); assert!(matches!( - cast_eval(LogicalType::Date, LogicalType::DateTime, &date)?, + cast_eval(LogicalType::Date, LogicalType::DateTime, date.clone())?, DataValue::Date64(_) )); @@ -1811,14 +1968,14 @@ mod test { .timestamp(), ); assert_eq!( - cast_eval(LogicalType::DateTime, LogicalType::Date, &datetime)?, + cast_eval(LogicalType::DateTime, LogicalType::Date, datetime.clone())?, date ); assert_eq!( cast_eval( LogicalType::DateTime, LogicalType::Char(19, CharLengthUnits::Characters), - &datetime, + datetime.clone(), )?, DataValue::Utf8 { value: "2024-01-02 03:04:05".to_string(), @@ -1830,7 +1987,7 @@ mod test { cast_eval( LogicalType::DateTime, LogicalType::Varchar(Some(19), CharLengthUnits::Characters), - &datetime, + datetime.clone(), )?, DataValue::Utf8 { value: "2024-01-02 03:04:05".to_string(), @@ -1839,14 +1996,18 @@ mod test { } ); assert!(matches!( - cast_eval(LogicalType::DateTime, LogicalType::Time(Some(0)), &datetime)?, + cast_eval( + LogicalType::DateTime, + LogicalType::Time(Some(0)), + datetime.clone() + )?, DataValue::Time32(_, 0) )); assert!(matches!( cast_eval( LogicalType::DateTime, LogicalType::TimeStamp(Some(0), false), - &datetime, + datetime.clone(), )?, DataValue::Time64(_, 0, false) )); @@ -1865,7 +2026,7 @@ mod test { cast_eval( LogicalType::TimeStamp(Some(3), false), LogicalType::Date, - ×tamp + timestamp.clone() )?, date ); @@ -1873,10 +2034,42 @@ mod test { cast_eval( LogicalType::TimeStamp(Some(3), false), LogicalType::Time(Some(3)), - ×tamp, + timestamp.clone(), )?, DataValue::Time32(_, 3) )); + assert_eq!( + cast_eval( + LogicalType::Date, + LogicalType::Varbinary(None), + date.clone() + )?, + DataValue::from(b"2024-01-02".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::DateTime, + LogicalType::Varbinary(None), + datetime.clone() + )?, + DataValue::from(b"2024-01-02 03:04:05".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::TimeStamp(Some(3), false), + LogicalType::Varbinary(None), + timestamp.clone() + )?, + DataValue::from(b"2024-01-02 03:04:05.123".to_vec()) + ); + assert_eq!( + cast_eval( + LogicalType::Time(Some(0)), + LogicalType::Varbinary(None), + DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0) + )?, + DataValue::from(b"03:04:05".to_vec()) + ); let DataValue::Time64(timestamp_value, ..) = ×tamp else { unreachable!("timestamp fixture must be Time64") }; @@ -1884,7 +2077,7 @@ mod test { cast_eval( LogicalType::TimeStamp(Some(3), false), LogicalType::TimeStamp(Some(3), true), - ×tamp, + timestamp.clone(), )?, DataValue::Time64(*timestamp_value, 3, true) ); @@ -1892,7 +2085,7 @@ mod test { cast_eval( LogicalType::Time(Some(0)), LogicalType::Char(8, CharLengthUnits::Characters), - &DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0), + DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0), )?, DataValue::Utf8 { value: "03:04:05".to_string(), @@ -1904,7 +2097,7 @@ mod test { cast_eval( LogicalType::Time(Some(0)), LogicalType::Varchar(Some(8), CharLengthUnits::Characters), - &DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0), + DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0), )?, DataValue::Utf8 { value: "03:04:05".to_string(), @@ -1916,7 +2109,7 @@ mod test { cast_eval( LogicalType::TimeStamp(Some(3), false), LogicalType::Char(23, CharLengthUnits::Characters), - ×tamp, + timestamp.clone(), )?, DataValue::Utf8 { value: "2024-01-02 03:04:05.123".to_string(), @@ -1928,7 +2121,7 @@ mod test { cast_eval( LogicalType::TimeStamp(Some(3), false), LogicalType::Varchar(Some(23), CharLengthUnits::Characters), - ×tamp, + timestamp.clone(), )?, DataValue::Utf8 { value: "2024-01-02 03:04:05.123".to_string(), @@ -1943,6 +2136,40 @@ mod test { #[test] fn test_cast_create_dispatches_tuple_and_rejects_unsupported_casts() -> Result<(), DatabaseError> { + assert!(create( + LogicalType::Tuple(vec![LogicalType::Integer]), + LogicalType::Varbinary(None) + ) + .is_err()); + assert!(create(LogicalType::Varbinary(None), LogicalType::Bigint).is_err()); + let bytes = vec![0, 255]; + let buffer = bytes.as_ptr(); + let binary = create( + LogicalType::Varbinary(None), + LogicalType::Varbinary(Some(2)), + )? + .eval(DataValue::from(bytes))?; + assert_eq!(binary.binary().unwrap().as_ptr(), buffer); + assert_eq!( + binary.clone().cast(&LogicalType::Varbinary(Some(2)))?, + DataValue::Binary { + value: vec![0, 255], + len: Some(2) + } + ); + assert_eq!( + cast_eval( + LogicalType::Varbinary(None), + LogicalType::Varbinary(Some(2)), + DataValue::Null + )?, + DataValue::Null + ); + assert!(matches!( + binary.cast(&LogicalType::Varbinary(Some(1))), + Err(DatabaseError::TooLong) + )); + let tuple = DataValue::Tuple(vec![DataValue::Int32(1), utf8("2")]); assert_eq!( cast_eval( @@ -1951,7 +2178,7 @@ mod test { LogicalType::Varchar(None, CharLengthUnits::Characters), ]), LogicalType::Tuple(vec![LogicalType::Bigint, LogicalType::Integer]), - &tuple, + tuple.clone(), )?, DataValue::Tuple(vec![DataValue::Int64(1), DataValue::Int32(2)]) ); @@ -1968,7 +2195,7 @@ mod test { Err(DatabaseError::CastFail { .. }) )); assert_eq!( - create(LogicalType::Integer, LogicalType::Integer)?.eval(&DataValue::Int32(1))?, + create(LogicalType::Integer, LogicalType::Integer)?.eval(DataValue::Int32(1))?, DataValue::Int32(1) ); @@ -1982,7 +2209,7 @@ mod test { cast_eval( LogicalType::Float, LogicalType::Decimal(Some(4), Some(2)), - &DataValue::Float32(OrderedFloat(f32::NAN)), + DataValue::Float32(OrderedFloat(f32::NAN)), ), Err(DatabaseError::CastFail { .. }) )); @@ -1990,7 +2217,7 @@ mod test { cast_eval( LogicalType::Double, LogicalType::Decimal(Some(4), Some(2)), - &DataValue::Float64(OrderedFloat(f64::NAN)), + DataValue::Float64(OrderedFloat(f64::NAN)), ), Err(DatabaseError::CastFail { .. }) )); @@ -1999,6 +2226,14 @@ mod test { #[cfg(feature = "decimal")] #[test] fn test_cast_create_dispatches_decimal_casts() -> Result<(), DatabaseError> { + assert_eq!( + cast_eval( + LogicalType::Decimal(None, Some(2)), + LogicalType::Varbinary(None), + DataValue::Decimal(Decimal::new(125, 2)) + )?, + DataValue::from(b"1.25".to_vec()) + ); let value = DataValue::Decimal(Decimal::new(125, 1)); let cases = vec![ (LogicalType::Float, DataValue::Float32(OrderedFloat(12.5))), @@ -2035,7 +2270,7 @@ mod test { for (to, expected) in cases { assert_eq!( - cast_eval(LogicalType::Decimal(None, None), to, &value)?, + cast_eval(LogicalType::Decimal(None, None), to, value.clone())?, expected ); } diff --git a/src/types/evaluator/date.rs b/src/types/evaluator/date.rs index e6b71583..9df5454e 100644 --- a/src/types/evaluator/date.rs +++ b/src/types/evaluator/date.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::numeric_binary_evaluator_definition; -use crate::types::evaluator::cast::{cast_fail, to_char, to_varchar}; +use crate::types::evaluator::cast::{cast_fail, to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -27,7 +27,7 @@ crate::define_cast_evaluator!( }, DataValue::Date32(value) => |this| { to_char( - DataValue::format_date(*value).ok_or_else(|| { + DataValue::format_date(value).ok_or_else(|| { cast_fail(LogicalType::Date, LogicalType::Char(this.len, this.unit)) })?, this.len, @@ -42,7 +42,7 @@ crate::define_cast_evaluator!( }, DataValue::Date32(value) => |this| { to_varchar( - DataValue::format_date(*value).ok_or_else(|| { + DataValue::format_date(value).ok_or_else(|| { cast_fail(LogicalType::Date, LogicalType::Varchar(this.len, this.unit)) })?, this.len, @@ -50,8 +50,19 @@ crate::define_cast_evaluator!( ) } ); +crate::define_cast_evaluator!( + date32_to_varbinary_cast_eval { len: Option }, + DataValue::Date32(value) => |this| { + to_varbinary( + DataValue::format_date(value).ok_or_else(|| { + cast_fail(LogicalType::Date, LogicalType::Varbinary(this.len)) + })?.into_bytes(), + this.len, + ) + } +); crate::define_cast_evaluator!(date32_to_datetime_cast_eval, DataValue::Date32(value) => { - let value = NaiveDate::from_num_days_from_ce_opt(*value) + let value = NaiveDate::from_num_days_from_ce_opt(value) .ok_or_else(|| cast_fail(LogicalType::Date, LogicalType::DateTime))? .and_hms_opt(0, 0, 0) .ok_or_else(|| cast_fail(LogicalType::Date, LogicalType::DateTime))? @@ -78,7 +89,7 @@ mod test { .num_days_from_ce(), ); assert_eq!( - date32_to_char_cast_eval(10, CharLengthUnits::Characters, &value).unwrap(), + date32_to_char_cast_eval(10, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "2024-01-02".to_string(), ty: Utf8Type::Fixed(10), @@ -86,7 +97,8 @@ mod test { } ); assert_eq!( - date32_to_varchar_cast_eval(Some(10), CharLengthUnits::Characters, &value).unwrap(), + date32_to_varchar_cast_eval(Some(10), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "2024-01-02".to_string(), ty: Utf8Type::Variable(Some(10)), @@ -94,7 +106,7 @@ mod test { } ); assert_eq!( - date32_to_datetime_cast_eval(&value).unwrap(), + date32_to_datetime_cast_eval(value.clone()).unwrap(), DataValue::Date64( NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -110,15 +122,15 @@ mod test { ); let invalid = DataValue::Date32(i32::MAX); assert!(matches!( - date32_to_char_cast_eval(10, CharLengthUnits::Characters, &invalid), + date32_to_char_cast_eval(10, CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - date32_to_varchar_cast_eval(Some(10), CharLengthUnits::Characters, &invalid), + date32_to_varchar_cast_eval(Some(10), CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - date32_to_datetime_cast_eval(&invalid), + date32_to_datetime_cast_eval(invalid.clone()), Err(DatabaseError::CastFail { .. }) )); } diff --git a/src/types/evaluator/datetime.rs b/src/types/evaluator/datetime.rs index ca9daaea..ad8a448d 100644 --- a/src/types/evaluator/datetime.rs +++ b/src/types/evaluator/datetime.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::numeric_binary_evaluator_definition; -use crate::types::evaluator::cast::{cast_fail, to_char, to_varchar}; +use crate::types::evaluator::cast::{cast_fail, to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -27,7 +27,7 @@ crate::define_cast_evaluator!( }, DataValue::Date64(value) => |this| { to_char( - DataValue::format_datetime(*value).ok_or_else(|| { + DataValue::format_datetime(value).ok_or_else(|| { cast_fail(LogicalType::DateTime, LogicalType::Char(this.len, this.unit)) })?, this.len, @@ -42,7 +42,7 @@ crate::define_cast_evaluator!( }, DataValue::Date64(value) => |this| { to_varchar( - DataValue::format_datetime(*value).ok_or_else(|| { + DataValue::format_datetime(value).ok_or_else(|| { cast_fail(LogicalType::DateTime, LogicalType::Varchar(this.len, this.unit)) })?, this.len, @@ -50,8 +50,19 @@ crate::define_cast_evaluator!( ) } ); +crate::define_cast_evaluator!( + date64_to_varbinary_cast_eval { len: Option }, + DataValue::Date64(value) => |this| { + to_varbinary( + DataValue::format_datetime(value).ok_or_else(|| { + cast_fail(LogicalType::DateTime, LogicalType::Varbinary(this.len)) + })?.into_bytes(), + this.len, + ) + } +); crate::define_cast_evaluator!(date64_to_date_cast_eval, DataValue::Date64(value) => { - let value = DateTime::from_timestamp(*value, 0) + let value = DateTime::from_timestamp(value, 0) .ok_or_else(|| cast_fail(LogicalType::DateTime, LogicalType::Date))? .naive_utc() .date() @@ -66,7 +77,7 @@ crate::define_cast_evaluator!( }, DataValue::Date64(value) => |this| { let precision = this.precision.unwrap_or(0); - let value = DateTime::from_timestamp(*value, 0) + let value = DateTime::from_timestamp(value, 0) .map(|date_time| date_time.time().num_seconds_from_midnight()) .ok_or_else(|| { cast_fail(LogicalType::DateTime, LogicalType::Time(this.precision)) @@ -81,7 +92,7 @@ crate::define_cast_evaluator!( zone: bool }, DataValue::Date64(value) => |this| { - Ok(DataValue::Time64(*value, this.precision.unwrap_or(0), this.zone)) + Ok(DataValue::Time64(value, this.precision.unwrap_or(0), this.zone)) } ); @@ -104,7 +115,7 @@ mod test { .timestamp(), ); assert_eq!( - date64_to_char_cast_eval(19, CharLengthUnits::Characters, &value).unwrap(), + date64_to_char_cast_eval(19, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "2024-01-02 03:04:05".to_string(), ty: Utf8Type::Fixed(19), @@ -112,7 +123,8 @@ mod test { } ); assert_eq!( - date64_to_varchar_cast_eval(Some(19), CharLengthUnits::Characters, &value).unwrap(), + date64_to_varchar_cast_eval(Some(19), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "2024-01-02 03:04:05".to_string(), ty: Utf8Type::Variable(Some(19)), @@ -120,7 +132,7 @@ mod test { } ); assert_eq!( - date64_to_date_cast_eval(&value).unwrap(), + date64_to_date_cast_eval(value.clone()).unwrap(), DataValue::Date32( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -128,11 +140,11 @@ mod test { ) ); assert_eq!( - date64_to_time_cast_eval(Some(0), &value).unwrap(), + date64_to_time_cast_eval(Some(0), value.clone()).unwrap(), DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0) ); assert_eq!( - date64_to_timestamp_cast_eval(Some(0), true, &value).unwrap(), + date64_to_timestamp_cast_eval(Some(0), true, value.clone()).unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -151,19 +163,19 @@ mod test { let invalid = DataValue::Date64(i64::MAX); assert!(matches!( - date64_to_char_cast_eval(19, CharLengthUnits::Characters, &invalid), + date64_to_char_cast_eval(19, CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - date64_to_varchar_cast_eval(Some(19), CharLengthUnits::Characters, &invalid), + date64_to_varchar_cast_eval(Some(19), CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - date64_to_date_cast_eval(&invalid), + date64_to_date_cast_eval(invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - date64_to_time_cast_eval(Some(0), &invalid), + date64_to_time_cast_eval(Some(0), invalid.clone()), Err(DatabaseError::CastFail { .. }) )); } diff --git a/src/types/evaluator/decimal.rs b/src/types/evaluator/decimal.rs index d9f54d80..c4cf41c1 100644 --- a/src/types/evaluator/decimal.rs +++ b/src/types/evaluator/decimal.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::types::evaluator::cast::{to_char, to_varchar}; +use crate::types::evaluator::cast::{to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; use ordered_float::OrderedFloat; @@ -60,7 +60,7 @@ crate::define_cast_evaluator!(decimal_to_double_cast_eval, DataValue::Decimal(va })?))) }); crate::define_cast_evaluator!(decimal_to_decimal_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::Decimal(*value)) + Ok(DataValue::Decimal(value)) }); crate::define_cast_evaluator!( decimal_to_char_cast_eval { @@ -76,29 +76,33 @@ crate::define_cast_evaluator!( }, DataValue::Decimal(value) => |this| to_varchar(value.to_string(), this.len, this.unit) ); +crate::define_cast_evaluator!( + decimal_to_varbinary_cast_eval { len: Option }, + DataValue::Decimal(value) => |this| to_varbinary(value.to_string().into_bytes(), this.len) +); crate::define_cast_evaluator!(decimal_to_tinyint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::Int8(crate::decimal_to_int_cast!(*value, i8))) + Ok(DataValue::Int8(crate::decimal_to_int_cast!(value, i8))) }); crate::define_cast_evaluator!(decimal_to_smallint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::Int16(crate::decimal_to_int_cast!(*value, i16))) + Ok(DataValue::Int16(crate::decimal_to_int_cast!(value, i16))) }); crate::define_cast_evaluator!(decimal_to_integer_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::Int32(crate::decimal_to_int_cast!(*value, i32))) + Ok(DataValue::Int32(crate::decimal_to_int_cast!(value, i32))) }); crate::define_cast_evaluator!(decimal_to_bigint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::Int64(crate::decimal_to_int_cast!(*value, i64))) + Ok(DataValue::Int64(crate::decimal_to_int_cast!(value, i64))) }); crate::define_cast_evaluator!(decimal_to_utinyint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::UInt8(crate::decimal_to_int_cast!(*value, u8))) + Ok(DataValue::UInt8(crate::decimal_to_int_cast!(value, u8))) }); crate::define_cast_evaluator!(decimal_to_usmallint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::UInt16(crate::decimal_to_int_cast!(*value, u16))) + Ok(DataValue::UInt16(crate::decimal_to_int_cast!(value, u16))) }); crate::define_cast_evaluator!(decimal_to_uinteger_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::UInt32(crate::decimal_to_int_cast!(*value, u32))) + Ok(DataValue::UInt32(crate::decimal_to_int_cast!(value, u32))) }); crate::define_cast_evaluator!(decimal_to_ubigint_cast_eval, DataValue::Decimal(value) => { - Ok(DataValue::UInt64(crate::decimal_to_int_cast!(*value, u64))) + Ok(DataValue::UInt64(crate::decimal_to_int_cast!(value, u64))) }); decimal_binary!( decimal_multiply_binary_eval, @@ -150,19 +154,19 @@ mod test { let value = DataValue::Decimal(Decimal::new(125, 1)); assert_eq!( - decimal_to_float_cast_eval(&value).unwrap(), + decimal_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(12.5)) ); assert_eq!( - decimal_to_double_cast_eval(&value).unwrap(), + decimal_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(12.5)) ); assert_eq!( - decimal_to_decimal_cast_eval(&value).unwrap(), + decimal_to_decimal_cast_eval(value.clone()).unwrap(), DataValue::Decimal(Decimal::new(125, 1)) ); assert_eq!( - decimal_to_char_cast_eval(4, CharLengthUnits::Characters, &value).unwrap(), + decimal_to_char_cast_eval(4, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "12.5".to_string(), ty: Utf8Type::Fixed(4), @@ -170,7 +174,8 @@ mod test { } ); assert_eq!( - decimal_to_varchar_cast_eval(Some(4), CharLengthUnits::Characters, &value).unwrap(), + decimal_to_varchar_cast_eval(Some(4), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "12.5".to_string(), ty: Utf8Type::Variable(Some(4)), @@ -178,35 +183,35 @@ mod test { } ); assert_eq!( - decimal_to_tinyint_cast_eval(&value).unwrap(), + decimal_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(12) ); assert_eq!( - decimal_to_smallint_cast_eval(&value).unwrap(), + decimal_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(12) ); assert_eq!( - decimal_to_integer_cast_eval(&value).unwrap(), + decimal_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(12) ); assert_eq!( - decimal_to_bigint_cast_eval(&value).unwrap(), + decimal_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(12) ); assert_eq!( - decimal_to_utinyint_cast_eval(&value).unwrap(), + decimal_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(12) ); assert_eq!( - decimal_to_usmallint_cast_eval(&value).unwrap(), + decimal_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(12) ); assert_eq!( - decimal_to_uinteger_cast_eval(&value).unwrap(), + decimal_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(12) ); assert_eq!( - decimal_to_ubigint_cast_eval(&value).unwrap(), + decimal_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(12) ); } diff --git a/src/types/evaluator/float32.rs b/src/types/evaluator/float32.rs index b7620cc5..af4e2d69 100644 --- a/src/types/evaluator/float32.rs +++ b/src/types/evaluator/float32.rs @@ -125,47 +125,47 @@ mod test { let value = DataValue::Float32(OrderedFloat(1.5)); assert_eq!( - float32_to_float_cast_eval(&value).unwrap(), + float32_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.5)) ); assert_eq!( - float32_to_double_cast_eval(&value).unwrap(), + float32_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.5)) ); assert_eq!( - float32_to_tinyint_cast_eval(&value).unwrap(), + float32_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - float32_to_smallint_cast_eval(&value).unwrap(), + float32_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - float32_to_integer_cast_eval(&value).unwrap(), + float32_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - float32_to_bigint_cast_eval(&value).unwrap(), + float32_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - float32_to_utinyint_cast_eval(&value).unwrap(), + float32_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - float32_to_usmallint_cast_eval(&value).unwrap(), + float32_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - float32_to_uinteger_cast_eval(&value).unwrap(), + float32_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - float32_to_ubigint_cast_eval(&value).unwrap(), + float32_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - float32_to_char_cast_eval(3, CharLengthUnits::Characters, &value).unwrap(), + float32_to_char_cast_eval(3, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1.5".to_string(), ty: Utf8Type::Fixed(3), @@ -173,7 +173,8 @@ mod test { } ); assert_eq!( - float32_to_varchar_cast_eval(Some(3), CharLengthUnits::Characters, &value).unwrap(), + float32_to_varchar_cast_eval(Some(3), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1.5".to_string(), ty: Utf8Type::Variable(Some(3)), @@ -182,7 +183,7 @@ mod test { ); #[cfg(feature = "decimal")] assert_eq!( - float32_to_decimal_cast_eval(None, Some(1), &value).unwrap(), + float32_to_decimal_cast_eval(None, Some(1), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(15, 1)) ); } diff --git a/src/types/evaluator/float64.rs b/src/types/evaluator/float64.rs index a1bc68d5..a4ec98c8 100644 --- a/src/types/evaluator/float64.rs +++ b/src/types/evaluator/float64.rs @@ -14,9 +14,9 @@ use crate::errors::DatabaseError; #[cfg(feature = "decimal")] -use crate::types::evaluator::cast::{cast_fail, to_char, to_varchar}; +use crate::types::evaluator::cast::{cast_fail, to_char, to_varbinary, to_varchar}; #[cfg(not(feature = "decimal"))] -use crate::types::evaluator::cast::{to_char, to_varchar}; +use crate::types::evaluator::cast::{to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; #[cfg(feature = "decimal")] @@ -65,7 +65,7 @@ crate::define_cast_evaluator!(float64_to_float_cast_eval, DataValue::Float64(val Ok(DataValue::Float32(ordered_float::OrderedFloat(value.0 as f32))) }); crate::define_cast_evaluator!(float64_to_double_cast_eval, DataValue::Float64(value) => { - Ok(DataValue::Float64(*value)) + Ok(DataValue::Float64(value)) }); crate::define_cast_evaluator!(float64_to_tinyint_cast_eval, DataValue::Float64(value) => { Ok(DataValue::Int8(crate::float_to_int_cast!(value.into_inner(), i8, f64)?)) @@ -105,6 +105,10 @@ crate::define_cast_evaluator!( }, DataValue::Float64(value) => |this| to_varchar(value.to_string(), this.len, this.unit) ); +crate::define_cast_evaluator!( + float64_to_varbinary_cast_eval { len: Option }, + DataValue::Float64(value) => |this| to_varbinary(value.to_string().into_bytes(), this.len) +); #[cfg(feature = "decimal")] crate::define_cast_evaluator!( float64_to_decimal_cast_eval { @@ -289,47 +293,47 @@ mod test { DataValue::Null ); assert_eq!( - float64_to_float_cast_eval(&value).unwrap(), + float64_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(ordered_float::OrderedFloat(1.5)) ); assert_eq!( - float64_to_double_cast_eval(&value).unwrap(), + float64_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(ordered_float::OrderedFloat(1.5)) ); assert_eq!( - float64_to_tinyint_cast_eval(&value).unwrap(), + float64_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - float64_to_smallint_cast_eval(&value).unwrap(), + float64_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - float64_to_integer_cast_eval(&value).unwrap(), + float64_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - float64_to_bigint_cast_eval(&value).unwrap(), + float64_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - float64_to_utinyint_cast_eval(&value).unwrap(), + float64_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - float64_to_usmallint_cast_eval(&value).unwrap(), + float64_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - float64_to_uinteger_cast_eval(&value).unwrap(), + float64_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - float64_to_ubigint_cast_eval(&value).unwrap(), + float64_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - float64_to_char_cast_eval(3, CharLengthUnits::Characters, &value).unwrap(), + float64_to_char_cast_eval(3, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1.5".to_string(), ty: Utf8Type::Fixed(3), @@ -337,7 +341,8 @@ mod test { } ); assert_eq!( - float64_to_varchar_cast_eval(Some(3), CharLengthUnits::Characters, &value).unwrap(), + float64_to_varchar_cast_eval(Some(3), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1.5".to_string(), ty: Utf8Type::Variable(Some(3)), @@ -346,7 +351,7 @@ mod test { ); #[cfg(feature = "decimal")] assert_eq!( - float64_to_decimal_cast_eval(None, Some(1), &value).unwrap(), + float64_to_decimal_cast_eval(None, Some(1), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(15, 1)) ); #[cfg(feature = "decimal")] @@ -354,7 +359,7 @@ mod test { float64_to_decimal_cast_eval( Some(4), Some(2), - &DataValue::Float64(ordered_float::OrderedFloat(f64::NAN)), + DataValue::Float64(ordered_float::OrderedFloat(f64::NAN)), ), Err(DatabaseError::CastFail { .. }) )); diff --git a/src/types/evaluator/int16.rs b/src/types/evaluator/int16.rs index f74199dc..4432bb27 100644 --- a/src/types/evaluator/int16.rs +++ b/src/types/evaluator/int16.rs @@ -34,51 +34,51 @@ mod test { let value = DataValue::Int16(1); assert_eq!( - int16_to_boolean_cast_eval(&value).unwrap(), + int16_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - int16_to_tinyint_cast_eval(&value).unwrap(), + int16_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - int16_to_utinyint_cast_eval(&value).unwrap(), + int16_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - int16_to_smallint_cast_eval(&value).unwrap(), + int16_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - int16_to_usmallint_cast_eval(&value).unwrap(), + int16_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - int16_to_integer_cast_eval(&value).unwrap(), + int16_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - int16_to_uinteger_cast_eval(&value).unwrap(), + int16_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - int16_to_bigint_cast_eval(&value).unwrap(), + int16_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - int16_to_ubigint_cast_eval(&value).unwrap(), + int16_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - int16_to_float_cast_eval(&value).unwrap(), + int16_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - int16_to_double_cast_eval(&value).unwrap(), + int16_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - int16_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + int16_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -86,7 +86,8 @@ mod test { } ); assert_eq!( - int16_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + int16_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -94,7 +95,7 @@ mod test { } ); assert_eq!( - int16_to_decimal_cast_eval(Some(2), &value).unwrap(), + int16_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/int32.rs b/src/types/evaluator/int32.rs index 0f43e129..32b97f5b 100644 --- a/src/types/evaluator/int32.rs +++ b/src/types/evaluator/int32.rs @@ -54,51 +54,51 @@ mod test { let value = DataValue::Int32(1); assert_eq!( - int32_to_boolean_cast_eval(&value).unwrap(), + int32_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - int32_to_tinyint_cast_eval(&value).unwrap(), + int32_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - int32_to_utinyint_cast_eval(&value).unwrap(), + int32_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - int32_to_smallint_cast_eval(&value).unwrap(), + int32_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - int32_to_usmallint_cast_eval(&value).unwrap(), + int32_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - int32_to_integer_cast_eval(&value).unwrap(), + int32_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - int32_to_uinteger_cast_eval(&value).unwrap(), + int32_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - int32_to_bigint_cast_eval(&value).unwrap(), + int32_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - int32_to_ubigint_cast_eval(&value).unwrap(), + int32_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - int32_to_float_cast_eval(&value).unwrap(), + int32_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - int32_to_double_cast_eval(&value).unwrap(), + int32_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - int32_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + int32_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -106,7 +106,8 @@ mod test { } ); assert_eq!( - int32_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + int32_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -114,7 +115,7 @@ mod test { } ); assert_eq!( - int32_to_decimal_cast_eval(Some(1), &value).unwrap(), + int32_to_decimal_cast_eval(Some(1), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(10, 1)) ); } diff --git a/src/types/evaluator/int64.rs b/src/types/evaluator/int64.rs index 0cd23c8c..2206f6e7 100644 --- a/src/types/evaluator/int64.rs +++ b/src/types/evaluator/int64.rs @@ -34,51 +34,51 @@ mod test { let value = DataValue::Int64(1); assert_eq!( - int64_to_boolean_cast_eval(&value).unwrap(), + int64_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - int64_to_tinyint_cast_eval(&value).unwrap(), + int64_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - int64_to_utinyint_cast_eval(&value).unwrap(), + int64_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - int64_to_smallint_cast_eval(&value).unwrap(), + int64_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - int64_to_usmallint_cast_eval(&value).unwrap(), + int64_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - int64_to_integer_cast_eval(&value).unwrap(), + int64_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - int64_to_uinteger_cast_eval(&value).unwrap(), + int64_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - int64_to_bigint_cast_eval(&value).unwrap(), + int64_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - int64_to_ubigint_cast_eval(&value).unwrap(), + int64_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - int64_to_float_cast_eval(&value).unwrap(), + int64_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - int64_to_double_cast_eval(&value).unwrap(), + int64_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - int64_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + int64_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -86,7 +86,8 @@ mod test { } ); assert_eq!( - int64_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + int64_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -94,7 +95,7 @@ mod test { } ); assert_eq!( - int64_to_decimal_cast_eval(Some(2), &value).unwrap(), + int64_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/int8.rs b/src/types/evaluator/int8.rs index 5f69cc7a..b7a55756 100644 --- a/src/types/evaluator/int8.rs +++ b/src/types/evaluator/int8.rs @@ -34,51 +34,51 @@ mod test { let value = DataValue::Int8(1); assert_eq!( - int8_to_boolean_cast_eval(&value).unwrap(), + int8_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - int8_to_tinyint_cast_eval(&value).unwrap(), + int8_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - int8_to_utinyint_cast_eval(&value).unwrap(), + int8_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - int8_to_smallint_cast_eval(&value).unwrap(), + int8_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - int8_to_usmallint_cast_eval(&value).unwrap(), + int8_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - int8_to_integer_cast_eval(&value).unwrap(), + int8_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - int8_to_uinteger_cast_eval(&value).unwrap(), + int8_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - int8_to_bigint_cast_eval(&value).unwrap(), + int8_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - int8_to_ubigint_cast_eval(&value).unwrap(), + int8_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - int8_to_float_cast_eval(&value).unwrap(), + int8_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - int8_to_double_cast_eval(&value).unwrap(), + int8_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - int8_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + int8_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -86,7 +86,7 @@ mod test { } ); assert_eq!( - int8_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + int8_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -94,7 +94,7 @@ mod test { } ); assert_eq!( - int8_to_decimal_cast_eval(Some(2), &value).unwrap(), + int8_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/mod.rs b/src/types/evaluator/mod.rs index 10d1f3da..c71900a1 100644 --- a/src/types/evaluator/mod.rs +++ b/src/types/evaluator/mod.rs @@ -48,6 +48,7 @@ pub use self::unary::unary_create; use crate::errors::DatabaseError; use crate::types::value::DataValue; use crate::types::CharLengthUnits; +use std::borrow::Cow; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BinaryEvaluatorParams { @@ -68,7 +69,7 @@ impl BinaryEvaluatorRef { pub fn binary_eval( &self, - left: &DataValue, + left: Cow<'_, DataValue>, right: &DataValue, ) -> Result { binary::eval_binary(self.pos, &self.params, left, right) diff --git a/src/types/evaluator/null.rs b/src/types/evaluator/null.rs index 3c9a802d..c1a14760 100644 --- a/src/types/evaluator/null.rs +++ b/src/types/evaluator/null.rs @@ -20,10 +20,10 @@ use crate::types::evaluator::DataValue; pub fn null_binary_eval(_: &DataValue, _: &DataValue) -> Result { Ok(DataValue::Null) } -pub fn to_sql_null_cast_eval(_value: &DataValue) -> Result { +pub fn to_sql_null_cast_eval(_value: DataValue) -> Result { Ok(DataValue::Null) } -pub fn null_cast_eval(_value: &DataValue) -> Result { +pub fn null_cast_eval(_value: DataValue) -> Result { Ok(DataValue::Null) } @@ -35,10 +35,10 @@ mod test { #[test] fn test_null_cast_evaluators() { assert_eq!( - to_sql_null_cast_eval(&DataValue::Int32(1)).unwrap(), + to_sql_null_cast_eval(DataValue::Int32(1)).unwrap(), DataValue::Null ); - assert_eq!(null_cast_eval(&DataValue::Null).unwrap(), DataValue::Null); + assert_eq!(null_cast_eval(DataValue::Null).unwrap(), DataValue::Null); } } // GRCOV_EXCL_STOP diff --git a/src/types/evaluator/time32.rs b/src/types/evaluator/time32.rs index 17eeacf2..e283e60c 100644 --- a/src/types/evaluator/time32.rs +++ b/src/types/evaluator/time32.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::types::evaluator::cast::{cast_fail, to_char, to_varchar}; +use crate::types::evaluator::cast::{cast_fail, to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::value::{ONE_DAY_TO_SEC, ONE_SEC_TO_NANO}; use crate::types::CharLengthUnits; @@ -97,9 +97,9 @@ crate::define_cast_evaluator!( }, DataValue::Time32(value, precision) => |this| { to_char( - DataValue::format_time(*value, *precision).ok_or_else(|| { + DataValue::format_time(value, precision).ok_or_else(|| { cast_fail( - LogicalType::Time(Some(*precision)), + LogicalType::Time(Some(precision)), LogicalType::Char(this.len, this.unit), ) })?, @@ -115,9 +115,9 @@ crate::define_cast_evaluator!( }, DataValue::Time32(value, precision) => |this| { to_varchar( - DataValue::format_time(*value, *precision).ok_or_else(|| { + DataValue::format_time(value, precision).ok_or_else(|| { cast_fail( - LogicalType::Time(Some(*precision)), + LogicalType::Time(Some(precision)), LogicalType::Varchar(this.len, this.unit), ) })?, @@ -126,12 +126,23 @@ crate::define_cast_evaluator!( ) } ); +crate::define_cast_evaluator!( + time32_to_varbinary_cast_eval { len: Option }, + DataValue::Time32(value, precision) => |this| { + to_varbinary( + DataValue::format_time(value, precision).ok_or_else(|| { + cast_fail(LogicalType::Time(Some(precision)), LogicalType::Varbinary(this.len)) + })?.into_bytes(), + this.len, + ) + } +); crate::define_cast_evaluator!( time32_to_time_cast_eval { precision: Option }, DataValue::Time32(value, _precision) => |this| { - Ok(DataValue::Time32(*value, this.precision.unwrap_or(0))) + Ok(DataValue::Time32(value, this.precision.unwrap_or(0))) } ); time32_order_binary!(time_lt_binary_eval, is_lt); @@ -268,7 +279,7 @@ mod test { 3, ); assert_eq!( - time32_to_char_cast_eval(12, CharLengthUnits::Characters, &value).unwrap(), + time32_to_char_cast_eval(12, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "03:04:05.123".to_string(), ty: Utf8Type::Fixed(12), @@ -276,21 +287,25 @@ mod test { } ); assert_eq!( - time32_to_varchar_cast_eval(Some(12), CharLengthUnits::Characters, &value).unwrap(), + time32_to_varchar_cast_eval(Some(12), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "03:04:05.123".to_string(), ty: Utf8Type::Variable(Some(12)), unit: CharLengthUnits::Characters, } ); - assert_eq!(time32_to_time_cast_eval(Some(3), &value).unwrap(), value); + assert_eq!( + time32_to_time_cast_eval(Some(3), value.clone()).unwrap(), + value + ); let invalid = DataValue::Time32(u32::MAX, 0); assert!(matches!( - time32_to_char_cast_eval(8, CharLengthUnits::Characters, &invalid), + time32_to_char_cast_eval(8, CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); assert!(matches!( - time32_to_varchar_cast_eval(Some(8), CharLengthUnits::Characters, &invalid), + time32_to_varchar_cast_eval(Some(8), CharLengthUnits::Characters, invalid.clone()), Err(DatabaseError::CastFail { .. }) )); } diff --git a/src/types/evaluator/time64.rs b/src/types/evaluator/time64.rs index 37c448e0..9abdb1f9 100644 --- a/src/types/evaluator/time64.rs +++ b/src/types/evaluator/time64.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::types::evaluator::cast::{cast_fail, to_char, to_varchar}; +use crate::types::evaluator::cast::{cast_fail, to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -57,9 +57,9 @@ crate::define_cast_evaluator!( }, DataValue::Time64(value, precision, zone) => |this| { to_char( - DataValue::format_timestamp(*value, *precision).ok_or_else(|| { + DataValue::format_timestamp(value, precision).ok_or_else(|| { cast_fail( - LogicalType::TimeStamp(Some(*precision), *zone), + LogicalType::TimeStamp(Some(precision), zone), LogicalType::Char(this.len, this.unit), ) })?, @@ -75,9 +75,9 @@ crate::define_cast_evaluator!( }, DataValue::Time64(value, precision, zone) => |this| { to_varchar( - DataValue::format_timestamp(*value, *precision).ok_or_else(|| { + DataValue::format_timestamp(value, precision).ok_or_else(|| { cast_fail( - LogicalType::TimeStamp(Some(*precision), *zone), + LogicalType::TimeStamp(Some(precision), zone), LogicalType::Varchar(this.len, this.unit), ) })?, @@ -86,13 +86,24 @@ crate::define_cast_evaluator!( ) } ); +crate::define_cast_evaluator!( + time64_to_varbinary_cast_eval { len: Option }, + DataValue::Time64(value, precision, zone) => |this| { + to_varbinary( + DataValue::format_timestamp(value, precision).ok_or_else(|| { + cast_fail(LogicalType::TimeStamp(Some(precision), zone), LogicalType::Varbinary(this.len)) + })?.into_bytes(), + this.len, + ) + } +); crate::define_cast_evaluator!( time64_to_date_cast_eval, DataValue::Time64(value, precision, zone) => { - let value = DataValue::from_timestamp_precision(*value, *precision) + let value = DataValue::from_timestamp_precision(value, precision) .ok_or_else(|| { cast_fail( - LogicalType::TimeStamp(Some(*precision), *zone), + LogicalType::TimeStamp(Some(precision), zone), LogicalType::Date, ) })? @@ -106,10 +117,10 @@ crate::define_cast_evaluator!( crate::define_cast_evaluator!( time64_to_datetime_cast_eval, DataValue::Time64(value, precision, zone) => { - let value = DataValue::from_timestamp_precision(*value, *precision) + let value = DataValue::from_timestamp_precision(value, precision) .ok_or_else(|| { cast_fail( - LogicalType::TimeStamp(Some(*precision), *zone), + LogicalType::TimeStamp(Some(precision), zone), LogicalType::DateTime, ) })? @@ -124,7 +135,7 @@ crate::define_cast_evaluator!( }, DataValue::Time64(value, precision, zone) => |this| { let target_precision = this.precision.unwrap_or(0); - let (value, nano) = DataValue::from_timestamp_precision(*value, *precision) + let (value, nano) = DataValue::from_timestamp_precision(value, precision) .map(|date_time| { ( date_time.time().num_seconds_from_midnight(), @@ -133,7 +144,7 @@ crate::define_cast_evaluator!( }) .ok_or_else(|| { cast_fail( - LogicalType::TimeStamp(Some(*precision), *zone), + LogicalType::TimeStamp(Some(precision), zone), LogicalType::Time(this.precision), ) })?; @@ -150,7 +161,7 @@ crate::define_cast_evaluator!( zone: bool }, DataValue::Time64(value, _precision, _) => |this| { - Ok(DataValue::Time64(*value, this.precision.unwrap_or(0), this.zone)) + Ok(DataValue::Time64(value, this.precision.unwrap_or(0), this.zone)) } ); time64_binary!(time64_lt_binary_eval, <); @@ -225,7 +236,7 @@ mod test { .timestamp_millis(); let value = DataValue::Time64(timestamp, 3, false); assert_eq!( - time64_to_char_cast_eval(23, CharLengthUnits::Characters, &value).unwrap(), + time64_to_char_cast_eval(23, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "2024-01-02 03:04:05.123".to_string(), ty: Utf8Type::Fixed(23), @@ -233,7 +244,8 @@ mod test { } ); assert_eq!( - time64_to_varchar_cast_eval(Some(23), CharLengthUnits::Characters, &value).unwrap(), + time64_to_varchar_cast_eval(Some(23), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "2024-01-02 03:04:05.123".to_string(), ty: Utf8Type::Variable(Some(23)), @@ -241,7 +253,7 @@ mod test { } ); assert_eq!( - time64_to_date_cast_eval(&value).unwrap(), + time64_to_date_cast_eval(value.clone()).unwrap(), DataValue::Date32( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -249,7 +261,7 @@ mod test { ) ); assert_eq!( - time64_to_datetime_cast_eval(&value).unwrap(), + time64_to_datetime_cast_eval(value.clone()).unwrap(), DataValue::Date64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -260,23 +272,28 @@ mod test { ) ); assert_eq!( - time64_to_time_cast_eval(Some(3), &value).unwrap(), + time64_to_time_cast_eval(Some(3), value.clone()).unwrap(), DataValue::Time32( DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 123_000_000, 3), 3 ) ); assert_eq!( - time64_to_timestamp_cast_eval(Some(3), true, &value).unwrap(), + time64_to_timestamp_cast_eval(Some(3), true, value.clone()).unwrap(), DataValue::Time64(timestamp, 3, true) ); let invalid = DataValue::Time64(i64::MAX, 0, false); - assert!(time64_to_char_cast_eval(23, CharLengthUnits::Characters, &invalid).is_err()); - assert!(time64_to_varchar_cast_eval(None, CharLengthUnits::Characters, &invalid).is_err()); - assert!(time64_to_date_cast_eval(&invalid).is_err()); - assert!(time64_to_datetime_cast_eval(&invalid).is_err()); - assert!(time64_to_time_cast_eval(Some(0), &invalid).is_err()); + assert!( + time64_to_char_cast_eval(23, CharLengthUnits::Characters, invalid.clone()).is_err() + ); + assert!( + time64_to_varchar_cast_eval(None, CharLengthUnits::Characters, invalid.clone()) + .is_err() + ); + assert!(time64_to_date_cast_eval(invalid.clone()).is_err()); + assert!(time64_to_datetime_cast_eval(invalid.clone()).is_err()); + assert!(time64_to_time_cast_eval(Some(0), invalid.clone()).is_err()); } } // GRCOV_EXCL_STOP diff --git a/src/types/evaluator/tuple.rs b/src/types/evaluator/tuple.rs index 33dc5671..5625ba0f 100644 --- a/src/types/evaluator/tuple.rs +++ b/src/types/evaluator/tuple.rs @@ -102,14 +102,14 @@ tuple_order_binary!(tuple_lt_eq_binary_eval, is_le); pub(crate) fn eval_tuple_cast( element_evaluators: &[CastEvaluatorRef], - value: &DataValue, + value: DataValue, ) -> Result { match value { DataValue::Null => Ok(DataValue::Null), DataValue::Tuple(values) => { let mut casted = Vec::with_capacity(values.len()); - for (value, evaluator) in values.iter().zip(element_evaluators.iter()) { + for (value, evaluator) in values.into_iter().zip(element_evaluators.iter()) { casted.push(evaluator.eval(value)?); } @@ -207,7 +207,7 @@ mod test { assert_eq!( evaluator - .eval(&DataValue::Tuple(vec![ + .eval(DataValue::Tuple(vec![ DataValue::Int32(1), DataValue::Utf8 { value: "2".to_string(), @@ -218,7 +218,7 @@ mod test { .unwrap(), DataValue::Tuple(vec![DataValue::Int64(1), DataValue::Int32(2)]) ); - assert_eq!(evaluator.eval(&DataValue::Null).unwrap(), DataValue::Null); + assert_eq!(evaluator.eval(DataValue::Null).unwrap(), DataValue::Null); } } // GRCOV_EXCL_STOP diff --git a/src/types/evaluator/uint16.rs b/src/types/evaluator/uint16.rs index 06356bc8..fc434942 100644 --- a/src/types/evaluator/uint16.rs +++ b/src/types/evaluator/uint16.rs @@ -33,51 +33,51 @@ mod test { let value = DataValue::UInt16(1); assert_eq!( - uint16_to_boolean_cast_eval(&value).unwrap(), + uint16_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - uint16_to_tinyint_cast_eval(&value).unwrap(), + uint16_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - uint16_to_utinyint_cast_eval(&value).unwrap(), + uint16_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - uint16_to_smallint_cast_eval(&value).unwrap(), + uint16_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - uint16_to_usmallint_cast_eval(&value).unwrap(), + uint16_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - uint16_to_integer_cast_eval(&value).unwrap(), + uint16_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - uint16_to_uinteger_cast_eval(&value).unwrap(), + uint16_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - uint16_to_bigint_cast_eval(&value).unwrap(), + uint16_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - uint16_to_ubigint_cast_eval(&value).unwrap(), + uint16_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - uint16_to_float_cast_eval(&value).unwrap(), + uint16_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - uint16_to_double_cast_eval(&value).unwrap(), + uint16_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - uint16_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + uint16_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -85,7 +85,8 @@ mod test { } ); assert_eq!( - uint16_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + uint16_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -93,7 +94,7 @@ mod test { } ); assert_eq!( - uint16_to_decimal_cast_eval(Some(2), &value).unwrap(), + uint16_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/uint32.rs b/src/types/evaluator/uint32.rs index 629316ce..64780e8e 100644 --- a/src/types/evaluator/uint32.rs +++ b/src/types/evaluator/uint32.rs @@ -33,51 +33,51 @@ mod test { let value = DataValue::UInt32(1); assert_eq!( - uint32_to_boolean_cast_eval(&value).unwrap(), + uint32_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - uint32_to_tinyint_cast_eval(&value).unwrap(), + uint32_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - uint32_to_utinyint_cast_eval(&value).unwrap(), + uint32_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - uint32_to_smallint_cast_eval(&value).unwrap(), + uint32_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - uint32_to_usmallint_cast_eval(&value).unwrap(), + uint32_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - uint32_to_integer_cast_eval(&value).unwrap(), + uint32_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - uint32_to_uinteger_cast_eval(&value).unwrap(), + uint32_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - uint32_to_bigint_cast_eval(&value).unwrap(), + uint32_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - uint32_to_ubigint_cast_eval(&value).unwrap(), + uint32_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - uint32_to_float_cast_eval(&value).unwrap(), + uint32_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - uint32_to_double_cast_eval(&value).unwrap(), + uint32_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - uint32_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + uint32_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -85,7 +85,8 @@ mod test { } ); assert_eq!( - uint32_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + uint32_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -93,7 +94,7 @@ mod test { } ); assert_eq!( - uint32_to_decimal_cast_eval(Some(2), &value).unwrap(), + uint32_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/uint64.rs b/src/types/evaluator/uint64.rs index 18a12b62..dca97772 100644 --- a/src/types/evaluator/uint64.rs +++ b/src/types/evaluator/uint64.rs @@ -33,51 +33,51 @@ mod test { let value = DataValue::UInt64(1); assert_eq!( - uint64_to_boolean_cast_eval(&value).unwrap(), + uint64_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - uint64_to_tinyint_cast_eval(&value).unwrap(), + uint64_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - uint64_to_utinyint_cast_eval(&value).unwrap(), + uint64_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - uint64_to_smallint_cast_eval(&value).unwrap(), + uint64_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - uint64_to_usmallint_cast_eval(&value).unwrap(), + uint64_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - uint64_to_integer_cast_eval(&value).unwrap(), + uint64_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - uint64_to_uinteger_cast_eval(&value).unwrap(), + uint64_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - uint64_to_bigint_cast_eval(&value).unwrap(), + uint64_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - uint64_to_ubigint_cast_eval(&value).unwrap(), + uint64_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - uint64_to_float_cast_eval(&value).unwrap(), + uint64_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - uint64_to_double_cast_eval(&value).unwrap(), + uint64_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - uint64_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + uint64_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -85,7 +85,8 @@ mod test { } ); assert_eq!( - uint64_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + uint64_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -93,7 +94,7 @@ mod test { } ); assert_eq!( - uint64_to_decimal_cast_eval(Some(2), &value).unwrap(), + uint64_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/uint8.rs b/src/types/evaluator/uint8.rs index 96f19e7a..56da2fa3 100644 --- a/src/types/evaluator/uint8.rs +++ b/src/types/evaluator/uint8.rs @@ -33,51 +33,51 @@ mod test { let value = DataValue::UInt8(1); assert_eq!( - uint8_to_boolean_cast_eval(&value).unwrap(), + uint8_to_boolean_cast_eval(value.clone()).unwrap(), DataValue::Boolean(true) ); assert_eq!( - uint8_to_tinyint_cast_eval(&value).unwrap(), + uint8_to_tinyint_cast_eval(value.clone()).unwrap(), DataValue::Int8(1) ); assert_eq!( - uint8_to_utinyint_cast_eval(&value).unwrap(), + uint8_to_utinyint_cast_eval(value.clone()).unwrap(), DataValue::UInt8(1) ); assert_eq!( - uint8_to_smallint_cast_eval(&value).unwrap(), + uint8_to_smallint_cast_eval(value.clone()).unwrap(), DataValue::Int16(1) ); assert_eq!( - uint8_to_usmallint_cast_eval(&value).unwrap(), + uint8_to_usmallint_cast_eval(value.clone()).unwrap(), DataValue::UInt16(1) ); assert_eq!( - uint8_to_integer_cast_eval(&value).unwrap(), + uint8_to_integer_cast_eval(value.clone()).unwrap(), DataValue::Int32(1) ); assert_eq!( - uint8_to_uinteger_cast_eval(&value).unwrap(), + uint8_to_uinteger_cast_eval(value.clone()).unwrap(), DataValue::UInt32(1) ); assert_eq!( - uint8_to_bigint_cast_eval(&value).unwrap(), + uint8_to_bigint_cast_eval(value.clone()).unwrap(), DataValue::Int64(1) ); assert_eq!( - uint8_to_ubigint_cast_eval(&value).unwrap(), + uint8_to_ubigint_cast_eval(value.clone()).unwrap(), DataValue::UInt64(1) ); assert_eq!( - uint8_to_float_cast_eval(&value).unwrap(), + uint8_to_float_cast_eval(value.clone()).unwrap(), DataValue::Float32(OrderedFloat(1.0)) ); assert_eq!( - uint8_to_double_cast_eval(&value).unwrap(), + uint8_to_double_cast_eval(value.clone()).unwrap(), DataValue::Float64(OrderedFloat(1.0)) ); assert_eq!( - uint8_to_char_cast_eval(1, CharLengthUnits::Characters, &value).unwrap(), + uint8_to_char_cast_eval(1, CharLengthUnits::Characters, value.clone()).unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Fixed(1), @@ -85,7 +85,8 @@ mod test { } ); assert_eq!( - uint8_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, &value).unwrap(), + uint8_to_varchar_cast_eval(Some(1), CharLengthUnits::Characters, value.clone()) + .unwrap(), DataValue::Utf8 { value: "1".to_string(), ty: Utf8Type::Variable(Some(1)), @@ -93,7 +94,7 @@ mod test { } ); assert_eq!( - uint8_to_decimal_cast_eval(Some(2), &value).unwrap(), + uint8_to_decimal_cast_eval(Some(2), value.clone()).unwrap(), DataValue::Decimal(Decimal::new(100, 2)) ); } diff --git a/src/types/evaluator/utf8.rs b/src/types/evaluator/utf8.rs index a73aeafd..8d2fa698 100644 --- a/src/types/evaluator/utf8.rs +++ b/src/types/evaluator/utf8.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::types::evaluator::cast::{to_char, to_varchar}; +use crate::types::evaluator::cast::{to_char, to_varbinary, to_varchar}; use crate::types::evaluator::DataValue; use crate::types::value::Utf8Type; use crate::types::CharLengthUnits; @@ -50,53 +50,58 @@ utf8_binary!(utf8_lt_binary_eval, |v1: &String, v2: &String| { }); crate::define_cast_evaluator!(utf8_to_boolean_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Boolean(bool::from_str(value)?)) + Ok(DataValue::Boolean(bool::from_str(&value)?)) } ); crate::define_cast_evaluator!(utf8_to_tinyint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Int8(i8::from_str(value)?)) + Ok(DataValue::Int8(i8::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_utinyint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::UInt8(u8::from_str(value)?)) + Ok(DataValue::UInt8(u8::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_smallint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Int16(i16::from_str(value)?)) + Ok(DataValue::Int16(i16::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_usmallint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::UInt16(u16::from_str(value)?)) + Ok(DataValue::UInt16(u16::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_integer_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Int32(i32::from_str(value)?)) + Ok(DataValue::Int32(i32::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_uinteger_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::UInt32(u32::from_str(value)?)) + Ok(DataValue::UInt32(u32::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_bigint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Int64(i64::from_str(value)?)) + Ok(DataValue::Int64(i64::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_ubigint_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::UInt64(u64::from_str(value)?)) + Ok(DataValue::UInt64(u64::from_str(&value)?)) }); crate::define_cast_evaluator!(utf8_to_float_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Float32(OrderedFloat(f32::from_str(value)?))) + Ok(DataValue::Float32(OrderedFloat(f32::from_str(&value)?))) }); crate::define_cast_evaluator!(utf8_to_double_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Float64(OrderedFloat(f64::from_str(value)?))) + Ok(DataValue::Float64(OrderedFloat(f64::from_str(&value)?))) }); crate::define_cast_evaluator!( utf8_to_char_cast_eval { len: u32, unit: CharLengthUnits }, - DataValue::Utf8 { value, .. } => |this| to_char(value.clone(), this.len, this.unit) + DataValue::Utf8 { value, .. } => |this| to_char(value, this.len, this.unit) ); crate::define_cast_evaluator!( utf8_to_varchar_cast_eval { len: Option, unit: CharLengthUnits }, - DataValue::Utf8 { value, .. } => |this| to_varchar(value.clone(), this.len, this.unit) + DataValue::Utf8 { value, .. } => |this| to_varchar(value, this.len, this.unit) ); +crate::define_cast_evaluator!( + utf8_to_varbinary_cast_eval { len: Option }, + DataValue::Utf8 { value, .. } => |this| to_varbinary(value.into_bytes(), this.len) +); + #[cfg(feature = "time")] mod chrono_cast { use super::DataValue; @@ -106,14 +111,14 @@ mod chrono_cast { crate::define_cast_evaluator!(utf8_to_date_cast_eval, DataValue::Utf8 { value, .. } => { Ok(DataValue::Date32( - NaiveDate::parse_from_str(value, crate::types::value::DATE_FMT)?.num_days_from_ce(), + NaiveDate::parse_from_str(&value, crate::types::value::DATE_FMT)?.num_days_from_ce(), )) }); crate::define_cast_evaluator!(utf8_to_datetime_cast_eval, DataValue::Utf8 { value, .. } => { - let value = NaiveDateTime::parse_from_str(value, crate::types::value::DATE_TIME_FMT) + let value = NaiveDateTime::parse_from_str(&value, crate::types::value::DATE_TIME_FMT) .or_else(|_| { - NaiveDate::parse_from_str(value, crate::types::value::DATE_FMT) + NaiveDate::parse_from_str(&value, crate::types::value::DATE_FMT) .map(|date| date.and_hms_opt(0, 0, 0).unwrap()) })? .and_utc() @@ -135,11 +140,11 @@ mod chrono_cast { }; let (value, nano) = match precision { 0 => ( - NaiveTime::parse_from_str(value, fmt) + NaiveTime::parse_from_str(&value, fmt) .map(|time| time.num_seconds_from_midnight())?, 0, ), - _ => NaiveTime::parse_from_str(value, fmt) + _ => NaiveTime::parse_from_str(&value, fmt) .map(|time| (time.num_seconds_from_midnight(), time.nanosecond()))?, }; @@ -161,15 +166,10 @@ mod chrono_cast { (3 | 6 | 9, false) => crate::types::value::TIME_STAMP_FMT_WITHOUT_ZONE, _ => crate::types::value::TIME_STAMP_FMT_WITH_ZONE, }; - let complete_value = if this.zone { - if value.contains("+") { - value.clone() - } else { - format!("{value}+00:00") - } - } else { - value.clone() - }; + let mut complete_value = value; + if this.zone && !complete_value.contains('+') { + complete_value.push_str("+00:00"); + } if !this.zone { let value = NaiveDateTime::parse_from_str(&complete_value, fmt)?.and_utc(); @@ -206,7 +206,7 @@ mod chrono_cast { pub use chrono_cast::*; #[cfg(feature = "decimal")] crate::define_cast_evaluator!(utf8_to_decimal_cast_eval, DataValue::Utf8 { value, .. } => { - Ok(DataValue::Decimal(Decimal::from_str(value)?)) + Ok(DataValue::Decimal(Decimal::from_str(&value)?)) }); utf8_binary!(utf8_lt_eq_binary_eval, |v1: &String, v2: &String| { DataValue::Boolean(v1 <= v2) @@ -217,16 +217,23 @@ utf8_binary!(utf8_eq_binary_eval, |v1: &String, v2: &String| { utf8_binary!(utf8_not_eq_binary_eval, |v1: &String, v2: &String| { DataValue::Boolean(v1 != v2) }); -utf8_binary!( - utf8_string_concat_binary_eval, - |v1: &String, v2: &String| { - DataValue::Utf8 { - value: v1.clone() + v2, - ty: Utf8Type::Variable(None), - unit: CharLengthUnits::Characters, +pub fn utf8_string_concat_binary_eval( + left: DataValue, + right: &DataValue, +) -> Result { + Ok(match (left, right) { + (DataValue::Utf8 { mut value, .. }, DataValue::Utf8 { value: right, .. }) => { + value.push_str(right); + DataValue::Utf8 { + value, + ty: Utf8Type::Variable(None), + unit: CharLengthUnits::Characters, + } } - } -); + (DataValue::Null, _) | (_, DataValue::Null) => DataValue::Null, + _ => unsafe { hint::unreachable_unchecked() }, + }) +} pub fn utf8_like_binary_eval( escape_char: Option, left: &DataValue, @@ -348,7 +355,11 @@ fn next_char_at(input: &str, index: usize) -> Option<(char, usize)> { #[cfg(all(test, not(target_arch = "wasm32")))] mod test { use super::*; + use crate::expression::BinaryOperator; + use crate::types::evaluator::binary_create; + use crate::types::LogicalType; use chrono::{Datelike, Timelike}; + use std::borrow::Cow; fn utf8(value: &str) -> DataValue { DataValue::Utf8 { @@ -364,6 +375,48 @@ mod test { #[test] fn test_utf8_binary_evaluators() { + let ty = LogicalType::Varchar(None, CharLengthUnits::Characters); + let concat = binary_create(Cow::Owned(ty.clone()), BinaryOperator::StringConcat).unwrap(); + let mut text = String::with_capacity(32); + text.push_str("ab"); + let buffer = text.as_ptr(); + let result = concat + .binary_eval(Cow::Owned(DataValue::from(text)), &utf8("cd")) + .unwrap(); + let result = concat.binary_eval(Cow::Owned(result), &utf8("中")).unwrap(); + let DataValue::Utf8 { value, .. } = result else { + unreachable!() + }; + assert_eq!(value, "abcd中"); + assert_eq!(value.as_ptr(), buffer); + + let left = utf8("ab"); + assert_eq!( + concat + .binary_eval(Cow::Borrowed(&left), &utf8("cd")) + .unwrap(), + utf8("abcd") + ); + assert_eq!(left.utf8(), Some("ab")); + assert_eq!( + concat + .binary_eval(Cow::Borrowed(&left), &DataValue::Null) + .unwrap(), + DataValue::Null + ); + assert_eq!( + concat + .binary_eval(Cow::Owned(DataValue::Null), &left) + .unwrap(), + DataValue::Null + ); + assert_eq!( + binary_create(Cow::Owned(ty), BinaryOperator::Eq) + .unwrap() + .binary_eval(Cow::Owned(left.clone()), &left) + .unwrap(), + DataValue::Boolean(true) + ); assert_eq!( utf8_gt_binary_eval(&utf8("b"), &utf8("a")).unwrap(), DataValue::Boolean(true) @@ -389,7 +442,7 @@ mod test { DataValue::Boolean(true) ); assert_eq!( - utf8_string_concat_binary_eval(&utf8("ab"), &utf8("cd")).unwrap(), + utf8_string_concat_binary_eval(utf8("ab"), &utf8("cd")).unwrap(), utf8("abcd") ); assert_eq!( @@ -467,51 +520,51 @@ mod test { #[test] fn test_utf8_cast_evaluators() { assert_eq!( - utf8_to_boolean_cast_eval(&utf8("true")).unwrap(), + utf8_to_boolean_cast_eval(utf8("true")).unwrap(), DataValue::Boolean(true) ); assert_eq!( - utf8_to_tinyint_cast_eval(&utf8("1")).unwrap(), + utf8_to_tinyint_cast_eval(utf8("1")).unwrap(), DataValue::Int8(1) ); assert_eq!( - utf8_to_utinyint_cast_eval(&utf8("1")).unwrap(), + utf8_to_utinyint_cast_eval(utf8("1")).unwrap(), DataValue::UInt8(1) ); assert_eq!( - utf8_to_smallint_cast_eval(&utf8("1")).unwrap(), + utf8_to_smallint_cast_eval(utf8("1")).unwrap(), DataValue::Int16(1) ); assert_eq!( - utf8_to_usmallint_cast_eval(&utf8("1")).unwrap(), + utf8_to_usmallint_cast_eval(utf8("1")).unwrap(), DataValue::UInt16(1) ); assert_eq!( - utf8_to_integer_cast_eval(&utf8("1")).unwrap(), + utf8_to_integer_cast_eval(utf8("1")).unwrap(), DataValue::Int32(1) ); assert_eq!( - utf8_to_uinteger_cast_eval(&utf8("1")).unwrap(), + utf8_to_uinteger_cast_eval(utf8("1")).unwrap(), DataValue::UInt32(1) ); assert_eq!( - utf8_to_bigint_cast_eval(&utf8("1")).unwrap(), + utf8_to_bigint_cast_eval(utf8("1")).unwrap(), DataValue::Int64(1) ); assert_eq!( - utf8_to_ubigint_cast_eval(&utf8("1")).unwrap(), + utf8_to_ubigint_cast_eval(utf8("1")).unwrap(), DataValue::UInt64(1) ); assert_eq!( - utf8_to_float_cast_eval(&utf8("1.5")).unwrap(), + utf8_to_float_cast_eval(utf8("1.5")).unwrap(), DataValue::Float32(OrderedFloat(1.5)) ); assert_eq!( - utf8_to_double_cast_eval(&utf8("1.5")).unwrap(), + utf8_to_double_cast_eval(utf8("1.5")).unwrap(), DataValue::Float64(OrderedFloat(1.5)) ); assert_eq!( - utf8_to_char_cast_eval(2, CharLengthUnits::Characters, &utf8("ab")).unwrap(), + utf8_to_char_cast_eval(2, CharLengthUnits::Characters, utf8("ab")).unwrap(), DataValue::Utf8 { value: "ab".to_string(), ty: Utf8Type::Fixed(2), @@ -519,7 +572,7 @@ mod test { } ); assert_eq!( - utf8_to_varchar_cast_eval(Some(2), CharLengthUnits::Characters, &utf8("ab")).unwrap(), + utf8_to_varchar_cast_eval(Some(2), CharLengthUnits::Characters, utf8("ab")).unwrap(), DataValue::Utf8 { value: "ab".to_string(), ty: Utf8Type::Variable(Some(2)), @@ -527,7 +580,7 @@ mod test { } ); assert_eq!( - utf8_to_date_cast_eval(&utf8("2024-01-02")).unwrap(), + utf8_to_date_cast_eval(utf8("2024-01-02")).unwrap(), DataValue::Date32( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -535,7 +588,7 @@ mod test { ) ); assert_eq!( - utf8_to_datetime_cast_eval(&utf8("2024-01-02 03:04:05")).unwrap(), + utf8_to_datetime_cast_eval(utf8("2024-01-02 03:04:05")).unwrap(), DataValue::Date64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -546,11 +599,11 @@ mod test { ) ); assert_eq!( - utf8_to_time_cast_eval(Some(0), &utf8("03:04:05")).unwrap(), + utf8_to_time_cast_eval(Some(0), utf8("03:04:05")).unwrap(), DataValue::Time32(DataValue::pack_time(3 * 3600 + 4 * 60 + 5, 0, 0), 0) ); assert_eq!( - utf8_to_time_cast_eval(Some(3), &utf8("03:04:05.123")).unwrap(), + utf8_to_time_cast_eval(Some(3), utf8("03:04:05.123")).unwrap(), { let time = chrono::NaiveTime::parse_from_str( "03:04:05.123", @@ -564,7 +617,7 @@ mod test { } ); assert_eq!( - utf8_to_timestamp_cast_eval(Some(3), false, &utf8("2024-01-02 03:04:05.123")).unwrap(), + utf8_to_timestamp_cast_eval(Some(3), false, utf8("2024-01-02 03:04:05.123")).unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -577,7 +630,7 @@ mod test { ) ); assert_eq!( - utf8_to_timestamp_cast_eval(Some(0), true, &utf8("2024-01-02 03:04:05+00:00")).unwrap(), + utf8_to_timestamp_cast_eval(Some(0), true, utf8("2024-01-02 03:04:05+00:00")).unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -590,7 +643,7 @@ mod test { ) ); assert_eq!( - utf8_to_timestamp_cast_eval(Some(0), true, &utf8("2024-01-02 03:04:05")).unwrap(), + utf8_to_timestamp_cast_eval(Some(0), true, utf8("2024-01-02 03:04:05")).unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -603,7 +656,7 @@ mod test { ) ); assert_eq!( - utf8_to_timestamp_cast_eval(Some(6), false, &utf8("2024-01-02 03:04:05.123456")) + utf8_to_timestamp_cast_eval(Some(6), false, utf8("2024-01-02 03:04:05.123456")) .unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) @@ -617,12 +670,8 @@ mod test { ) ); assert_eq!( - utf8_to_timestamp_cast_eval( - Some(9), - true, - &utf8("2024-01-02 03:04:05.123456789+00:00") - ) - .unwrap(), + utf8_to_timestamp_cast_eval(Some(9), true, utf8("2024-01-02 03:04:05.123456789+00:00")) + .unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() @@ -636,7 +685,7 @@ mod test { ) ); assert_eq!( - utf8_to_timestamp_cast_eval(Some(9), false, &utf8("2024-01-02 03:04:05.123456789")) + utf8_to_timestamp_cast_eval(Some(9), false, utf8("2024-01-02 03:04:05.123456789")) .unwrap(), DataValue::Time64( chrono::NaiveDate::from_ymd_opt(2024, 1, 2) @@ -651,17 +700,17 @@ mod test { ) ); assert_eq!( - utf8_to_decimal_cast_eval(&utf8("12.34")).unwrap(), + utf8_to_decimal_cast_eval(utf8("12.34")).unwrap(), DataValue::Decimal(Decimal::from_str("12.34").unwrap()) ); assert_panics(|| { - let _ = utf8_to_timestamp_cast_eval(Some(4), false, &utf8("2024-01-02 03:04:05.1234")) + let _ = utf8_to_timestamp_cast_eval(Some(4), false, utf8("2024-01-02 03:04:05.1234")) .unwrap(); }); assert_panics(|| { let _ = - utf8_to_timestamp_cast_eval(Some(4), true, &utf8("2024-01-02 03:04:05.1234+00:00")) + utf8_to_timestamp_cast_eval(Some(4), true, utf8("2024-01-02 03:04:05.1234+00:00")) .unwrap(); }); } diff --git a/src/types/mod.rs b/src/types/mod.rs index 78fcdf4b..9f6439d0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -261,7 +261,9 @@ impl LogicalType { return Ok(Cow::Owned(LogicalType::DateTime)); } } - if let (LogicalType::Varbinary(_), LogicalType::Varbinary(_)) = (left, right) { + if matches!(left, LogicalType::Varbinary(_)) && !matches!(right, LogicalType::Tuple(_)) + || matches!(right, LogicalType::Varbinary(_)) && !matches!(left, LogicalType::Tuple(_)) + { return Ok(Cow::Owned(LogicalType::Varbinary(None))); } if (left.is_numeric() && matches!(right, LogicalType::Char(..) | LogicalType::Varchar(..))) diff --git a/src/types/value.rs b/src/types/value.rs index b23e37ee..a1e8fe0f 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -16,7 +16,7 @@ use super::LogicalType; use crate::errors::DatabaseError; use crate::iter_ext::Itertools; use crate::storage::table_codec::{BumpBytes, NOTNULL_TAG, NULL_TAG}; -use crate::types::evaluator::cast::{cast_create, to_char, to_varchar}; +use crate::types::evaluator::cast::cast_create; use crate::types::CharLengthUnits; #[cfg(feature = "time")] use chrono::{ @@ -1502,20 +1502,9 @@ impl DataValue { Ok(DataValue::Parameter { id, ty: ty.clone() }) } (DataValue::Null, _) => Ok(DataValue::Null), - (DataValue::Binary { value, .. }, LogicalType::Varbinary(len)) => { - let result = DataValue::Binary { value, len: *len }; - result.check_len(to)?; - Ok(result) - } - (DataValue::Utf8 { value, .. }, LogicalType::Char(len, unit)) => { - to_char(value, *len, *unit) - } - (DataValue::Utf8 { value, .. }, LogicalType::Varchar(len, unit)) => { - to_varchar(value, *len, *unit) - } (value, _) => { let evaluator = cast_create(&from, to)?; - evaluator.eval(&value) + evaluator.eval(value) } } } @@ -2009,6 +1998,16 @@ mod test { } previous = Some(encoded); } + let text = "中\0".to_string(); + let buffer = text.as_ptr(); + let binary = DataValue::from(text).cast(&LogicalType::Varbinary(Some(4)))?; + assert_eq!(binary.binary(), Some("中\0".as_bytes())); + assert_eq!(binary.binary().unwrap().as_ptr(), buffer); + assert_eq!(binary.logical_type(), LogicalType::Varbinary(Some(4))); + assert!(matches!( + DataValue::from("中".to_string()).cast(&LogicalType::Varbinary(Some(2))), + Err(DatabaseError::TooLong) + )); let bytes = DataValue::from(vec![0, 255]); assert_eq!(bytes.binary(), Some([0, 255].as_slice())); assert_eq!(values[0].binary(), Some([].as_slice())); diff --git a/tests/macros-test/src/main.rs b/tests/macros-test/src/main.rs index fb508310..ec399e45 100644 --- a/tests/macros-test/src/main.rs +++ b/tests/macros-test/src/main.rs @@ -34,6 +34,7 @@ mod test { use kite_sql::types::{CharLengthUnits, LogicalType}; use kite_sql::{from_tuple, scala_function, table_function, Model, Projection}; use rust_decimal::Decimal; + use std::borrow::Cow; use tempfile::TempDir; fn build_tuple(arena: &mut impl MetaArena) -> (Tuple, Schema) { @@ -3027,7 +3028,7 @@ mod test { } scala_function!(MyScalaFunction::SUM(LogicalType::Integer, LogicalType::Integer) -> LogicalType::Integer => (|v1: DataValue, v2: DataValue| { - binary_create(std::borrow::Cow::Owned(LogicalType::Integer), BinaryOperator::Plus)?.binary_eval(&v1, &v2) + binary_create(Cow::Owned(LogicalType::Integer), BinaryOperator::Plus)?.binary_eval(Cow::Owned(v1), &v2) })); scala_function!(MyOrmFunction::ADD_ONE(LogicalType::Integer) -> LogicalType::Integer => (|v1: DataValue| { diff --git a/tests/slt/binary.slt b/tests/slt/binary.slt index 50ec7c3d..1dbae1e8 100644 --- a/tests/slt/binary.slt +++ b/tests/slt/binary.slt @@ -112,5 +112,69 @@ X'FF' statement ok drop table binary_pk; +# Scalar conversions use their text representation, not storage/index encoding. +query TTTT +select cast(123 as varbinary),cast(-12 as varbinary(3)),cast(true as varbinary),cast('中' as varbinary(3)); +---- +X'313233' X'2D3132' X'74727565' X'E4B8AD' + +query TTT +select cast(cast('2024-01-02' as date) as varbinary),cast(cast('03:04:05' as time) as varbinary),cast(cast('1.25' as decimal(4,2)) as varbinary); +---- +X'323032342D30312D3032' X'30333A30343A3035' X'312E3235' + +query TTTT +select x'313233'=cast(123 as bigint),cast(123 as bigint)=x'313233',x'313233'='123','123'=x'313233'; +---- +true true true true + +query TTTT +select x'313233' in (cast(123 as bigint),null),123 in (x'313233',null),x'30' not in (123,null),x'313233' between 100 and 200; +---- +true true NULL true + +query TT +select x'303132' in (select cast(id as bigint) from binary_values where false),x'303132' not in (select cast(id as bigint) from binary_values where false); +---- +false true + +query TT +select x'31' in (select cast(id as bigint) from binary_values),cast(1 as bigint) in (select cast(id as varbinary) from binary_values); +---- +true true + +statement error +select cast('中' as varbinary(2)); + +statement error +select cast(123 as varbinary(2)); + +statement error +select x'31'+1; + +statement ok +create table binary_casts (id int primary key,b varbinary(4) unique); + +statement ok +insert into binary_casts values (1,123),(2,'abc'),(3,null); + +query IT +select id,b from binary_casts order by id; +---- +1 X'313233' +2 X'616263' +3 NULL + +statement error +insert into binary_casts values (4,'123'); + +query I +select id from binary_casts where b=x'313233'; +---- +1 + +statement ok +drop table binary_casts; + statement ok drop table binary_values; From 3e7bc18425983887b671e7aa7c075306feeccc39 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 07/10] fix: map REPLACE inserts to overwrite semantics --- src/binder/parser.rs | 13 +++++++++---- tests/slt/insert.slt | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index ed0a9895..05535f00 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -48,8 +48,9 @@ pub(super) use sqlparser::ast::{ FunctionArguments, GroupByExpr, Ident, IndexColumn, IndexOption, IndexType, Join, JoinConstraint, JoinOperator, LimitClause, ObjectName, ObjectNamePart, ObjectType, OrderByExpr, OrderByKind, Query, Select, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, - SetOperator, SetQuantifier, Spanned, TableAlias, TableConstraint, TableFactor, TableObject, - TableWithJoins, TypedString, UnaryOperator, Value, ValueWithSpan, WindowType, + SetOperator, SetQuantifier, Spanned, SqliteOnConflict, TableAlias, TableConstraint, + TableFactor, TableObject, TableWithJoins, TypedString, UnaryOperator, Value, ValueWithSpan, + WindowType, }; #[cfg(feature = "copy")] pub(super) use sqlparser::ast::{CopyOption, CopySource, CopyTarget}; @@ -1023,9 +1024,13 @@ where table, columns, overwrite, + or, + replace_into, source, .. } = insert; + let is_overwrite = + *overwrite || *replace_into || matches!(or, Some(SqliteOnConflict::Replace)); let table_name = match table { TableObject::TableName(table_name) => table_name.clone(), TableObject::TableFunction(_) => { @@ -1039,9 +1044,9 @@ where DatabaseError::UnsupportedStmt("insert without source is not supported".to_string()) })?; if let SetExpr::Values(values) = source.body.as_ref() { - self.insert_values(table_name, columns, &values.rows, *overwrite, false) + self.insert_values(table_name, columns, &values.rows, is_overwrite, false) } else { - self.insert_query(table_name, columns, source, *overwrite) + self.insert_query(table_name, columns, source, is_overwrite) } } diff --git a/tests/slt/insert.slt b/tests/slt/insert.slt index ca48be81..4037852f 100644 --- a/tests/slt/insert.slt +++ b/tests/slt/insert.slt @@ -57,6 +57,50 @@ select * from t statement ok drop table t; +# REPLACE spellings share INSERT OVERWRITE semantics for VALUES and queries. +statement ok +create table t_replace(id int primary key, v varchar(16) default 'default') + +statement ok +insert into t_replace values (1, 'original') + +statement ok +insert or replace into t_replace values (1, 'or replace'), (2, 'new') + +query IT +select * from t_replace order by id +---- +1 or replace +2 new + +statement ok +replace into t_replace values (1, 'replace') + +statement ok +insert or replace into t_replace select 2, 'query replace' + +statement ok +replace into t_replace select 3, 'query new' + +statement ok +replace into t_replace(id) values (1) + +statement error +insert or replace into t_replace values (2, 'insert or replace') + +statement error +insert into t_replace values (2, 'duplicate') + +query IT +select * from t_replace order by id +---- +1 default +2 query replace +3 query new + +statement ok +drop table t_replace + statement ok create table t1(id int primary key, v1 bigint default 233) From 3d1b5de773c2e9d18c896bb3f23054aa30ec6572 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:39:04 +0800 Subject: [PATCH 08/10] fix: honor AVG DISTINCT and reject DISTINCT wildcard arguments - Deduplicate non-null AVG inputs only when DISTINCT is requested and preserve DISTINCT in aggregate display. - Reject DISTINCT wildcard arguments before wildcard lowering while keeping COUNT(*) and COUNT(DISTINCT '*') valid. - Cover simple, grouped, empty, and cached DISTINCT aggregate results. --- src/binder/parser.rs | 8 ++++++ src/execution/dql/aggregate/avg.rs | 43 +++++++++++++++++++++++++++--- src/execution/dql/aggregate/mod.rs | 2 +- src/expression/agg.rs | 2 +- tests/slt/aggregation.slt | 37 +++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 05535f00..71357ab9 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2671,6 +2671,14 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' args.push(arena.alloc_expression(expr)) } FunctionArgExpr::Wildcard => { + if is_distinct { + return Err(attach_span_if_absent( + DatabaseError::UnsupportedStmt( + "DISTINCT cannot be used with *".to_string(), + ), + func_span, + )); + } args.push(arena.alloc_expression(Self::wildcard_expr())) } expr => { diff --git a/src/execution/dql/aggregate/avg.rs b/src/execution/dql/aggregate/avg.rs index a1873aa5..b9afbac2 100644 --- a/src/execution/dql/aggregate/avg.rs +++ b/src/execution/dql/aggregate/avg.rs @@ -20,16 +20,19 @@ use crate::types::evaluator::binary_create; use crate::types::value::DataValue; use crate::types::LogicalType; use std::borrow::Cow; +use std::collections::HashSet; pub struct AvgAccumulator { + distinct_values: Option>, inner: Option, count: usize, result: DataValue, } impl AvgAccumulator { - pub fn new() -> Self { + pub fn new(distinct: bool) -> Self { Self { + distinct_values: distinct.then(HashSet::new), inner: None, count: 0, result: DataValue::Null, @@ -40,6 +43,11 @@ impl AvgAccumulator { impl Accumulator for AvgAccumulator { fn update_value(&mut self, value: &DataValue) -> Result<(), DatabaseError> { if !value.is_null() { + if let Some(values) = &mut self.distinct_values { + if !values.insert(value.clone()) { + return Ok(()); + } + } let acc = if let Some(ref mut inner) = self.inner { inner } else { @@ -88,7 +96,7 @@ mod tests { #[test] fn invalidate_cached_result_after_update() -> Result<(), DatabaseError> { - let mut accumulator = AvgAccumulator::new(); + let mut accumulator = AvgAccumulator::new(false); accumulator.update_value(&DataValue::Int32(2))?; accumulator.evaluate()?; let first = accumulator.result().clone(); @@ -104,14 +112,41 @@ mod tests { Ok(()) } + #[test] + fn distinct_values_ignore_nulls_and_update_cached_result() -> Result<(), DatabaseError> { + let mut accumulator = AvgAccumulator::new(true); + accumulator.update_value(&DataValue::Null)?; + accumulator.evaluate()?; + assert_eq!(accumulator.result(), &DataValue::Null); + + for value in [1, 0, 2, 2] { + accumulator.update_value(&DataValue::Int32(value))?; + } + accumulator.evaluate()?; + assert_eq!(accumulator.result(), &DataValue::Float64(1.0.into())); + + accumulator.update_value(&DataValue::Int32(2))?; + accumulator.update_value(&DataValue::Null)?; + accumulator.evaluate()?; + assert_eq!(accumulator.result(), &DataValue::Float64(1.0.into())); + + accumulator.update_value(&DataValue::Int32(3))?; + accumulator.evaluate()?; + assert_eq!( + Box::new(accumulator).result_owned(), + DataValue::Float64(1.5.into()) + ); + Ok(()) + } + #[test] fn empty_and_unsigned_results() -> Result<(), DatabaseError> { - let mut empty = AvgAccumulator::new(); + let mut empty = AvgAccumulator::new(false); empty.evaluate()?; assert_eq!(empty.result(), &DataValue::Null); assert_eq!(Box::new(empty).result_owned(), DataValue::Null); - let mut accumulator = AvgAccumulator::new(); + let mut accumulator = AvgAccumulator::new(false); accumulator.update_value(&DataValue::UInt32(2))?; accumulator.update_value(&DataValue::UInt32(4))?; accumulator.evaluate()?; diff --git a/src/execution/dql/aggregate/mod.rs b/src/execution/dql/aggregate/mod.rs index 2735f699..87c685b5 100644 --- a/src/execution/dql/aggregate/mod.rs +++ b/src/execution/dql/aggregate/mod.rs @@ -64,7 +64,7 @@ pub(crate) fn create_accumulator( (AggKind::Sum, true) => Box::new(DistinctSumAccumulator::new(ty)?), (AggKind::Min, _) => Box::new(MinMaxAccumulator::new(false)), (AggKind::Max, _) => Box::new(MinMaxAccumulator::new(true)), - (AggKind::Avg, _) => Box::new(AvgAccumulator::new()), + (AggKind::Avg, distinct) => Box::new(AvgAccumulator::new(distinct)), }) } diff --git a/src/expression/agg.rs b/src/expression/agg.rs index 1761d03b..13198aef 100644 --- a/src/expression/agg.rs +++ b/src/expression/agg.rs @@ -47,7 +47,7 @@ impl AggKind { pub fn allow_distinct(&self) -> bool { match self { - AggKind::Avg => false, + AggKind::Avg => true, AggKind::Max => false, AggKind::Min => false, AggKind::Sum => true, diff --git a/tests/slt/aggregation.slt b/tests/slt/aggregation.slt index 7744ad70..d4eb10f1 100644 --- a/tests/slt/aggregation.slt +++ b/tests/slt/aggregation.slt @@ -141,3 +141,40 @@ select a from (select a,sum(a) as total from sn group by a) q; statement ok drop table sn + +# AVG(DISTINCT) deduplicates non-NULL values in both simple and grouped aggregates. +statement ok +create table avg_distinct_test(id int primary key, g int, v int); + +statement ok +insert into avg_distinct_test values + (1, 1, 1), (2, 1, 0), (3, 1, null), + (4, 1, 2), (5, 1, 2), (6, 2, null); + +query RR +select avg(v), avg(distinct v) from avg_distinct_test; +---- +1.25 1.0 + +query IRR +select g, avg(v), avg(distinct v) from avg_distinct_test group by g order by g; +---- +1 1.25 1.0 +2 NULL NULL + +query R +select avg(distinct v) from avg_distinct_test where false; +---- +NULL + +# DISTINCT rejects a wildcard, not an ordinary '*' string literal. +statement error DISTINCT cannot be used with * +select count(distinct *) from avg_distinct_test where false; + +query III +select count(*), count(distinct '*'), count(distinct v) from avg_distinct_test; +---- +6 1 3 + +statement ok +drop table avg_distinct_test; From a4f4c18d449963c110848df605eb89440c4d5c7b Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 11:59:05 +0800 Subject: [PATCH 09/10] test: match the stable error prefix for out-of-range ALTER COLUMN casts --- tests/slt/change_column.slt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/slt/change_column.slt b/tests/slt/change_column.slt index 7ba12ce4..820c3883 100644 --- a/tests/slt/change_column.slt +++ b/tests/slt/change_column.slt @@ -229,7 +229,7 @@ select id,v from alter_atomic order by id; statement ok drop index alter_atomic.uk_v_index; -statement error out of range integral type conversion +statement error try from int: alter table alter_atomic alter column v type int; query II From e3dc1d3a2489ac0d8a0d619918208a411bd35d7d Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:18:26 +0800 Subject: [PATCH 10/10] fix: return an error for SUM/AVG over non-numeric types instead of asserting --- src/execution/dql/aggregate/sum.rs | 7 +++---- tests/slt/aggregation.slt | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/execution/dql/aggregate/sum.rs b/src/execution/dql/aggregate/sum.rs index 33150e7a..1e220167 100644 --- a/src/execution/dql/aggregate/sum.rs +++ b/src/execution/dql/aggregate/sum.rs @@ -28,10 +28,9 @@ pub struct SumAccumulator { impl SumAccumulator { pub fn new(ty: Cow<'_, LogicalType>) -> Result { - // `sum(null)` binds with the type of the NULL literal; it never sees a - // non-NULL value, so the result is NULL like in release builds. - debug_assert!(ty.is_numeric() || matches!(ty.as_ref(), LogicalType::SqlNull)); - + // The binder does not restrict SUM/AVG argument types, so `binary_create` + // decides: types without `+` (e.g. VARCHAR) return an error instead of + // panicking, and `sum(null)` uses the SqlNull evaluator. Ok(Self { result: DataValue::Null, evaluator: binary_create(ty, BinaryOperator::Plus)?, diff --git a/tests/slt/aggregation.slt b/tests/slt/aggregation.slt index d4eb10f1..811b89c6 100644 --- a/tests/slt/aggregation.slt +++ b/tests/slt/aggregation.slt @@ -98,6 +98,28 @@ select (select sum(null) from sn) from sn where id = 1; ---- NULL +# SUM/AVG over strings is an error, not an assertion failure. +statement ok +create table ss(id int primary key, v varchar); + +statement ok +insert into ss values (1, '10'), (2, 'x'); + +statement error +select sum(v) from ss; + +statement error +select sum(distinct v) from ss; + +statement error +select avg(v) from ss; + +statement error +select sum('a') from ss; + +statement ok +drop table ss; + # Aggregates, including SELECT aliases containing aggregates, cannot run in WHERE. statement error agg miss: aggregate functions are not allowed in WHERE select count(*) as n from sn where n is not null;