-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix: stop treating arithmetic negation as bitwise NOT #24668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cfcd150
c37e666
4e9d57f
1b7d5f9
00b519c
69af506
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ use datafusion_expr::physical_planning_context::PhysicalPlanningContext; | |
| use datafusion_expr::{ | ||
| BinaryExpr, Case, ColumnarValue, Expr, ExprSchemable, Like, Operator, Volatility, | ||
| and, binary::BinaryTypeCoercer, lit, or, preimage::PreimageResult, | ||
| type_coercion::is_signed_numeric, | ||
| }; | ||
| use datafusion_expr::{Cast, TryCast, simplify::ExprSimplifyResult}; | ||
| use datafusion_expr::{expr::ScalarFunction, interval_arithmetic::NullableInterval}; | ||
|
|
@@ -760,6 +761,10 @@ struct Simplifier<'a> { | |
| info: &'a SimplifyContext, | ||
| } | ||
|
|
||
| fn can_cancel_double_negation(data_type: &DataType) -> bool { | ||
| data_type.is_null() || is_signed_numeric(data_type) | ||
| } | ||
|
|
||
| impl<'a> Simplifier<'a> { | ||
| pub fn new(info: &'a SimplifyContext) -> Self { | ||
| Self { info } | ||
|
|
@@ -1178,30 +1183,6 @@ impl TreeNodeRewriter for Simplifier<'_> { | |
| right, | ||
| }) if !info.nullable(&right)? && is_zero(&left) => Transformed::yes(*left), | ||
|
|
||
| // !A & A -> 0 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseAnd, | ||
| right, | ||
| }) if is_negative_of(&left, &right) && !info.nullable(&right)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_zero(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // A & !A -> 0 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseAnd, | ||
| right, | ||
| }) if is_negative_of(&right, &left) && !info.nullable(&left)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_zero(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // (..A..) & A --> (..A..) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
|
|
@@ -1252,30 +1233,6 @@ impl TreeNodeRewriter for Simplifier<'_> { | |
| right, | ||
| }) if is_zero(&left) => Transformed::yes(*right), | ||
|
|
||
| // !A | A -> -1 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseOr, | ||
| right, | ||
| }) if is_negative_of(&left, &right) && !info.nullable(&right)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_negative_one(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // A | !A -> -1 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseOr, | ||
| right, | ||
| }) if is_negative_of(&right, &left) && !info.nullable(&left)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_negative_one(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // (..A..) | A --> (..A..) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
|
|
@@ -1326,30 +1283,6 @@ impl TreeNodeRewriter for Simplifier<'_> { | |
| right, | ||
| }) if !info.nullable(&right)? && is_zero(&left) => Transformed::yes(*right), | ||
|
|
||
| // !A ^ A -> -1 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseXor, | ||
| right, | ||
| }) if is_negative_of(&left, &right) && !info.nullable(&right)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_negative_one(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // A ^ !A -> -1 (if A not nullable) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
| op: BitwiseXor, | ||
| right, | ||
| }) if is_negative_of(&right, &left) && !info.nullable(&left)? => { | ||
| Transformed::yes(Expr::Literal( | ||
| ScalarValue::new_negative_one(&info.get_data_type(&left)?)?, | ||
| None, | ||
| )) | ||
| } | ||
|
|
||
| // (..A..) ^ A --> (the expression without A, if number of A is odd, otherwise one A) | ||
| Expr::BinaryExpr(BinaryExpr { | ||
| left, | ||
|
|
@@ -1411,10 +1344,21 @@ impl TreeNodeRewriter for Simplifier<'_> { | |
| // | ||
| Expr::Not(inner) => Transformed::yes(negate_clause(*inner)), | ||
|
|
||
| // | ||
| // Rules for Negative | ||
| // | ||
| Expr::Negative(inner) => Transformed::yes(distribute_negation(*inner)), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Preserve integer double-negation ordering equivalence Removing this arm preserves SELECT i, j FROM fixed_prefix_stream
ORDER BY -(-i) ASC NULLS LAST, j ASC NULLS LAST
LIMIT 1;Base Could we restore cancellation specifically for signed integers, where wrapping makes it safe even at MIN, or preserve equivalent ordering metadata? Extending the new streaming regression to two sort keys would cover this without changing checked timestamp/interval behavior.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| // -(-A) -> A for types where negation is total and involutive. | ||
| // Signed integer negation wraps, floating-point negation is total, | ||
| // and valid decimal precision excludes the native signed minimum. | ||
| // Timestamp and interval negation remains explicit because it can | ||
| // overflow. | ||
| Expr::Negative(inner) => match *inner { | ||
| Expr::Negative(inner) | ||
| if can_cancel_double_negation( | ||
| &info.get_data_type(inner.as_ref())?, | ||
| ) => | ||
| { | ||
| Transformed::yes(*inner) | ||
| } | ||
| inner => Transformed::no(Expr::Negative(Box::new(inner))), | ||
| }, | ||
|
|
||
| // | ||
| // Rules for Case | ||
|
|
@@ -3115,47 +3059,91 @@ mod tests { | |
| } | ||
|
|
||
| #[test] | ||
| fn test_simplify_negated_bitwise_and() { | ||
| // !c3 & c3 --> 0 | ||
| let expr = (-col("c3_non_null")) & col("c3_non_null"); | ||
| let expected = lit(0i64); | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
| // c3 & !c3 --> 0 | ||
| let expr = col("c3_non_null") & (-col("c3_non_null")); | ||
| let expected = lit(0i64); | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_simplify_negated_bitwise_or() { | ||
| // !c3 | c3 --> -1 | ||
| let expr = (-col("c3_non_null")) | col("c3_non_null"); | ||
| let expected = lit(-1i64); | ||
| fn test_preserve_arithmetic_negation() { | ||
| let c3 = col("c3_non_null"); | ||
| let expressions = [ | ||
| (-c3.clone()) & c3.clone(), | ||
| c3.clone() & (-c3.clone()), | ||
| (-c3.clone()) | c3.clone(), | ||
| c3.clone() | (-c3.clone()), | ||
| (-c3.clone()) ^ c3.clone(), | ||
| c3.clone() ^ (-c3.clone()), | ||
| -bitwise_and(col("c3"), c3.clone()), | ||
| -bitwise_or(col("c3"), c3.clone()), | ||
| ]; | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
| for expr in expressions { | ||
| assert_eq!(simplify(expr.clone()), expr); | ||
| } | ||
|
|
||
| // c3 | !c3 --> -1 | ||
| let expr = col("c3_non_null") | (-col("c3_non_null")); | ||
| let expected = lit(-1i64); | ||
| // Timestamp and interval negation are checked, so the inner expression | ||
| // can overflow and must remain visible. | ||
| let checked_types = [ | ||
| ( | ||
| "ts", | ||
| DataType::Timestamp(arrow::datatypes::TimeUnit::Nanosecond, None), | ||
| ), | ||
| ( | ||
| "year_month", | ||
| DataType::Interval(arrow::datatypes::IntervalUnit::YearMonth), | ||
| ), | ||
| ( | ||
| "day_time", | ||
| DataType::Interval(arrow::datatypes::IntervalUnit::DayTime), | ||
| ), | ||
| ( | ||
| "month_day_nano", | ||
| DataType::Interval(arrow::datatypes::IntervalUnit::MonthDayNano), | ||
| ), | ||
| ]; | ||
| let schema = Schema::new( | ||
| checked_types | ||
| .iter() | ||
| .map(|(name, data_type)| Field::new(*name, data_type.clone(), false)) | ||
| .collect::<Vec<_>>(), | ||
| ) | ||
| .to_dfschema_ref() | ||
| .unwrap(); | ||
| let simplifier = | ||
| ExprSimplifier::new(SimplifyContext::builder().with_schema(schema).build()); | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
| for (name, _) in checked_types { | ||
| let expr = -(-col(name)); | ||
| assert_eq!(simplifier.simplify(expr.clone()).unwrap(), expr); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_simplify_negated_bitwise_xor() { | ||
| // !c3 ^ c3 --> -1 | ||
| let expr = (-col("c3_non_null")) ^ col("c3_non_null"); | ||
| let expected = lit(-1i64); | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
|
|
||
| // c3 ^ !c3 --> -1 | ||
| let expr = col("c3_non_null") ^ (-col("c3_non_null")); | ||
| let expected = lit(-1i64); | ||
| fn test_cancel_safe_double_negation() { | ||
| let safe_types = [ | ||
| ("i8", DataType::Int8), | ||
| ("i16", DataType::Int16), | ||
| ("i32", DataType::Int32), | ||
| ("i64", DataType::Int64), | ||
| ("f16", DataType::Float16), | ||
| ("f32", DataType::Float32), | ||
| ("f64", DataType::Float64), | ||
| ("d32", DataType::Decimal32(9, 0)), | ||
| ("d64", DataType::Decimal64(18, 0)), | ||
| ("d128", DataType::Decimal128(38, 0)), | ||
| ("d256", DataType::Decimal256(76, 0)), | ||
| ("null", DataType::Null), | ||
| ]; | ||
| let schema = Schema::new( | ||
| safe_types | ||
| .iter() | ||
| .map(|(name, data_type)| Field::new(*name, data_type.clone(), true)) | ||
| .collect::<Vec<_>>(), | ||
| ) | ||
| .to_dfschema_ref() | ||
| .unwrap(); | ||
| let simplifier = | ||
| ExprSimplifier::new(SimplifyContext::builder().with_schema(schema).build()); | ||
|
|
||
| assert_eq!(simplify(expr), expected); | ||
| for (name, _) in safe_types { | ||
| assert_eq!(simplifier.simplify(-(-col(name))).unwrap(), col(name)); | ||
| assert_eq!(simplifier.simplify(-(-(-col(name)))).unwrap(), -col(name)); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -3340,20 +3328,6 @@ mod tests { | |
| let expr = col("c3").not().not(); | ||
| let expected = col("c3"); | ||
| assert_eq!(simplify(expr), expected); | ||
|
|
||
| // Laws with bitwise operations | ||
| // !(c3 & c4) --> !c3 | !c4 | ||
| let expr = -bitwise_and(col("c3"), col("c4")); | ||
| let expected = bitwise_or(-col("c3"), -col("c4")); | ||
| assert_eq!(simplify(expr), expected); | ||
| // !(c3 | c4) --> !c3 & !c4 | ||
| let expr = -bitwise_or(col("c3"), col("c4")); | ||
| let expected = bitwise_and(-col("c3"), -col("c4")); | ||
| assert_eq!(simplify(expr), expected); | ||
| // !(!c3) --> c3 | ||
| let expr = -(-col("c3")); | ||
| let expected = col("c3"); | ||
| assert_eq!(simplify(expr), expected); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Preserve timestamp-column support when retaining double negation
Removing this arm also stops cancelling
-(-timestamp_column). I reproducedSELECT -(-ts) AS x FROM tusing a registeredTimestampNanosecondArraycontaining2020-01-01and2020-01-02: base63f5b55freturns both timestamps, while headc37e6666fails withInvalid arithmetic operation: !Timestamp(ns). SQL analysis and physical planning explicitly accept timestamp negation, butNegativeExpr::evaluatepasses arrays to Arrow'sneg_wrapping, whose fallback does not support timestamp arrays. Timestamp literals still work through the separate scalar implementation.The single-negation kernel gap already existed, but retaining both nodes newly breaks these previously working double-negation queries. Could we add timestamp-array negation support and a column-based execution regression test alongside this change, while preserving the intended overflow behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
4e9d57f3c.NegativeExprnow uses checked negation for timestamp arrays in seconds, milliseconds, microseconds, and nanoseconds. It preserves timezone metadata and nulls, and reports overflow fori64::MIN. I added unit coverage for each case and an end-to-end timestamp-column regression.