Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions datafusion/expr-common/src/casts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,19 @@ fn is_date_type(data_type: &DataType) -> bool {
/// For example, `CAST(ts AS DATE) = DATE '2024-01-01'` means "any timestamp
/// during that day", but unwrapping it to `ts = TIMESTAMP '2024-01-01
/// 00:00:00'` matches only midnight.
///
/// An identity cast (`from_type == to_type`, e.g. `Date32 -> Date32`) never
/// changes comparison semantics and is therefore not lossy. This has to be
/// handled explicitly because `DataType::is_temporal()` is true for both
/// `Date32` and `Date64`, so `is_date_type(from) && to.is_temporal()` would
/// otherwise report an identity `Date -> Date` cast as lossy and block the
/// rewrite. Note this is deliberately limited to *identical* types: a genuine
/// `Date32 <-> Date64` cast changes units (days vs milliseconds) and must
/// still be treated as lossy here.
fn is_lossy_temporal_cast(from_type: &DataType, to_type: &DataType) -> bool {
if from_type == to_type {
return false;
}
(is_date_type(from_type) && to_type.is_temporal())
|| (is_date_type(to_type) && from_type.is_temporal())
}
Expand Down Expand Up @@ -760,6 +772,57 @@ mod tests {
);
}

#[test]
fn test_try_cast_identity_date_allowed() {
// An identity Date cast (e.g. `CAST(date_col AS DATE)` where the column
// is already Date32) must fold: it never changes comparison semantics,
// so `try_cast_literal_to_type` should return the same value rather than
// treating it as a lossy temporal cast.
expect_cast(
ScalarValue::Date32(Some(19_723)),
DataType::Date32,
ExpectedCast::Value(ScalarValue::Date32(Some(19_723))),
);

expect_cast(
ScalarValue::Date64(Some(1_704_067_200_000)),
DataType::Date64,
ExpectedCast::Value(ScalarValue::Date64(Some(1_704_067_200_000))),
);

// is_lossy_temporal_cast must classify an identity cast as non-lossy.
assert!(!is_lossy_temporal_cast(
&DataType::Date32,
&DataType::Date32
));
assert!(!is_lossy_temporal_cast(
&DataType::Date64,
&DataType::Date64
));
}

#[test]
fn test_try_cast_date32_date64_still_blocked() {
// `Date32` counts days and `Date64` counts milliseconds, but
// try_cast_numeric_literal uses mul = 1 for both, so a cross cast would
// convert units wrongly. The identity short-circuit must NOT open this
// up: Date32 <-> Date64 has to stay blocked.
assert!(is_lossy_temporal_cast(&DataType::Date32, &DataType::Date64));
assert!(is_lossy_temporal_cast(&DataType::Date64, &DataType::Date32));

expect_cast(
ScalarValue::Date32(Some(1)),
DataType::Date64,
ExpectedCast::NoValue,
);

expect_cast(
ScalarValue::Date64(Some(86_400_000)),
DataType::Date32,
ExpectedCast::NoValue,
);
}

#[test]
fn test_try_cast_to_type_unsupported() {
// int64 to list
Expand Down
13 changes: 13 additions & 0 deletions datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,18 @@ mod tests {
assert_eq!(optimize_test(expr_lt, &schema), expected);
}

#[test]
/// An identity `CAST(date_col AS DATE)` on a Date32 column must fold away so
/// the comparison is against the bare column, e.g. `date_col = DATE '...'`.
fn test_unwrap_identity_date_cast() {
let schema = expr_test_schema();
// cast(date32_col as Date32) = Date32(19723) -> date32_col = Date32(19723)
let lit_date = lit(ScalarValue::Date32(Some(19_723)));
let expr_eq = cast(col("date32_col"), DataType::Date32).eq(lit_date.clone());
let expected = col("date32_col").eq(lit_date);
assert_eq!(optimize_test(expr_eq, &schema), expected);
}

fn optimize_test(expr: Expr, schema: &DFSchemaRef) -> Expr {
let simplifier = ExprSimplifier::new(
SimplifyContext::builder()
Expand All @@ -608,6 +620,7 @@ mod tests {
Field::new("c6", DataType::UInt32, false),
Field::new("ts_nano_none", timestamp_nano_none_type(), false),
Field::new("ts_nano_utf", timestamp_nano_utc_type(), false),
Field::new("date32_col", DataType::Date32, false),
Field::new("str1", DataType::Utf8, false),
Field::new("largestr", DataType::LargeUtf8, false),
Field::new("tag", dictionary_tag_type(), false),
Expand Down
Loading