@@ -60,8 +60,7 @@ use datafusion_common::{internal_err, tree_node::Transformed};
6060use datafusion_expr:: { BinaryExpr , lit} ;
6161use datafusion_expr:: { Cast , Expr , Operator , TryCast , simplify:: SimplifyContext } ;
6262use datafusion_expr_common:: casts:: {
63- is_date_narrowing_cast, is_supported_type, is_timestamp_precision_narrowing_cast,
64- try_cast_literal_to_type,
63+ is_date_narrowing_cast, is_supported_type, try_cast_literal_to_type,
6564} ;
6665
6766pub ( super ) fn unwrap_cast_in_comparison_for_binary (
@@ -135,7 +134,7 @@ pub(super) fn is_cast_expr_and_support_unwrap_cast_in_comparison_for_binary(
135134 return false ;
136135 } ;
137136
138- if is_timestamp_precision_narrowing_cast ( & expr_type, field. data_type ( ) )
137+ if changes_timestamp_semantics ( & expr_type, field. data_type ( ) )
139138 || is_date_narrowing_cast ( & expr_type, field. data_type ( ) )
140139 {
141140 return false ;
@@ -180,7 +179,7 @@ pub(super) fn is_cast_expr_and_support_unwrap_cast_in_comparison_for_inlist(
180179 return false ;
181180 }
182181
183- if is_timestamp_precision_narrowing_cast ( & expr_type, field. data_type ( ) )
182+ if changes_timestamp_semantics ( & expr_type, field. data_type ( ) )
184183 || is_date_narrowing_cast ( & expr_type, field. data_type ( ) )
185184 {
186185 return false ;
@@ -205,6 +204,15 @@ pub(super) fn is_cast_expr_and_support_unwrap_cast_in_comparison_for_inlist(
205204 true
206205}
207206
207+ fn changes_timestamp_semantics ( from_type : & DataType , to_type : & DataType ) -> bool {
208+ // Changing units can truncate or overflow; attaching a timezone can shift values.
209+ matches ! (
210+ ( from_type, to_type) ,
211+ ( DataType :: Timestamp ( from_unit, from_tz) , DataType :: Timestamp ( to_unit, to_tz) )
212+ if from_unit != to_unit || from_tz != to_tz
213+ )
214+ }
215+
208216///// Tries to move a cast from an expression (such as column) to the literal other side of a comparison operator./
209217///
210218/// Specifically, rewrites
@@ -598,15 +606,57 @@ mod tests {
598606 }
599607
600608 #[ test]
601- /// Basic integration test for unwrapping casts with different timezones
602- fn test_unwrap_cast_with_timestamp_nanos ( ) {
609+ fn test_not_unwrap_cast_with_timestamp_timezone_change ( ) {
610+ use arrow:: array:: { Array , TimestampNanosecondArray } ;
611+ use arrow:: compute:: { CastOptions , cast_with_options} ;
612+
613+ let offset: Arc < str > = Arc :: from ( "+01:00" ) ;
614+ let offset_type =
615+ DataType :: Timestamp ( TimeUnit :: Nanosecond , Some ( Arc :: clone ( & offset) ) ) ;
616+ let source = TimestampNanosecondArray :: from ( vec ! [ Some ( 0 ) ] ) ;
617+ let shifted = cast_with_options (
618+ & source,
619+ & offset_type,
620+ & CastOptions {
621+ safe : false ,
622+ ..Default :: default ( )
623+ } ,
624+ )
625+ . unwrap ( ) ;
626+ let shifted = shifted
627+ . as_any ( )
628+ . downcast_ref :: < TimestampNanosecondArray > ( )
629+ . unwrap ( ) ;
630+ assert_ne ! ( shifted. value( 0 ) , 0 ) ;
631+
603632 let schema = expr_test_schema ( ) ;
604- // cast(ts_nano as Timestamp(Nanosecond, UTC)) < 1666612093000000000::Timestamp(Nanosecond, Utc))
605- let expr_lt = try_cast ( col ( "ts_nano_none" ) , timestamp_nano_utc_type ( ) )
606- . lt ( lit_timestamp_nano_utc ( 1666612093000000000 ) ) ;
633+ let widened = try_cast ( col ( "ts_nano_none" ) , offset_type) ;
634+ let offset_literal = |value| {
635+ lit ( ScalarValue :: TimestampNanosecond (
636+ Some ( value) ,
637+ Some ( Arc :: clone ( & offset) ) ,
638+ ) )
639+ } ;
640+ let equality = widened. clone ( ) . eq ( offset_literal ( 0 ) ) ;
641+ assert_eq ! ( optimize_test( equality. clone( ) , & schema) , equality) ;
642+
643+ let list = in_list (
644+ widened,
645+ vec ! [
646+ offset_literal( 0 ) ,
647+ offset_literal( 1 ) ,
648+ offset_literal( 2 ) ,
649+ offset_literal( 3 ) ,
650+ ] ,
651+ false ,
652+ ) ;
653+ assert_eq ! ( optimize_test( list. clone( ) , & schema) , list) ;
654+
655+ let unchanged = try_cast ( col ( "ts_nano_utf" ) , timestamp_nano_utc_type ( ) )
656+ . lt ( lit_timestamp_nano_utc ( 1_666_612_093_000_000_000 ) ) ;
607657 let expected =
608- col ( "ts_nano_none " ) . lt ( lit_timestamp_nano_none ( 1666612093000000000 ) ) ;
609- assert_eq ! ( optimize_test( expr_lt , & schema) , expected) ;
658+ col ( "ts_nano_utf " ) . lt ( lit_timestamp_nano_utc ( 1_666_612_093_000_000_000 ) ) ;
659+ assert_eq ! ( optimize_test( unchanged , & schema) , expected) ;
610660 }
611661
612662 #[ test]
@@ -619,13 +669,102 @@ mod tests {
619669 }
620670
621671 #[ test]
622- fn test_unwrap_cast_timestamp_precision_widening ( ) {
672+ fn test_not_unwrap_cast_timestamp_precision_widening ( ) {
623673 let schema = expr_test_schema ( ) ;
624674 let expr_input = cast ( col ( "ts_millis_none" ) , timestamp_nano_none_type ( ) )
625675 . eq ( lit_timestamp_nano_none ( 1_000_000 ) ) ;
626- let expected = col ( "ts_millis_none" ) . eq ( lit_timestamp_millis_none ( 1 ) ) ;
676+ assert_eq ! ( optimize_test ( expr_input . clone ( ) , & schema ) , expr_input ) ;
627677
628- assert_eq ! ( optimize_test( expr_input, & schema) , expected) ;
678+ let exact_list = in_list (
679+ cast ( col ( "ts_millis_none" ) , timestamp_nano_none_type ( ) ) ,
680+ vec ! [
681+ lit_timestamp_nano_none( 1_000_000 ) ,
682+ lit_timestamp_nano_none( 2_000_000 ) ,
683+ lit_timestamp_nano_none( 3_000_000 ) ,
684+ lit_timestamp_nano_none( 4_000_000 ) ,
685+ ] ,
686+ false ,
687+ ) ;
688+ assert_eq ! ( optimize_test( exact_list. clone( ) , & schema) , exact_list) ;
689+ }
690+
691+ #[ test]
692+ fn test_not_unwrap_try_cast_timestamp_widening_that_can_overflow ( ) {
693+ use arrow:: array:: { Array , TimestampMillisecondArray } ;
694+ use arrow:: compute:: { CastOptions , cast_with_options} ;
695+
696+ let overflow_ms = i64:: MAX / 1_000_000 + 1 ;
697+ let values = TimestampMillisecondArray :: from ( vec ! [ Some ( overflow_ms) ] ) ;
698+ assert ! (
699+ cast_with_options(
700+ & values,
701+ & timestamp_nano_none_type( ) ,
702+ & CastOptions {
703+ safe: false ,
704+ ..Default :: default ( )
705+ } ,
706+ )
707+ . is_err( )
708+ ) ;
709+ let safe_cast = cast_with_options (
710+ & values,
711+ & timestamp_nano_none_type ( ) ,
712+ & CastOptions {
713+ safe : true ,
714+ ..Default :: default ( )
715+ } ,
716+ )
717+ . unwrap ( ) ;
718+ assert ! ( safe_cast. is_null( 0 ) ) ;
719+
720+ let schema = expr_test_schema ( ) ;
721+ let widened = try_cast ( col ( "ts_millis_none" ) , timestamp_nano_none_type ( ) ) ;
722+ let equality = widened. clone ( ) . eq ( lit_timestamp_nano_none ( 1_000_000 ) ) ;
723+ assert_eq ! ( optimize_test( equality. clone( ) , & schema) , equality) ;
724+
725+ let range = widened. clone ( ) . gt ( lit_timestamp_nano_none ( 0 ) ) ;
726+ assert_eq ! ( optimize_test( range. clone( ) , & schema) , range) ;
727+
728+ let not_in = in_list (
729+ widened,
730+ vec ! [
731+ lit_timestamp_nano_none( 1_000_000 ) ,
732+ lit_timestamp_nano_none( 2_000_000 ) ,
733+ lit_timestamp_nano_none( 3_000_000 ) ,
734+ lit_timestamp_nano_none( 4_000_000 ) ,
735+ ] ,
736+ true ,
737+ ) ;
738+ assert_eq ! ( optimize_test( not_in. clone( ) , & schema) , not_in) ;
739+ }
740+
741+ #[ test]
742+ fn test_not_unwrap_timestamp_precision_widening_with_inexact_literal ( ) {
743+ let schema = expr_test_schema ( ) ;
744+ let column = cast ( col ( "ts_millis_none" ) , timestamp_nano_none_type ( ) ) ;
745+
746+ for literal in [ 1_000_001 , -1 ] {
747+ let equality = column. clone ( ) . eq ( lit_timestamp_nano_none ( literal) ) ;
748+ assert_eq ! ( optimize_test( equality. clone( ) , & schema) , equality) ;
749+
750+ let reversed = lit_timestamp_nano_none ( literal) . eq ( column. clone ( ) ) ;
751+ assert_eq ! ( optimize_test( reversed. clone( ) , & schema) , reversed) ;
752+
753+ let range = column. clone ( ) . lt ( lit_timestamp_nano_none ( literal) ) ;
754+ assert_eq ! ( optimize_test( range. clone( ) , & schema) , range) ;
755+ }
756+
757+ let inexact_list = in_list (
758+ column,
759+ vec ! [
760+ lit_timestamp_nano_none( 1_000_000 ) ,
761+ lit_timestamp_nano_none( 2_000_001 ) ,
762+ lit_timestamp_nano_none( 3_000_000 ) ,
763+ lit_timestamp_nano_none( 4_000_000 ) ,
764+ ] ,
765+ false ,
766+ ) ;
767+ assert_eq ! ( optimize_test( inexact_list. clone( ) , & schema) , inexact_list) ;
629768 }
630769
631770 fn optimize_test ( expr : Expr , schema : & DFSchemaRef ) -> Expr {
0 commit comments