fix: unwrap identity Date cast in comparison unwrapping#64
Closed
adriangb wants to merge 1 commit into
Closed
Conversation
`unwrap_cast_in_comparison` refused to fold an identity `CAST(col AS DATE)`
on a `Date32` column, leaving a residual `Cast(col AS Date32)` in the
predicate instead of comparing against the bare column. SQL `cast(col AS
date)` plants a real `Expr::Cast` with no identity elision (unlike the
`arrow_cast` UDF, whose `simplify()` short-circuits identity), so this cast
reached `try_cast_literal_to_type` and was rejected.
Root cause is in `is_lossy_temporal_cast`:
(is_date_type(from) && to.is_temporal()) || (is_date_type(to) && from.is_temporal())
For an identity `Date32 -> Date32` cast this is `true && true`, because
arrow's `DataType::is_temporal()` is true for `Date32`/`Date64`. The cast is
wrongly classified as a lossy temporal cast, `try_cast_literal_to_type`
returns `None`, and the residual cast is left in place.
Fix: short-circuit an identity cast (`from_type == to_type`) as non-lossy at
the top of `is_lossy_temporal_cast`. An identity cast can never change
comparison semantics.
This is deliberately scoped to *identical* types only. `Date32` counts days
and `Date64` counts milliseconds, but `try_cast_numeric_literal` uses
`mul = 1` for both, so a `Date32 <-> Date64` cast would convert units
wrongly and must stay blocked. A regression test asserts that.
Tests: identity `Date32 -> Date32` / `Date64 -> Date64` now fold;
`Date32 <-> Date64` still does not; plus an end-to-end simplifier test that
`cast(date_col AS DATE) = DATE '...'` simplifies to `date_col = <lit>`.
apache/datafusion `main` carries the same `is_lossy_temporal_cast`, so this
is also a candidate to upstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member
Author
|
Superseded by the upstream PR apache#23727 — this is a general upstream bug, so the canonical fix belongs in apache/datafusion. It will reach |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
unwrap_cast_in_comparisonrefuses to fold an identityCAST(col AS DATE)on aDate32column, leaving a residualCast(col AS Date32)in the predicate instead of comparing against the bare column. SQLcast(col AS date)plants a realExpr::Castwith no identity elision (unlike thearrow_castUDF, whosesimplify()short-circuits identity), so this cast reachestry_cast_literal_to_typeand is rejected. The residual cast defeats downstream pruning/pushdown that expects a bare column comparison.Root cause
In
datafusion/expr-common/src/casts.rs,is_lossy_temporal_cast:For an identity
Date32 -> Date32cast this istrue && true, because arrow'sDataType::is_temporal()is true for bothDate32andDate64. The identity cast is therefore misclassified as a lossy temporal cast,try_cast_literal_to_typereturnsNone, andunwrap_cast_in_comparisonleaves the cast in place.Fix
Short-circuit an identity cast as non-lossy at the top of
is_lossy_temporal_cast:Caveat:
Date32 <-> Date64stays blockedThis is deliberately scoped to identical types only, not "any date-to-date".
Date32counts days whileDate64counts milliseconds, buttry_cast_numeric_literalusesmul = 1for both, so allowing aDate32 <-> Date64unwrap would convert units wrongly. Thefrom_type == to_typeidentity guard is the exact correct scope; a regression test assertsDate32 <-> Date64still does not unwrap.Tests
test_try_cast_identity_date_allowed— identityDate32 -> Date32/Date64 -> Date64now fold (try_cast_literal_to_typereturnsSome), andis_lossy_temporal_castreports them non-lossy.test_try_cast_date32_date64_still_blocked—Date32 <-> Date64remains lossy/blocked.test_unwrap_identity_date_cast(optimizer) — end-to-end:cast(date_col AS DATE) = DATE '...'simplifies todate_col = <lit>.All
datafusion-expr-commonanddatafusion-optimizertests pass;cargo fmt+cargo clippy -D warningsclean on the touched crates.Upstream
apache/datafusion
maincarries the sameis_lossy_temporal_cast, so this bug exists upstream too and this is a candidate to port to apache/datafusion.🤖 Generated with Claude Code