Skip to content

Commit 2ab6092

Browse files
committed
catch errors when simplifying cast(lit(...), ...) and bubble those up
1 parent f57da83 commit 2ab6092

File tree

1 file changed

+62
-1
lines changed

1 file changed

+62
-1
lines changed

datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,18 @@ impl TreeNodeRewriter for ConstEvaluator<'_> {
571571
ConstSimplifyResult::NotSimplified(s, m) => {
572572
Ok(Transformed::no(Expr::Literal(s, m)))
573573
}
574-
ConstSimplifyResult::SimplifyRuntimeError(_, expr) => {
574+
ConstSimplifyResult::SimplifyRuntimeError(err, expr) => {
575+
// For CAST expressions with literal inputs, propagate the error at plan time rather than deferring to execution time.
576+
// This provides clearer error messages and fails fast.
577+
if let Expr::Cast(Cast { ref expr, .. })
578+
| Expr::TryCast(TryCast { ref expr, .. }) = expr
579+
{
580+
if matches!(expr.as_ref(), Expr::Literal(_, _)) {
581+
return Err(err);
582+
}
583+
}
584+
// For other expressions (like CASE, COALESCE), preserve the original
585+
// to allow short-circuit evaluation at execution time
575586
Ok(Transformed::yes(expr))
576587
}
577588
},
@@ -4968,6 +4979,56 @@ mod tests {
49684979
);
49694980
}
49704981

4982+
#[test]
4983+
fn simplify_cast_literal() {
4984+
// Test that CAST(literal) expressions are evaluated at plan time
4985+
4986+
// CAST(123 AS Int64) should become 123i64
4987+
let expr = Expr::Cast(Cast::new(Box::new(lit(123i32)), DataType::Int64));
4988+
let expected = lit(123i64);
4989+
assert_eq!(simplify(expr), expected);
4990+
4991+
// CAST(1761630189642 AS Timestamp(Nanosecond, Some("+00:00")))
4992+
// Integer to timestamp cast
4993+
let expr = Expr::Cast(Cast::new(
4994+
Box::new(lit(1761630189642i64)),
4995+
DataType::Timestamp(
4996+
arrow::datatypes::TimeUnit::Nanosecond,
4997+
Some("+00:00".into()),
4998+
),
4999+
));
5000+
// Should evaluate to a timestamp literal
5001+
let result = simplify(expr);
5002+
match result {
5003+
Expr::Literal(ScalarValue::TimestampNanosecond(Some(val), tz), _) => {
5004+
assert_eq!(val, 1761630189642i64);
5005+
assert_eq!(tz.as_deref(), Some("+00:00"));
5006+
}
5007+
other => panic!("Expected TimestampNanosecond literal, got: {:?}", other),
5008+
}
5009+
5010+
// Test CAST of invalid string to timestamp - should return an error at plan time
5011+
// This represents the case from the issue: CAST(Utf8("1761630189642") AS Timestamp)
5012+
// "1761630189642" is NOT a valid timestamp string format
5013+
let expr = Expr::Cast(Cast::new(
5014+
Box::new(lit("1761630189642")),
5015+
DataType::Timestamp(
5016+
arrow::datatypes::TimeUnit::Nanosecond,
5017+
Some("+00:00".into()),
5018+
),
5019+
));
5020+
5021+
// The simplification should now fail with an error at plan time
5022+
let schema = test_schema();
5023+
let props = ExecutionProps::new();
5024+
let simplifier =
5025+
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(schema));
5026+
let result = simplifier.simplify(expr);
5027+
assert!(result.is_err(), "Expected error for invalid cast");
5028+
let err_msg = result.unwrap_err().to_string();
5029+
assert_contains!(err_msg, "Error parsing timestamp");
5030+
}
5031+
49715032
fn if_not_null(expr: Expr, then: bool) -> Expr {
49725033
Expr::Case(Case {
49735034
expr: Some(expr.is_not_null().into()),

0 commit comments

Comments
 (0)