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