diff --git a/Cargo.lock b/Cargo.lock index 16fee0c0..76616c37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,7 +373,7 @@ dependencies = [ [[package]] name = "asap-aware-mapping" version = "0.1.0" -source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=7261dab97f8e43070e6b9e33c85493b12b9a351c#7261dab97f8e43070e6b9e33c85493b12b9a351c" +source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=b2b05628dd9a58db555ba309bf7201e10513f4d2#b2b05628dd9a58db555ba309bf7201e10513f4d2" dependencies = [ "asap-types", "serde", @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "asap-frontend-promql" version = "0.1.0" -source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=7261dab97f8e43070e6b9e33c85493b12b9a351c#7261dab97f8e43070e6b9e33c85493b12b9a351c" +source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=b2b05628dd9a58db555ba309bf7201e10513f4d2#b2b05628dd9a58db555ba309bf7201e10513f4d2" dependencies = [ "asap-types", "promql-parser", @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "asap-frontend-sql" version = "0.1.0" -source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=7261dab97f8e43070e6b9e33c85493b12b9a351c#7261dab97f8e43070e6b9e33c85493b12b9a351c" +source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=b2b05628dd9a58db555ba309bf7201e10513f4d2#b2b05628dd9a58db555ba309bf7201e10513f4d2" dependencies = [ "asap-sql-function-catalog", "asap-types", @@ -415,12 +415,12 @@ dependencies = [ [[package]] name = "asap-sql-function-catalog" version = "0.1.0" -source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=7261dab97f8e43070e6b9e33c85493b12b9a351c#7261dab97f8e43070e6b9e33c85493b12b9a351c" +source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=b2b05628dd9a58db555ba309bf7201e10513f4d2#b2b05628dd9a58db555ba309bf7201e10513f4d2" [[package]] name = "asap-types" version = "0.1.0" -source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=7261dab97f8e43070e6b9e33c85493b12b9a351c#7261dab97f8e43070e6b9e33c85493b12b9a351c" +source = "git+https://github.com/ProjectASAP/ASAPPlanner?rev=b2b05628dd9a58db555ba309bf7201e10513f4d2#b2b05628dd9a58db555ba309bf7201e10513f4d2" dependencies = [ "serde", "serde_json", diff --git a/control_plane/Cargo.toml b/control_plane/Cargo.toml index 537c5e96..2aec4683 100644 --- a/control_plane/Cargo.toml +++ b/control_plane/Cargo.toml @@ -76,8 +76,8 @@ asap_types.workspace = true # scaffolding, unaware that `data_plane`'s `summary_executor.rs` in *this* # repo is a real one. Vendored locally instead of chased upstream -- see # `data_plane/src/query_engines/asap_query_engine/summary_exec.rs`. -planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } -asap-aware-mapping = { git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } +planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } +asap-aware-mapping = { git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } # L1 adoption (design-target-architecture.md Part B): the PromQL front # end itself, replacing control_plane's own query_parser/promql.rs. @@ -85,8 +85,8 @@ asap-aware-mapping = { git = "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/ProjectASAP/ASAPPlanner", rev = # `planner-types`/`asap-aware-mapping` above -- these three MUST move # together (two revs of the same upstream repo's types in one workspace # resolve to distinct Rust types that won't unify). -asap-frontend-promql = { git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } -asap-frontend-sql = { git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } +asap-frontend-promql = { git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } +asap-frontend-sql = { git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } [dev-dependencies] tokio = { version = "1", features = ["full", "test-util"] } diff --git a/crates/asap_types/Cargo.toml b/crates/asap_types/Cargo.toml index 21d80432..a50afa72 100644 --- a/crates/asap_types/Cargo.toml +++ b/crates/asap_types/Cargo.toml @@ -33,4 +33,4 @@ sha2 = "0.10" # exactly (`control_plane/Cargo.toml`) -- two different revs of the same # git dependency in one workspace resolve to two distinct Rust types that # won't unify. -planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } +planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } diff --git a/data_plane/Cargo.toml b/data_plane/Cargo.toml index 14c29ec9..6bf88325 100644 --- a/data_plane/Cargo.toml +++ b/data_plane/Cargo.toml @@ -39,8 +39,8 @@ sha2 = "0.10" # reduction: Reduction, .. }`) are `pre_asap` types, in the same crate now # (not a separate `asap-ir` import). Query serving consumes the compiled # QueryPlan; these types are used at physical-plan compilation boundaries. -planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } -asap-frontend-promql = { git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } +planner-types = { package = "asap-types", git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } +asap-frontend-promql = { git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } # Shared external (workspace) serde.workspace = true @@ -133,7 +133,7 @@ fs2 = "0.4" # none of them. [dev-dependencies] -asap-aware-mapping = { git = "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/ProjectASAP/ASAPPlanner", rev = "7261dab97f8e43070e6b9e33c85493b12b9a351c" } +asap-aware-mapping = { git = "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/ProjectASAP/ASAPPlanner", rev = "b2b05628dd9a58db555ba309bf7201e10513f4d2" } tempfile = "3.20.0" criterion = { version = "0.5", features = ["html_reports"] } tokio-tungstenite = "0.21" diff --git a/data_plane/src/query_engines/asap_clickhouse_query_engine/relational_adapter.rs b/data_plane/src/query_engines/asap_clickhouse_query_engine/relational_adapter.rs index 56cc44f8..d14e5719 100644 --- a/data_plane/src/query_engines/asap_clickhouse_query_engine/relational_adapter.rs +++ b/data_plane/src/query_engines/asap_clickhouse_query_engine/relational_adapter.rs @@ -4,8 +4,8 @@ use std::{cmp::Ordering, collections::BTreeMap, sync::Arc}; use arrow::{ array::{ - ArrayRef, BooleanArray, Float64Array, Int64Array, MapArray, StringArray, StructArray, - TimestampMillisecondArray, + ArrayRef, BooleanArray, Float64Array, Int64Array, MapArray, NullArray, StringArray, + StructArray, TimestampMillisecondArray, }, datatypes::{DataType as ArrowDataType, Field, Schema}, record_batch::RecordBatch, @@ -64,6 +64,11 @@ fn json_cell( )) }; match dtype { + DataType::Null if value.is_null() => Ok(Cell::Null), + DataType::Null => Err(invalid()), + DataType::List { .. } | DataType::Struct { .. } => Err( + ClickHouseRelationalError::Unsupported("collection value transport".into()), + ), DataType::Int64 => value.as_i64().map(Cell::Int64).ok_or_else(invalid), DataType::Float64 => value.as_f64().map(Cell::Float64).ok_or_else(invalid), DataType::Utf8 => value @@ -307,6 +312,8 @@ fn clickhouse_type_matches(actual: Option<&str>, expected: &DataType, nullable: return false; } match expected { + DataType::Null => actual == "Nothing", + DataType::List { .. } | DataType::Struct { .. } => false, DataType::Int64 => actual == "Int64", DataType::Float64 => actual == "Float64", DataType::Utf8 => actual == "String", @@ -346,13 +353,16 @@ impl ClickHouseRelationalAdapter { } _ => None, }; + let mut fields = left.fields.clone(); + fields.extend(right.fields.clone()); + let schema = scalar_schema(&fields); let mut rows = Vec::new(); for left_row in &left.rows { for right_row in &right.rows { let mut joined = Vec::with_capacity(left_row.len() + right_row.len()); joined.extend(left_row.iter().cloned()); joined.extend(right_row.iter().cloned()); - if matches!(eval(&pred.0, &joined)?, Cell::Bool(true)) { + if matches!(eval(&pred.0, &joined, &schema)?, Cell::Bool(true)) { rows.push(joined); } } @@ -369,10 +379,11 @@ impl ClickHouseRelationalAdapter { pred: &planner_types::pre_asap::Predicate, mut input: ClickHouseRelation, ) -> Result { + let schema = scalar_schema(&input.fields); input.rows = input .rows .into_iter() - .filter_map(|row| match eval(&pred.0, &row) { + .filter_map(|row| match eval(&pred.0, &row, &schema) { Ok(Cell::Bool(true)) => Some(Ok(row)), Ok(_) => None, Err(error) => Some(Err(error)), @@ -387,13 +398,14 @@ impl ClickHouseRelationalAdapter { output_schema: &SummarySchema, mut input: ClickHouseRelation, ) -> Result { + let schema = scalar_schema(&input.fields); match operation { ValueOperation::Project { cols, .. } => { let mut rows = Vec::with_capacity(input.rows.len()); for row in &input.rows { rows.push( cols.iter() - .map(|item| eval(&item.expr, row)) + .map(|item| eval(&item.expr, row, &schema)) .collect::, _>>()?, ); } @@ -408,12 +420,16 @@ impl ClickHouseRelationalAdapter { } for row in &input.rows { for key in keys { - eval(&key.expr, row)?; + if contains_nan(&eval(&key.expr, row, &schema)?) { + return Err(ClickHouseRelationalError::Unsupported( + "NaN sort key".into(), + )); + } } } input .rows - .sort_by(|left, right| compare_sort_keys(left, right, keys)); + .sort_by(|left, right| compare_sort_keys(left, right, keys, &schema)); } ValueOperation::Limit { n, offset } => { input.rows = input.rows.into_iter().skip(*offset).take(*n).collect(); @@ -571,7 +587,22 @@ fn row_from_value( .collect() } -fn eval(expr: &QueryExpr, row: &[Cell]) -> Result { +fn scalar_schema(fields: &[(String, DataType, bool)]) -> planner_types::pre_asap::Schema { + planner_types::pre_asap::Schema::new( + fields + .iter() + .map(|(name, dtype, nullable)| { + planner_types::pre_asap::Column::new(name.clone(), dtype.clone(), *nullable) + }) + .collect(), + ) +} + +fn eval( + expr: &QueryExpr, + row: &[Cell], + schema: &planner_types::pre_asap::Schema, +) -> Result { match expr { QueryExpr::Column(index) => { row.get(*index) @@ -589,12 +620,89 @@ fn eval(expr: &QueryExpr, row: &[Cell]) -> Result Cell::Null, }), QueryExpr::Compare { left, op, right } => { - let left = eval(left, row)?; - let right = eval(right, row)?; + let left = eval(left, row, schema)?; + let right = eval(right, row, schema)?; compare(op, left, right) } QueryExpr::Arithmetic { op, left, right } => { - arithmetic(op, eval(left, row)?, eval(right, row)?) + arithmetic(op, eval(left, row, schema)?, eval(right, row, schema)?) + } + QueryExpr::FunctionCall { name, args } => { + use planner_types::pre_asap::scalar_signature::MapScalarFunction; + let function = MapScalarFunction::from_name(name).ok_or_else(|| { + ClickHouseRelationalError::Unsupported(format!("scalar function {name}")) + })?; + expr.scalar_type(schema) + .map_err(|error| ClickHouseRelationalError::Invalid(error.to_string()))?; + let values = args + .iter() + .map(|arg| eval(arg, row, schema)) + .collect::, _>>()?; + match function { + MapScalarFunction::Construct => { + let mut values = values.into_iter(); + let mut entries = Vec::new(); + while let Some(key) = values.next() { + if !matches!(key, Cell::Int64(_) | Cell::Utf8(_) | Cell::Bool(_)) { + return Err(ClickHouseRelationalError::Unsupported( + "map key value type".into(), + )); + } + entries.push(( + key, + values.next().ok_or_else(|| { + ClickHouseRelationalError::Invalid("odd map argument count".into()) + })?, + )); + } + Ok(Cell::Map(entries)) + } + MapScalarFunction::Concat => { + let mut entries = Vec::new(); + for value in values { + let Cell::Map(mut next) = value else { + return Err(ClickHouseRelationalError::Invalid( + "map concat argument".into(), + )); + }; + entries.append(&mut next); + } + Ok(Cell::Map(entries)) + } + MapScalarFunction::Access => { + let [Cell::Map(entries), key] = values.as_slice() else { + return Err(ClickHouseRelationalError::Invalid( + "map access arguments".into(), + )); + }; + if matches!(key, Cell::Null) { + return Ok(Cell::Null); + } + if !matches!(key, Cell::Int64(_) | Cell::Utf8(_) | Cell::Bool(_)) { + return Err(ClickHouseRelationalError::Unsupported( + "map lookup key type".into(), + )); + } + if let Some((_, value)) = entries.iter().find(|(candidate, _)| candidate == key) + { + return Ok(value.clone()); + } + let ( + DataType::Map { + value, + value_nullable, + .. + }, + _, + ) = args[0] + .scalar_type(schema) + .map_err(|error| ClickHouseRelationalError::Invalid(error.to_string()))? + else { + unreachable!() + }; + default_map_value(&value, value_nullable) + } + } } other => Err(ClickHouseRelationalError::Unsupported(format!( "scalar expression {other:?}" @@ -602,6 +710,25 @@ fn eval(expr: &QueryExpr, row: &[Cell]) -> Result Result { + if nullable { + return Ok(Cell::Null); + } + Ok(match dtype { + DataType::Null => Cell::Null, + DataType::Int64 => Cell::Int64(0), + DataType::Float64 => Cell::Float64(0.0), + DataType::Utf8 => Cell::Utf8(String::new()), + DataType::Bool => Cell::Bool(false), + DataType::Map { .. } => Cell::Map(Vec::new()), + _ => { + return Err(ClickHouseRelationalError::Unsupported( + "map missing-key default type".into(), + )) + } + }) +} + fn compare(op: &CompareOpKind, left: Cell, right: Cell) -> Result { if matches!(left, Cell::Null) || matches!(right, Cell::Null) { return Ok(Cell::Null); @@ -633,6 +760,22 @@ fn arithmetic( if matches!(left, Cell::Null) || matches!(right, Cell::Null) { return Ok(Cell::Null); } + if let (Cell::Int64(left), Cell::Int64(right)) = (&left, &right) { + let integer = match op { + ArithmeticOpKind::Add => Some(left.checked_add(*right)), + ArithmeticOpKind::Sub => Some(left.checked_sub(*right)), + ArithmeticOpKind::Mul => Some(left.checked_mul(*right)), + ArithmeticOpKind::Mod => Some(left.checked_rem(*right)), + _ => None, + }; + if let Some(value) = integer { + return value.map(Cell::Int64).ok_or_else(|| { + ClickHouseRelationalError::Invalid( + "integer arithmetic overflow or zero divisor".into(), + ) + }); + } + } let (left, right) = match (left, right) { (Cell::Int64(left), Cell::Int64(right)) => (left as f64, right as f64), (Cell::Int64(left), Cell::Float64(right)) => (left as f64, right), @@ -660,12 +803,17 @@ fn arithmetic( Ok(Cell::Float64(value)) } -fn compare_sort_keys(left: &[Cell], right: &[Cell], keys: &[SortKey]) -> Ordering { +fn compare_sort_keys( + left: &[Cell], + right: &[Cell], + keys: &[SortKey], + schema: &planner_types::pre_asap::Schema, +) -> Ordering { for key in keys { - let Ok(left) = eval(&key.expr, left) else { + let Ok(left) = eval(&key.expr, left, schema) else { return Ordering::Equal; }; - let Ok(right) = eval(&key.expr, right) else { + let Ok(right) = eval(&key.expr, right, schema) else { return Ordering::Equal; }; let (ordering, order_depends_on_direction) = match (&left, &right) { @@ -698,12 +846,42 @@ fn compare_sort_keys(left: &[Cell], right: &[Cell], keys: &[SortKey]) -> Orderin Ordering::Equal } +fn contains_nan(value: &Cell) -> bool { + match value { + Cell::Float64(value) => value.is_nan(), + Cell::Map(entries) => entries + .iter() + .any(|(key, value)| contains_nan(key) || contains_nan(value)), + _ => false, + } +} + +fn integer_float_cmp(integer: i64, float: f64) -> Option { + if float.is_nan() { + return None; + } + // These bounds are powers of two, exactly representable as Float64. + if float >= 9_223_372_036_854_775_808.0 { + return Some(Ordering::Less); + } + if float < -9_223_372_036_854_775_808.0 { + return Some(Ordering::Greater); + } + let integral = float as i64; + match integer.cmp(&integral) { + Ordering::Equal => 0.0_f64.partial_cmp(&float.fract()), + other => Some(other), + } +} + fn cell_cmp(left: &Cell, right: &Cell) -> Option { match (left, right) { (Cell::Int64(left), Cell::Int64(right)) => Some(left.cmp(right)), (Cell::Float64(left), Cell::Float64(right)) => left.partial_cmp(right), - (Cell::Int64(left), Cell::Float64(right)) => (*left as f64).partial_cmp(right), - (Cell::Float64(left), Cell::Int64(right)) => left.partial_cmp(&(*right as f64)), + (Cell::Int64(left), Cell::Float64(right)) => integer_float_cmp(*left, *right), + (Cell::Float64(left), Cell::Int64(right)) => { + integer_float_cmp(*right, *left).map(Ordering::reverse) + } (Cell::Utf8(left), Cell::Utf8(right)) => Some(left.cmp(right)), (Cell::Bool(left), Cell::Bool(right)) => Some(left.cmp(right)), (Cell::Timestamp(left), Cell::Timestamp(right)) => Some(left.cmp(right)), @@ -739,6 +917,19 @@ fn intersect_coverage(current: Option<(u64, u64)>, next: Option<(u64, u64)>) -> fn arrow_type(dtype: &DataType) -> ArrowDataType { match dtype { + DataType::Null => ArrowDataType::Null, + DataType::List { element } => ArrowDataType::List(Arc::new(Field::new( + &element.name, + arrow_type(&element.dtype), + element.nullable, + ))), + DataType::Struct { fields } => ArrowDataType::Struct( + fields + .iter() + .map(|field| Field::new(&field.name, arrow_type(&field.dtype), field.nullable)) + .collect::>() + .into(), + ), DataType::Int64 => ArrowDataType::Int64, DataType::Float64 => ArrowDataType::Float64, DataType::Utf8 => ArrowDataType::Utf8, @@ -786,6 +977,22 @@ fn build_array( }}; } Ok(match dtype { + DataType::Null => { + if rows + .iter() + .any(|row| !matches!(row.get(column), Some(Cell::Null))) + { + return Err(ClickHouseRelationalError::Invalid( + "non-null value in bottom-typed column".into(), + )); + } + Arc::new(NullArray::new(rows.len())) as ArrayRef + } + DataType::List { .. } | DataType::Struct { .. } => { + return Err(ClickHouseRelationalError::Unsupported( + "collection value transport".into(), + )) + } DataType::Int64 => Arc::new(Int64Array::from(values!(Int64))) as ArrayRef, DataType::Float64 => Arc::new(Float64Array::from(values!(Float64))) as ArrayRef, DataType::Utf8 => Arc::new(StringArray::from(values!(Utf8))) as ArrayRef, @@ -1054,7 +1261,12 @@ mod tests { #[test] fn unsupported_scalar_expression_fails_closed() { let row = vec![Cell::Float64(1.0)]; - let error = eval(&QueryExpr::BoolAnd(vec![]), &row).unwrap_err(); + let error = eval( + &QueryExpr::BoolAnd(vec![]), + &row, + &planner_types::pre_asap::Schema::new(vec![]), + ) + .unwrap_err(); assert!(matches!(error, ClickHouseRelationalError::Unsupported(_))); } @@ -1124,3 +1336,180 @@ mod tests { ); } } + +#[cfg(test)] +mod scalar_contract_tests { + use super::*; + use planner_types::pre_asap::{Column, Schema}; + + fn function(name: &str, args: Vec) -> QueryExpr { + QueryExpr::FunctionCall { + name: name.into(), + args, + } + } + fn text(value: &str) -> QueryExpr { + QueryExpr::Literal(ScalarValue::Utf8(value.into())) + } + + #[test] + fn map_access_uses_declared_default_and_first_duplicate() { + let dtype = DataType::Map { + key: Box::new(DataType::Utf8), + value: Box::new(DataType::Int64), + value_nullable: false, + }; + let schema = Schema::new(vec![Column::new("m", dtype, false)]); + let access = function("asap_map_access", vec![QueryExpr::Column(0), text("a")]); + assert_eq!( + eval(&access, &[Cell::Map(vec![])], &schema).unwrap(), + Cell::Int64(0) + ); + assert_eq!( + eval( + &access, + &[Cell::Map(vec![ + (Cell::Utf8("a".into()), Cell::Int64(7)), + (Cell::Utf8("a".into()), Cell::Int64(9)) + ])], + &schema + ) + .unwrap(), + Cell::Int64(7) + ); + let nullable = Schema::new(vec![Column::new( + "m", + DataType::Map { + key: Box::new(DataType::Utf8), + value: Box::new(DataType::Int64), + value_nullable: true, + }, + false, + )]); + assert_eq!( + eval(&access, &[Cell::Map(vec![])], &nullable).unwrap(), + Cell::Null + ); + let null_key = function( + "asap_map_access", + vec![QueryExpr::Column(0), QueryExpr::Literal(ScalarValue::Null)], + ); + assert_eq!( + eval(&null_key, &[Cell::Map(vec![])], &schema).unwrap(), + Cell::Null + ); + } + + #[test] + fn map_concat_preserves_duplicates_and_empty_map() { + let map = |value| { + function( + "map", + vec![text("a"), QueryExpr::Literal(ScalarValue::Int64(value))], + ) + }; + let concat = function("mapConcat", vec![function("map", vec![]), map(7), map(9)]); + let schema = Schema::new(vec![]); + assert_eq!( + eval(&concat, &[], &schema).unwrap(), + Cell::Map(vec![ + (Cell::Utf8("a".into()), Cell::Int64(7)), + (Cell::Utf8("a".into()), Cell::Int64(9)) + ]) + ); + let mixed = function( + "map", + vec![ + text("a"), + QueryExpr::Literal(ScalarValue::Int64(1)), + text("b"), + QueryExpr::Literal(ScalarValue::Float64(2.5)), + ], + ); + assert!(eval(&mixed, &[], &schema).is_err()); + } + + #[test] + fn sorting_nested_nan_fails_before_comparator_can_treat_it_as_equal() { + let dtype = DataType::Map { + key: Box::new(DataType::Utf8), + value: Box::new(DataType::Float64), + value_nullable: false, + }; + let input = ClickHouseRelation { + rows: vec![vec![Cell::Map(vec![( + Cell::Utf8("a".into()), + Cell::Float64(f64::NAN), + )])]], + fields: vec![("m".into(), dtype.clone(), false)], + coverage: None, + }; + let schema = SummarySchema { + fields: vec![planner_types::post_asap::SummaryField { + name: "m".into(), + dtype: SummaryFamilyType::Plain(dtype), + nullable: false, + }], + time_index: None, + }; + let operation = ValueOperation::Sort { + keys: vec![SortKey { + expr: QueryExpr::Column(0), + ascending: true, + nulls_first: false, + }], + partition_by: planner_types::pre_asap::GroupKeys::none(), + }; + assert!(ClickHouseRelationalAdapter + .apply_operation(&operation, &schema, input) + .is_err()); + } + + #[test] + fn mixed_comparison_preserves_integer_precision_and_boundaries() { + assert_eq!( + integer_float_cmp(9_007_199_254_740_993, 9_007_199_254_740_992.0), + Some(Ordering::Greater) + ); + assert_eq!( + integer_float_cmp(i64::MAX, 9_223_372_036_854_775_808.0), + Some(Ordering::Less) + ); + assert_eq!( + integer_float_cmp(i64::MIN, -9_223_372_036_854_775_808.0), + Some(Ordering::Equal) + ); + assert_eq!(integer_float_cmp(-1, -1.5), Some(Ordering::Greater)); + assert_eq!(integer_float_cmp(1, 1.5), Some(Ordering::Less)); + assert_eq!(integer_float_cmp(0, f64::INFINITY), Some(Ordering::Less)); + assert_eq!( + integer_float_cmp(0, f64::NEG_INFINITY), + Some(Ordering::Greater) + ); + assert_eq!(integer_float_cmp(0, f64::NAN), None); + } + + #[test] + fn integer_modulo_never_rounds_through_float() { + assert_eq!( + arithmetic( + &ArithmeticOpKind::Mod, + Cell::Int64(9_007_199_254_740_993), + Cell::Int64(2) + ) + .unwrap(), + Cell::Int64(1) + ); + assert_eq!( + arithmetic(&ArithmeticOpKind::Mod, Cell::Int64(-7), Cell::Int64(3)).unwrap(), + Cell::Int64(-1) + ); + assert!(arithmetic(&ArithmeticOpKind::Mod, Cell::Int64(7), Cell::Int64(0)).is_err()); + assert!(arithmetic( + &ArithmeticOpKind::Mod, + Cell::Int64(i64::MIN), + Cell::Int64(-1) + ) + .is_err()); + } +}