This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 93724b0b8e fix: validate wrapped negation during type coercion (#20965)
93724b0b8e is described below

commit 93724b0b8ea283e78f3ef9068bc0666c9af39e7e
Author: yaommen <[email protected]>
AuthorDate: Sun Mar 22 23:20:28 2026 +0800

    fix: validate wrapped negation during type coercion (#20965)
    
    ## Which issue does this PR close?
    
    - Related to #20988 .
    
    ## Rationale for this change
    
    Invalid wrapped negation expressions such as `(-'a') IS NULL` could
    still escape type validation during optimization.
    
    This PR keeps the fix in the analyzer/type coercion path, which is where
    most of DataFusion's type validation already happens, rather than adding
    SQL-planner-specific validation.
    
    ## What changes are included in this PR?
    
    - add type validation for `Expr::Negative` in `TypeCoercionRewriter`
    - return an error for invalid negation operands during optimization
    - add focused regression tests for wrapped negation
    
    ## Are these changes tested?
    
    Yes.
    
    Added:
    - a unit test for `(-'a') IS NULL` in `type_coercion`
    - an integration test that verifies the same case fails during
    `into_optimized_plan()`
    
    ## Are there any user-facing changes?
    
    Yes. Invalid wrapped negation expressions now fail during optimization
    instead of escaping type validation until later phases.
---
 datafusion/core/tests/sql/sql_api.rs               | 17 ++++++++++++
 datafusion/optimizer/src/analyzer/type_coercion.rs | 30 ++++++++++++++++++++--
 2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/datafusion/core/tests/sql/sql_api.rs 
b/datafusion/core/tests/sql/sql_api.rs
index b87afd27dd..290aa737d2 100644
--- a/datafusion/core/tests/sql/sql_api.rs
+++ b/datafusion/core/tests/sql/sql_api.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use datafusion::prelude::*;
+use datafusion_common::assert_contains;
 
 use tempfile::TempDir;
 
@@ -206,3 +207,19 @@ async fn ddl_can_not_be_planned_by_session_state() {
         "This feature is not implemented: Unsupported logical plan: DropTable"
     );
 }
+
+#[tokio::test]
+async fn invalid_wrapped_negation_fails_during_optimization() {
+    let ctx = SessionContext::new();
+    let err = ctx
+        .sql("SELECT * FROM (SELECT 1) WHERE ((-'a') IS NULL)")
+        .await
+        .unwrap()
+        .into_optimized_plan()
+        .unwrap_err();
+
+    assert_contains!(
+        err.strip_backtrace(),
+        "Negation only supports numeric, interval and timestamp types"
+    );
+}
diff --git a/datafusion/optimizer/src/analyzer/type_coercion.rs 
b/datafusion/optimizer/src/analyzer/type_coercion.rs
index efc9984acb..e21edeccdc 100644
--- a/datafusion/optimizer/src/analyzer/type_coercion.rs
+++ b/datafusion/optimizer/src/analyzer/type_coercion.rs
@@ -43,10 +43,12 @@ use datafusion_expr::expr_schema::cast_subquery;
 use datafusion_expr::logical_plan::Subquery;
 use datafusion_expr::type_coercion::binary::{comparison_coercion, 
like_coercion};
 use datafusion_expr::type_coercion::functions::{UDFCoercionExt, 
fields_with_udf};
-use datafusion_expr::type_coercion::is_datetime;
 use datafusion_expr::type_coercion::other::{
     get_coerce_type_for_case_expression, get_coerce_type_for_list,
 };
+use datafusion_expr::type_coercion::{
+    is_datetime, is_interval, is_signed_numeric, is_timestamp,
+};
 use datafusion_expr::utils::merge_schema;
 use datafusion_expr::{
     Cast, Expr, ExprSchemable, Join, Limit, LogicalPlan, Operator, Projection, 
Union,
@@ -559,6 +561,20 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> {
             Expr::IsNotUnknown(expr) => Ok(Transformed::yes(is_not_unknown(
                 get_casted_expr_for_bool_op(*expr, self.schema)?,
             ))),
+            Expr::Negative(expr) => {
+                let data_type = expr.get_type(self.schema)?;
+                if data_type.is_null()
+                    || is_signed_numeric(&data_type)
+                    || is_interval(&data_type)
+                    || is_timestamp(&data_type)
+                {
+                    Ok(Transformed::no(Expr::Negative(expr)))
+                } else {
+                    plan_err!(
+                        "Negation only supports numeric, interval and 
timestamp types"
+                    )
+                }
+            }
             Expr::Like(Like {
                 negated,
                 expr,
@@ -753,7 +769,6 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> {
             | Expr::SimilarTo(_)
             | Expr::IsNotNull(_)
             | Expr::IsNull(_)
-            | Expr::Negative(_)
             | Expr::Cast(_)
             | Expr::TryCast(_)
             | Expr::Wildcard { .. }
@@ -1369,6 +1384,17 @@ mod test {
         )
     }
 
+    #[test]
+    fn negative_expr_wrapped_by_is_null_errors() -> Result<()> {
+        let predicate = 
Expr::IsNull(Box::new(Expr::Negative(Box::new(lit("a")))));
+        let plan = LogicalPlan::Filter(Filter::try_new(predicate, empty())?);
+
+        assert_type_coercion_error(
+            plan,
+            "Negation only supports numeric, interval and timestamp types",
+        )
+    }
+
     #[test]
     fn test_coerce_union() -> Result<()> {
         let left_plan = Arc::new(LogicalPlan::EmptyRelation(EmptyRelation {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to