Skip to main content

datafusion_physical_expr/simplifier/
unwrap_cast.rs

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