Skip to content

Commit 81bf4c5

Browse files
authored
Merge pull request #128 from Embucket/fix-df55-timestamp-cast-unwrap
Preserve timestamp casts when unit or timezone changes
2 parents 5e2f790 + 963d3b1 commit 81bf4c5

1 file changed

Lines changed: 153 additions & 14 deletions

File tree

‎datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs‎

Lines changed: 153 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ use datafusion_common::{internal_err, tree_node::Transformed};
6060
use datafusion_expr::{BinaryExpr, lit};
6161
use datafusion_expr::{Cast, Expr, Operator, TryCast, simplify::SimplifyContext};
6262
use 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

6766
pub(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

Comments
 (0)