## Coverage Report

#### Created: 2025-11-22 06:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/datafusion/datafusion/physical-expr/src/simplifier/unwrap_cast.rs

    Line
    Count
    Source
    1
    // Licensed to the Apache Software Foundation (ASF) under one
    2
    // or more contributor license agreements.  See the NOTICE file
    3
    // distributed with this work for additional information
    4
    // regarding copyright ownership.  The ASF licenses this file
    5
    // to you under the Apache License, Version 2.0 (the
    6
    // "License"); you may not use this file except in compliance
    7
    // with the License.  You may obtain a copy of the License at
    8
    //
    9
    //   http://www.apache.org/licenses/LICENSE-2.0
    10
    //
    11
    // Unless required by applicable law or agreed to in writing,
    12
    // software distributed under the License is distributed on an
    13
    // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14
    // KIND, either express or implied.  See the License for the
    15
    // specific language governing permissions and limitations
    16
    // under the License.
    17
    18
    //! Unwrap casts in binary comparisons for physical expressions
    19
    //!
    20
    //! This module provides optimization for physical expressions similar to the logical
    21
    //! optimizer's unwrap_cast module. It attempts to remove casts from comparisons to
    22
    //! literals by applying the casts to the literals if possible.
    23
    //!
    24
    //! The optimization improves performance by:
    25
    //! 1. Reducing runtime cast operations on column data
    26
    //! 2. Enabling better predicate pushdown opportunities
    27
    //! 3. Optimizing filter expressions in physical plans
    28
    //!
    29
    //! # Example
    30
    //!
    31
    //! Physical expression: `cast(column as INT64) > INT64(10)`
    32
    //! Optimized to: `column > INT32(10)` (assuming column is INT32)
    33
    34
    use std::sync::Arc;
    35
    36
    use arrow::datatypes::{DataType, Schema};
    37
    use datafusion_common::{
    38
        tree_node::{Transformed, TreeNode},
    39
        Result, ScalarValue,
    40
    };
    41
    use datafusion_expr::Operator;
    42
    use datafusion_expr_common::casts::try_cast_literal_to_type;
    43
    44
    use crate::expressions::{lit, BinaryExpr, CastExpr, Literal, TryCastExpr};
    45
    use crate::PhysicalExpr;
    46
    47
    /// Attempts to unwrap casts in comparison expressions.
    48
    0
    pub(crate) fn unwrap_cast_in_comparison(
    49
    0
        expr: Arc<dyn PhysicalExpr>,
    50
    0
        schema: &Schema,
    51
    0
    ) -> Result<Transformed<Arc<dyn PhysicalExpr>>> {
    52
    0
        expr.transform_down(|e| {
    53
    0
            if let Some(binary) = e.as_any().downcast_ref::<BinaryExpr>() {
    54
    0
                if let Some(unwrapped) = try_unwrap_cast_binary(binary, schema)? {
    55
    0
                    return Ok(Transformed::yes(unwrapped));
    56
    0
                }
    57
    0
            }
    58
    0
            Ok(Transformed::no(e))
    59
    0
        })
    60
    0
    }
    61
    62
    /// Try to unwrap casts in binary expressions
    63
    0
    fn try_unwrap_cast_binary(
    64
    0
        binary: &BinaryExpr,
    65
    0
        schema: &Schema,
    66
    0
    ) -> Result<Option<Arc<dyn PhysicalExpr>>> {
    67
        // Case 1: cast(left_expr) op literal
    68
    0
        if let (Some((inner_expr, _cast_type)), Some(literal)) = (
    69
    0
            extract_cast_info(binary.left()),
    70
    0
            binary.right().as_any().downcast_ref::<Literal>(),
    71
        ) {
    72
    0
            if binary.op().supports_propagation() {
    73
    0
                if let Some(unwrapped) = try_unwrap_cast_comparison(
    74
    0
                    Arc::clone(inner_expr),
    75
    0
                    literal.value(),
    76
    0
                    *binary.op(),
    77
    0
                    schema,
    78
    0
                )? {
    79
    0
                    return Ok(Some(unwrapped));
    80
    0
                }
    81
    0
            }
    82
    0
        }
    83
    84
        // Case 2: literal op cast(right_expr)
    85
    0
        if let (Some(literal), Some((inner_expr, _cast_type))) = (
    86
    0
            binary.left().as_any().downcast_ref::<Literal>(),
    87
    0
            extract_cast_info(binary.right()),
    88
        ) {
    89
            // For literal op cast(expr), we need to swap the operator
    90
    0
            if let Some(swapped_op) = binary.op().swap() {
    91
    0
                if binary.op().supports_propagation() {
    92
    0
                    if let Some(unwrapped) = try_unwrap_cast_comparison(
    93
    0
                        Arc::clone(inner_expr),
    94
    0
                        literal.value(),
    95
    0
                        swapped_op,
    96
    0
                        schema,
    97
    0
                    )? {
    98
    0
                        return Ok(Some(unwrapped));
    99
    0
                    }
    100
    0
                }
    101
    0
            }
    102
            // If the operator cannot be swapped, we skip this optimization case
    103
            // but don't prevent other optimizations
    104
    0
        }
    105
    106
    0
        Ok(None)
    107
    0
    }
    108
    109
    /// Extract cast information from a physical expression
    110
    ///
    111
    /// If the expression is a CAST(expr, datatype) or TRY_CAST(expr, datatype),
    112
    /// returns Some((inner_expr, target_datatype)). Otherwise returns None.
    113
    0
    fn extract_cast_info(
    114
    0
        expr: &Arc<dyn PhysicalExpr>,
    115
    0
    ) -> Option<(&Arc<dyn PhysicalExpr>, &DataType)> {
    116
    0
        if let Some(cast) = expr.as_any().downcast_ref::<CastExpr>() {
    117
    0
            Some((cast.expr(), cast.cast_type()))
    118
    0
        } else if let Some(try_cast) = expr.as_any().downcast_ref::<TryCastExpr>() {
    119
    0
            Some((try_cast.expr(), try_cast.cast_type()))
    120
        } else {
    121
    0
            None
    122
        }
    123
    0
    }
    124
    125
    /// Try to unwrap a cast in comparison by moving the cast to the literal
    126
    0
    fn try_unwrap_cast_comparison(
    127
    0
        inner_expr: Arc<dyn PhysicalExpr>,
    128
    0
        literal_value: &ScalarValue,
    129
    0
        op: Operator,
    130
    0
        schema: &Schema,
    131
    0
    ) -> Result<Option<Arc<dyn PhysicalExpr>>> {
    132
        // Get the data type of the inner expression
    133
    0
        let inner_type = inner_expr.data_type(schema)?;
    134
    135
        // Try to cast the literal to the inner expression's type
    136
    0
        if let Some(casted_literal) = try_cast_literal_to_type(literal_value, &inner_type) {
    137
    0
            let literal_expr = lit(casted_literal);
    138
    0
            let binary_expr = BinaryExpr::new(inner_expr, op, literal_expr);
    139
    0
            return Ok(Some(Arc::new(binary_expr)));
    140
    0
        }
    141
    142
    0
        Ok(None)
    143
    0
    }
    144
    145
    #[cfg(test)]
    146
    mod tests {
    147
        use super::*;
    148
        use crate::expressions::{col, lit};
    149
        use arrow::datatypes::{DataType, Field, Schema};
    150
        use datafusion_common::ScalarValue;
    151
        use datafusion_expr::Operator;
    152
    153
        /// Check if an expression is a cast expression
    154
    0
        fn is_cast_expr(expr: &Arc<dyn PhysicalExpr>) -> bool {
    155
    0
            expr.as_any().downcast_ref::<CastExpr>().is_some()
    156
    0
                || expr.as_any().downcast_ref::<TryCastExpr>().is_some()
    157
    0
        }
    158
    159
        /// Check if a binary expression is suitable for cast unwrapping
    160
    0
        fn is_binary_expr_with_cast_and_literal(binary: &BinaryExpr) -> bool {
    161
            // Check if left is cast and right is literal
    162
    0
            let left_cast_right_literal = is_cast_expr(binary.left())
    163
    0
                && binary.right().as_any().downcast_ref::<Literal>().is_some();
    164
    165
            // Check if left is literal and right is cast
    166
    0
            let left_literal_right_cast =
    167
    0
                binary.left().as_any().downcast_ref::<Literal>().is_some()
    168
    0
                    && is_cast_expr(binary.right());
    169
    170
    0
            left_cast_right_literal || left_literal_right_cast
    171
    0
        }
    172
    173
    0
        fn test_schema() -> Schema {
    174
    0
            Schema::new(vec![
    175
    0
                Field::new("c1", DataType::Int32, false),
    176
    0
                Field::new("c2", DataType::Int64, false),
    177
    0
                Field::new("c3", DataType::Utf8, false),
    178
            ])
    179
    0
        }
    180
    181
        #[test]
    182
    0
        fn test_unwrap_cast_in_binary_comparison() {
    183
    0
            let schema = test_schema();
    184
    185
            // Create: cast(c1 as INT64) > INT64(10)
    186
    0
            let column_expr = col("c1", &schema).unwrap();
    187
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    188
    0
            let literal_expr = lit(10i64);
    189
    0
            let binary_expr =
    190
    0
                Arc::new(BinaryExpr::new(cast_expr, Operator::Gt, literal_expr));
    191
    192
            // Apply unwrap cast optimization
    193
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    194
    195
            // Should be transformed
    196
    0
            assert!(result.transformed);
    197
    198
            // The result should be: c1 > INT32(10)
    199
    0
            let optimized = result.data;
    200
    0
            let optimized_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    201
    202
            // Check that left side is no longer a cast
    203
    0
            assert!(!is_cast_expr(optimized_binary.left()));
    204
    205
            // Check that right side is a literal with the correct type and value
    206
    0
            let right_literal = optimized_binary
    207
    0
                .right()
    208
    0
                .as_any()
    209
    0
                .downcast_ref::<Literal>()
    210
    0
                .unwrap();
    211
    0
            assert_eq!(right_literal.value(), &ScalarValue::Int32(Some(10)));
    212
    0
        }
    213
    214
        #[test]
    215
    0
        fn test_unwrap_cast_with_literal_on_left() {
    216
    0
            let schema = test_schema();
    217
    218
            // Create: INT64(10) < cast(c1 as INT64)
    219
    0
            let column_expr = col("c1", &schema).unwrap();
    220
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    221
    0
            let literal_expr = lit(10i64);
    222
    0
            let binary_expr =
    223
    0
                Arc::new(BinaryExpr::new(literal_expr, Operator::Lt, cast_expr));
    224
    225
            // Apply unwrap cast optimization
    226
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    227
    228
            // Should be transformed
    229
    0
            assert!(result.transformed);
    230
    231
            // The result should be equivalent to: c1 > INT32(10)
    232
    0
            let optimized = result.data;
    233
    0
            let optimized_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    234
    235
            // Check the operator was swapped
    236
    0
            assert_eq!(*optimized_binary.op(), Operator::Gt);
    237
    0
        }
    238
    239
        #[test]
    240
    0
        fn test_no_unwrap_when_types_unsupported() {
    241
    0
            let schema = Schema::new(vec![Field::new("f1", DataType::Float32, false)]);
    242
    243
            // Create: cast(f1 as FLOAT64) > FLOAT64(10.5)
    244
    0
            let column_expr = col("f1", &schema).unwrap();
    245
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Float64, None));
    246
    0
            let literal_expr = lit(10.5f64);
    247
    0
            let binary_expr =
    248
    0
                Arc::new(BinaryExpr::new(cast_expr, Operator::Gt, literal_expr));
    249
    250
            // Apply unwrap cast optimization
    251
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    252
    253
            // Should NOT be transformed (floating point types not supported)
    254
    0
            assert!(!result.transformed);
    255
    0
        }
    256
    257
        #[test]
    258
    0
        fn test_is_binary_expr_with_cast_and_literal() {
    259
    0
            let schema = test_schema();
    260
    261
    0
            let column_expr = col("c1", &schema).unwrap();
    262
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    263
    0
            let literal_expr = lit(10i64);
    264
    0
            let binary_expr =
    265
    0
                Arc::new(BinaryExpr::new(cast_expr, Operator::Gt, literal_expr));
    266
    0
            let binary_ref = binary_expr.as_any().downcast_ref::<BinaryExpr>().unwrap();
    267
    268
    0
            assert!(is_binary_expr_with_cast_and_literal(binary_ref));
    269
    0
        }
    270
    271
        #[test]
    272
    0
        fn test_unwrap_cast_literal_on_left_side() {
    273
            // Test case for: literal <= cast(column)
    274
            // This was the specific case that caused the bug
    275
    0
            let schema = Schema::new(vec![Field::new(
    276
                "decimal_col",
    277
    0
                DataType::Decimal128(9, 2),
    278
                true,
    279
            )]);
    280
    281
            // Create: Decimal128(400) <= cast(decimal_col as Decimal128(22, 2))
    282
    0
            let column_expr = col("decimal_col", &schema).unwrap();
    283
    0
            let cast_expr = Arc::new(CastExpr::new(
    284
    0
                column_expr,
    285
    0
                DataType::Decimal128(22, 2),
    286
    0
                None,
    287
            ));
    288
    0
            let literal_expr = lit(ScalarValue::Decimal128(Some(400), 22, 2));
    289
    0
            let binary_expr =
    290
    0
                Arc::new(BinaryExpr::new(literal_expr, Operator::LtEq, cast_expr));
    291
    292
            // Apply unwrap cast optimization
    293
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    294
    295
            // Should be transformed
    296
    0
            assert!(result.transformed);
    297
    298
            // The result should be: decimal_col >= Decimal128(400, 9, 2)
    299
    0
            let optimized = result.data;
    300
    0
            let optimized_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    301
    302
            // Check operator was swapped correctly
    303
    0
            assert_eq!(*optimized_binary.op(), Operator::GtEq);
    304
    305
            // Check that left side is the column without cast
    306
    0
            assert!(!is_cast_expr(optimized_binary.left()));
    307
    308
            // Check that right side is a literal with the correct type
    309
    0
            let right_literal = optimized_binary
    310
    0
                .right()
    311
    0
                .as_any()
    312
    0
                .downcast_ref::<Literal>()
    313
    0
                .unwrap();
    314
    0
            assert_eq!(
    315
    0
                right_literal.value().data_type(),
    316
                DataType::Decimal128(9, 2)
    317
            );
    318
    0
        }
    319
    320
        #[test]
    321
    0
        fn test_unwrap_cast_with_different_comparison_operators() {
    322
    0
            let schema = Schema::new(vec![Field::new("int_col", DataType::Int32, false)]);
    323
    324
            // Test all comparison operators with literal on the left
    325
    0
            let operators = vec![
    326
    0
                (Operator::Lt, Operator::Gt),
    327
    0
                (Operator::LtEq, Operator::GtEq),
    328
    0
                (Operator::Gt, Operator::Lt),
    329
    0
                (Operator::GtEq, Operator::LtEq),
    330
    0
                (Operator::Eq, Operator::Eq),
    331
    0
                (Operator::NotEq, Operator::NotEq),
    332
            ];
    333
    334
    0
            for (original_op, expected_op) in operators {
    335
                // Create: INT64(100) op cast(int_col as INT64)
    336
    0
                let column_expr = col("int_col", &schema).unwrap();
    337
    0
                let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    338
    0
                let literal_expr = lit(100i64);
    339
    0
                let binary_expr =
    340
    0
                    Arc::new(BinaryExpr::new(literal_expr, original_op, cast_expr));
    341
    342
                // Apply unwrap cast optimization
    343
    0
                let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    344
    345
                // Should be transformed
    346
    0
                assert!(result.transformed);
    347
    348
    0
                let optimized = result.data;
    349
    0
                let optimized_binary =
    350
    0
                    optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    351
    352
                // Check the operator was swapped correctly
    353
    0
                assert_eq!(
    354
    0
                    *optimized_binary.op(),
    355
                    expected_op,
    356
    0
                    "Failed for operator {original_op:?} -> {expected_op:?}"
    357
                );
    358
    359
                // Check that left side has no cast
    360
    0
                assert!(!is_cast_expr(optimized_binary.left()));
    361
    362
                // Check that the literal was cast to the column type
    363
    0
                let right_literal = optimized_binary
    364
    0
                    .right()
    365
    0
                    .as_any()
    366
    0
                    .downcast_ref::<Literal>()
    367
    0
                    .unwrap();
    368
    0
                assert_eq!(right_literal.value(), &ScalarValue::Int32(Some(100)));
    369
            }
    370
    0
        }
    371
    372
        #[test]
    373
    0
        fn test_unwrap_cast_with_decimal_types() {
    374
            // Test various decimal precision/scale combinations
    375
    0
            let test_cases = vec![
    376
                // (column_precision, column_scale, cast_precision, cast_scale, value)
    377
    0
                (9, 2, 22, 2, 400),
    378
    0
                (10, 3, 20, 3, 1000),
    379
    0
                (5, 1, 10, 1, 99),
    380
            ];
    381
    382
    0
            for (col_p, col_s, cast_p, cast_s, value) in test_cases {
    383
    0
                let schema = Schema::new(vec![Field::new(
    384
                    "decimal_col",
    385
    0
                    DataType::Decimal128(col_p, col_s),
    386
                    true,
    387
                )]);
    388
    389
                // Test both: cast(column) op literal AND literal op cast(column)
    390
    391
                // Case 1: cast(column) > literal
    392
    0
                let column_expr = col("decimal_col", &schema).unwrap();
    393
    0
                let cast_expr = Arc::new(CastExpr::new(
    394
    0
                    Arc::clone(&column_expr),
    395
    0
                    DataType::Decimal128(cast_p, cast_s),
    396
    0
                    None,
    397
                ));
    398
    0
                let literal_expr = lit(ScalarValue::Decimal128(Some(value), cast_p, cast_s));
    399
    0
                let binary_expr =
    400
    0
                    Arc::new(BinaryExpr::new(cast_expr, Operator::Gt, literal_expr));
    401
    402
    0
                let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    403
    0
                assert!(result.transformed);
    404
    405
                // Case 2: literal < cast(column)
    406
    0
                let cast_expr = Arc::new(CastExpr::new(
    407
    0
                    column_expr,
    408
    0
                    DataType::Decimal128(cast_p, cast_s),
    409
    0
                    None,
    410
                ));
    411
    0
                let literal_expr = lit(ScalarValue::Decimal128(Some(value), cast_p, cast_s));
    412
    0
                let binary_expr =
    413
    0
                    Arc::new(BinaryExpr::new(literal_expr, Operator::Lt, cast_expr));
    414
    415
    0
                let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    416
    0
                assert!(result.transformed);
    417
            }
    418
    0
        }
    419
    420
        #[test]
    421
    0
        fn test_unwrap_cast_with_null_literals() {
    422
            // Test with NULL literals to ensure they're handled correctly
    423
    0
            let schema = Schema::new(vec![Field::new("int_col", DataType::Int32, true)]);
    424
    425
            // Create: cast(int_col as INT64) = NULL
    426
    0
            let column_expr = col("int_col", &schema).unwrap();
    427
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    428
    0
            let null_literal = lit(ScalarValue::Int64(None));
    429
    0
            let binary_expr =
    430
    0
                Arc::new(BinaryExpr::new(cast_expr, Operator::Eq, null_literal));
    431
    432
            // Apply unwrap cast optimization
    433
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    434
    435
            // Should be transformed
    436
    0
            assert!(result.transformed);
    437
    438
            // Verify the NULL was cast to the column type
    439
    0
            let optimized = result.data;
    440
    0
            let optimized_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    441
    0
            let right_literal = optimized_binary
    442
    0
                .right()
    443
    0
                .as_any()
    444
    0
                .downcast_ref::<Literal>()
    445
    0
                .unwrap();
    446
    0
            assert_eq!(right_literal.value(), &ScalarValue::Int32(None));
    447
    0
        }
    448
    449
        #[test]
    450
    0
        fn test_unwrap_cast_with_try_cast() {
    451
            // Test that TryCast expressions are also unwrapped correctly
    452
    0
            let schema = Schema::new(vec![Field::new("str_col", DataType::Utf8, true)]);
    453
    454
            // Create: try_cast(str_col as INT64) > INT64(100)
    455
    0
            let column_expr = col("str_col", &schema).unwrap();
    456
    0
            let try_cast_expr = Arc::new(TryCastExpr::new(column_expr, DataType::Int64));
    457
    0
            let literal_expr = lit(100i64);
    458
    0
            let binary_expr =
    459
    0
                Arc::new(BinaryExpr::new(try_cast_expr, Operator::Gt, literal_expr));
    460
    461
            // Apply unwrap cast optimization
    462
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    463
    464
            // Should NOT be transformed (string to int cast not supported)
    465
    0
            assert!(!result.transformed);
    466
    0
        }
    467
    468
        #[test]
    469
    0
        fn test_unwrap_cast_preserves_non_comparison_operators() {
    470
            // Test that non-comparison operators in AND/OR expressions are preserved
    471
    0
            let schema = Schema::new(vec![Field::new("int_col", DataType::Int32, false)]);
    472
    473
            // Create: cast(int_col as INT64) > INT64(10) AND cast(int_col as INT64) < INT64(20)
    474
    0
            let column_expr = col("int_col", &schema).unwrap();
    475
    476
    0
            let cast1 = Arc::new(CastExpr::new(
    477
    0
                Arc::clone(&column_expr),
    478
    0
                DataType::Int64,
    479
    0
                None,
    480
            ));
    481
    0
            let lit1 = lit(10i64);
    482
    0
            let compare1 = Arc::new(BinaryExpr::new(cast1, Operator::Gt, lit1));
    483
    484
    0
            let cast2 = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    485
    0
            let lit2 = lit(20i64);
    486
    0
            let compare2 = Arc::new(BinaryExpr::new(cast2, Operator::Lt, lit2));
    487
    488
    0
            let and_expr = Arc::new(BinaryExpr::new(compare1, Operator::And, compare2));
    489
    490
            // Apply unwrap cast optimization
    491
    0
            let result = unwrap_cast_in_comparison(and_expr, &schema).unwrap();
    492
    493
            // Should be transformed
    494
    0
            assert!(result.transformed);
    495
    496
            // Verify the AND operator is preserved
    497
    0
            let optimized = result.data;
    498
    0
            let and_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    499
    0
            assert_eq!(*and_binary.op(), Operator::And);
    500
    501
            // Both sides should have their casts unwrapped
    502
    0
            let left_binary = and_binary
    503
    0
                .left()
    504
    0
                .as_any()
    505
    0
                .downcast_ref::<BinaryExpr>()
    506
    0
                .unwrap();
    507
    0
            let right_binary = and_binary
    508
    0
                .right()
    509
    0
                .as_any()
    510
    0
                .downcast_ref::<BinaryExpr>()
    511
    0
                .unwrap();
    512
    513
    0
            assert!(!is_cast_expr(left_binary.left()));
    514
    0
            assert!(!is_cast_expr(right_binary.left()));
    515
    0
        }
    516
    517
        #[test]
    518
    0
        fn test_try_cast_unwrapping() {
    519
    0
            let schema = test_schema();
    520
    521
            // Create: try_cast(c1 as INT64) <= INT64(100)
    522
    0
            let column_expr = col("c1", &schema).unwrap();
    523
    0
            let try_cast_expr = Arc::new(TryCastExpr::new(column_expr, DataType::Int64));
    524
    0
            let literal_expr = lit(100i64);
    525
    0
            let binary_expr =
    526
    0
                Arc::new(BinaryExpr::new(try_cast_expr, Operator::LtEq, literal_expr));
    527
    528
            // Apply unwrap cast optimization
    529
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    530
    531
            // Should be transformed to: c1 <= INT32(100)
    532
    0
            assert!(result.transformed);
    533
    534
    0
            let optimized = result.data;
    535
    0
            let optimized_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    536
    537
            // Verify the try_cast was removed
    538
    0
            assert!(!is_cast_expr(optimized_binary.left()));
    539
    540
            // Verify the literal was converted
    541
    0
            let right_literal = optimized_binary
    542
    0
                .right()
    543
    0
                .as_any()
    544
    0
                .downcast_ref::<Literal>()
    545
    0
                .unwrap();
    546
    0
            assert_eq!(right_literal.value(), &ScalarValue::Int32(Some(100)));
    547
    0
        }
    548
    549
        #[test]
    550
    0
        fn test_non_swappable_operator() {
    551
            // Test case with an operator that cannot be swapped
    552
    0
            let schema = Schema::new(vec![Field::new("int_col", DataType::Int32, false)]);
    553
    554
            // Create: INT64(10) + cast(int_col as INT64)
    555
            // The Plus operator cannot be swapped, so this should not be transformed
    556
    0
            let column_expr = col("int_col", &schema).unwrap();
    557
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    558
    0
            let literal_expr = lit(10i64);
    559
    0
            let binary_expr =
    560
    0
                Arc::new(BinaryExpr::new(literal_expr, Operator::Plus, cast_expr));
    561
    562
            // Apply unwrap cast optimization
    563
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    564
    565
            // Should NOT be transformed because Plus cannot be swapped
    566
    0
            assert!(!result.transformed);
    567
    0
        }
    568
    569
        #[test]
    570
    0
        fn test_cast_that_cannot_be_unwrapped_overflow() {
    571
            // Test case where the literal value would overflow the target type
    572
    0
            let schema = Schema::new(vec![Field::new("small_int", DataType::Int8, false)]);
    573
    574
            // Create: cast(small_int as INT64) > INT64(1000)
    575
            // This should NOT be unwrapped because 1000 cannot fit in Int8 (max value is 127)
    576
    0
            let column_expr = col("small_int", &schema).unwrap();
    577
    0
            let cast_expr = Arc::new(CastExpr::new(column_expr, DataType::Int64, None));
    578
    0
            let literal_expr = lit(1000i64); // Value too large for Int8
    579
    0
            let binary_expr =
    580
    0
                Arc::new(BinaryExpr::new(cast_expr, Operator::Gt, literal_expr));
    581
    582
            // Apply unwrap cast optimization
    583
    0
            let result = unwrap_cast_in_comparison(binary_expr, &schema).unwrap();
    584
    585
            // Should NOT be transformed due to overflow
    586
    0
            assert!(!result.transformed);
    587
    0
        }
    588
    589
        #[test]
    590
    0
        fn test_complex_nested_expression() {
    591
    0
            let schema = test_schema();
    592
    593
            // Create a more complex expression with nested casts
    594
            // (cast(c1 as INT64) > INT64(10)) AND (cast(c2 as INT32) = INT32(20))
    595
    0
            let c1_expr = col("c1", &schema).unwrap();
    596
    0
            let c1_cast = Arc::new(CastExpr::new(c1_expr, DataType::Int64, None));
    597
    0
            let c1_literal = lit(10i64);
    598
    0
            let c1_binary = Arc::new(BinaryExpr::new(c1_cast, Operator::Gt, c1_literal));
    599
    600
    0
            let c2_expr = col("c2", &schema).unwrap();
    601
    0
            let c2_cast = Arc::new(CastExpr::new(c2_expr, DataType::Int32, None));
    602
    0
            let c2_literal = lit(20i32);
    603
    0
            let c2_binary = Arc::new(BinaryExpr::new(c2_cast, Operator::Eq, c2_literal));
    604
    605
            // Create AND expression
    606
    0
            let and_expr = Arc::new(BinaryExpr::new(c1_binary, Operator::And, c2_binary));
    607
    608
            // Apply unwrap cast optimization
    609
    0
            let result = unwrap_cast_in_comparison(and_expr, &schema).unwrap();
    610
    611
            // Should be transformed
    612
    0
            assert!(result.transformed);
    613
    614
            // Verify both sides of the AND were optimized
    615
    0
            let optimized = result.data;
    616
    0
            let and_binary = optimized.as_any().downcast_ref::<BinaryExpr>().unwrap();
    617
    618
            // Left side should be: c1 > INT32(10)
    619
    0
            let left_binary = and_binary
    620
    0
                .left()
    621
    0
                .as_any()
    622
    0
                .downcast_ref::<BinaryExpr>()
    623
    0
                .unwrap();
    624
    0
            assert!(!is_cast_expr(left_binary.left()));
    625
    0
            let left_literal = left_binary
    626
    0
                .right()
    627
    0
                .as_any()
    628
    0
                .downcast_ref::<Literal>()
    629
    0
                .unwrap();
    630
    0
            assert_eq!(left_literal.value(), &ScalarValue::Int32(Some(10)));
    631
    632
            // Right side should be: c2 = INT64(20) (c2 is already INT64, literal cast to match)
    633
    0
            let right_binary = and_binary
    634
    0
                .right()
    635
    0
                .as_any()
    636
    0
                .downcast_ref::<BinaryExpr>()
    637
    0
                .unwrap();
    638
    0
            assert!(!is_cast_expr(right_binary.left()));
    639
    0
            let right_literal = right_binary
    640
    0
                .right()
    641
    0
                .as_any()
    642
    0
                .downcast_ref::<Literal>()
    643
    0
                .unwrap();
    644
    0
            assert_eq!(right_literal.value(), &ScalarValue::Int64(Some(20)));
    645
    0
        }
    646
    }