Repository navigation
Conversation
|
This PR looks like a very nice solution for the cast pattern. I'm comfortable proceeding with it, but please forgive me for briefly advocating an alternative approach (that I'm to happy to help reviewing or implementing): I believe the fundamental goal here is to enable pruning through nested expressions, and the propagation based approach could be a better long term solution. My concern with the preimage approach is that it requires introducing and maintaining an ever-growing set of reverse-transformation rules. Even with additional rules, there will likely still be cases that cannot be handled. If this becomes a supported pattern, I worry that the long-term maintenance burden could be significant. In contrast, the propagation approach seems both more general and easier to reason about. The key intuition is that it follows a forward-evaluation model, similar to normal expression evaluation, whereas the preimage approach attempts to reverse complex expressions back into a simpler form. In many cases, the latter is inherently more difficult and may require expression-specific logic. |
|
I think this idea shows promise -- I will review it more carefully shortly |
a292aab to
17d45ee
Compare
|
Hi @alamb, quick update: I rebased this PR onto latest main and all CI checks are green now. No rush, but when you get a chance I'd appreciate your review. |
|
Thank you -- I will try and review it shorlty. |
17d45ee to
574c724
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #22906 +/- ##
==========================================
+ Coverage 82.73% 82.75% +0.02%
==========================================
Files 1147 1147
Lines 449509 450471 +962
Branches 449509 450471 +962
==========================================
+ Hits 371893 372790 +897
- Misses 54944 54988 +44
- Partials 22672 22693 +21 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
574c724 to
afe8c38
Compare
|
One scope question before review: the latest push includes the ordered The motivating shape appears after mixed timestamp coercion, for example: The non-aligned literal has no singleton equality preimage, so equality and More generally, for widening ratio
The implementation uses Euclidean There is an important policy caveat: for extreme source values where widening So I see two reasonable choices:
I am happy to keep the latest commits or split them back out, depending on what |
3d87cdb to
70fa177
Compare
Signed-off-by: discord9 <discord9@163.com>
Signed-off-by: discord9 <discord9@163.com>
Signed-off-by: discord9 <discord9@163.com>
Signed-off-by: discord9 <discord9@163.com>
Signed-off-by: discord9 <discord9@163.com>
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
70fa177 to
0835709
Compare
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @discord9 , I left some suggestions
| ) | ||
| } | ||
|
|
||
| fn maybe_range_preimage( |
There was a problem hiding this comment.
Casting a timezone-naive timestamp to a timezone-aware one isn't a pure unit change: arrow-cast calls adjust_timestamp_to_timezone for (None, Some(tz)), which reads the stored value as local time and shifts it to UTC. timestamp_narrowing_range_preimage builds the bucket from the raw literal and ignores this, so the range is off by the zone offset. On main this query wasn't rewritten. The exact path (is_exact_cast_safe, same-unit and widening) has the same hole, and #22142 lists it as "timezone silently dropped".
create table t(ts timestamp, l timestamp) as values
('2024-01-01T00:00:00.5'::timestamp, '2024-01-01T00:00:00.5'::timestamp);
-- literal moved into a column, no rewrite: 1
select count(*) from t where arrow_cast(ts, 'Timestamp(Millisecond, Some("+07:00"))')
= arrow_cast(l, 'Timestamp(Millisecond, Some("+07:00"))');
-- literal, rewritten to `ts >= 1704042000500000000 AND ts < 1704042000501000000`: 0
select count(*) from t where arrow_cast(ts, 'Timestamp(Millisecond, Some("+07:00"))')
= arrow_cast('2024-01-01T00:00:00.5'::timestamp, 'Timestamp(Millisecond, Some("+07:00"))');Fix: reject this timezone pair in both places (the second check also covers the IN-list path):
/// Arrow shifts values when casting a timezone-naive timestamp to a
/// timezone-aware one (local wall clock -> UTC), so the cast has no
/// literal-independent preimage.
fn is_naive_to_tz_timestamp_cast(source_type: &DataType, target_type: &DataType) -> bool {
matches!(
(source_type, target_type),
(DataType::Timestamp(_, None), DataType::Timestamp(_, Some(_)))
)
} pub fn cast_predicate_preimage(
@@
) -> Result<Option<CastPredicatePreimage>> {
+ if is_naive_to_tz_timestamp_cast(source_type, target_type) {
+ return Ok(None);
+ }
if let Some(preimage) = maybe_range_preimage(source_type, target_type, lit_value)? {
@@ fn is_exact_cast_safe
if is_timestamp_cast(src, tgt) {
- return !is_timestamp_precision_narrowing_cast(src, tgt);
+ return !is_timestamp_precision_narrowing_cast(src, tgt)
+ && !is_naive_to_tz_timestamp_cast(src, tgt);
}Please add a result-level slt for this case, not just EXPLAIN. With the fix, optimizer_ine) -> ms("UTC")) goes back to keeping the cast, unless you explicitly allow zero-offset"UTC"/"+00:00".
| @@ -156,16 +654,6 @@ pub fn is_timestamp_precision_narrowing_cast( | |||
| pub fn is_date_narrowing_cast(from_type: &DataType, to_type: &DataType) -> bool { | |||
There was a problem hiding this comment.
The is_date_narrowing_cast early returns here, at casts.rs:159 and at physical-expr/src/simplifier/unwrap_cast.rs:134 are dead code. is_exact_cast_safe already rejects Date64 -> Date32 (including the dictionary-wrapped form, which this bare-type check misses), and the range, int→string and widening paths can't match date types. Dropping all three leaves the allowlist as the single gate:
- if is_date_narrowing_cast(source_type, target_type) {
- return Ok(None);
- }| Operator::Or, | ||
| is_null(expr)?, | ||
| ), | ||
| _ => unreachable!("preimage only supports comparison operators"), |
There was a problem hiding this comment.
| _ => unreachable!("preimage only supports comparison operators"), | |
| _ => return internal_err!("Expect comparison operators, got {op}"), |
Merge Apache main at 9547b09 without rewriting the reviewed PR history. Reject naive-to-timezone-aware timestamp preimages in both comparison and direct exact-conversion paths, retaining Arrow's timezone adjustment. Remove redundant Date64 narrowing guards and return an internal error for unsupported physical comparison operators. Add result-level coverage for equal-unit, widening, narrowing and multi-item IN predicates. Correct the existing cross-timezone equality expectations using raw-instant and column-versus-column controls, while preserving a true matching positive case. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
Merge Apache main at 97c7593 after publishing the reviewed cast-predicate follow-up. Preserve the existing review history and timezone-aware cast safeguards without rebasing. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @discord9 , overall LGTM!
| /// This pair is already rejected by the `is_exact_cast_safe` allowlist | ||
| /// (including when wrapped in a `Dictionary`), so predicate rewrites do not need | ||
| /// a separate `Date64 -> Date32` early return. | ||
| pub fn is_date_narrowing_cast(from_type: &DataType, to_type: &DataType) -> bool { |
There was a problem hiding this comment.
| pub fn is_date_narrowing_cast(from_type: &DataType, to_type: &DataType) -> bool { | |
| #[deprecated( | |
| since = "56.0.0", | |
| note = "Date64 -> Date32 is rejected by the cast_predicate_preimage allowlist" | |
| )] | |
| pub fn is_date_narrowing_cast(from_type: &DataType, to_type: &DataType) -> bool { |
| if preimage.is_none() { | ||
| return false; | ||
| } | ||
| // Equality-like range predicates duplicate their input; volatile expressions | ||
| // cannot be duplicated. | ||
| !(matches!(preimage, Some(CastPredicatePreimage::Range(_))) | ||
| && matches!( | ||
| op, | ||
| Operator::Eq | ||
| | Operator::NotEq | ||
| | Operator::IsDistinctFrom | ||
| | Operator::IsNotDistinctFrom | ||
| ) | ||
| && inner_expr.is_volatile()) |
There was a problem hiding this comment.
| if preimage.is_none() { | |
| return false; | |
| } | |
| // Equality-like range predicates duplicate their input; volatile expressions | |
| // cannot be duplicated. | |
| !(matches!(preimage, Some(CastPredicatePreimage::Range(_))) | |
| && matches!( | |
| op, | |
| Operator::Eq | |
| | Operator::NotEq | |
| | Operator::IsDistinctFrom | |
| | Operator::IsNotDistinctFrom | |
| ) | |
| && inner_expr.is_volatile()) | |
| // Volatile expressions cannot be duplicated. | |
| preimage.is_some_and(|p| !(p.duplicates_input(op) && inner_expr.is_volatile())) |
Deprecate the retained date-narrowing helper for 56.0.0 and keep its existing behavior covered. Extract input-duplication metadata onto CastPredicatePreimage and use it in the logical volatility guard. Preserve existing cast comparison, timezone and NULL semantics without changing physical rewrite logic or the supported cast allowlist. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Which issue does this PR close?
Rationale for this change
The previous cast-unwrap path could only move the original comparison operator
from
CAST(expr AS target_type) OP literaltoexpr OP casted_literal. That isnot correct for many-to-one casts such as timestamp precision narrowing, where
the source-domain preimage of one target value is a range rather than a
singleton.
For example,
CAST(ts_ns AS Timestamp(ms)) > 1000msmust not becomets_ns > 1_000_000_000ns; its exact source boundary ists_ns >= 1_001_000_000ns.Timestamp precision widening has a related ordered-comparison case. A
non-aligned target literal has no singleton equality preimage, but it does have
an exact source-unit boundary for an ordered predicate. For example:
becomes:
This PR also makes exact cast rewrites closed-by-default: exact rewrites require
a supported value-preserving cast family. Many-to-one or source-domain-reducing
casts either use an explicit range/boundary preimage or remain unchanged.
The ordered timestamp-widening rewrite deliberately follows the existing
widening policy used by this work. At extreme source values where widening
overflows, ordinary
CASTcan error andTRY_CASTcan returnNULL, while therewritten source-unit comparison returns a Boolean. This is not a claim of
full-domain equivalence for those overflow cases; a guarded/error-aware
preimage representation is outside this PR's scope.
What changes are included in this PR?
CastPredicatePreimageabstraction indatafusion-expr-common:Exact(ScalarValue)for a same-operator source literal or boundary.Range(Interval)for a half-open source-domain interval.exact, and ordered timestamp-widening paths.
with truncation-toward-zero semantics, including negative timestamps.
floor/ceil arithmetic in
i128:>= Land< Luseceil(L / q).> Land<= Lusefloor(L / q).metadata, including
CAST,TRY_CAST, and literal-left comparisons.INpredicates unchanged.INrewrites because its preimageis a range rather than a singleton.
signedness, decimal precision/scale, and canonical integer/string checks.
and update the physical simplifier to use the same helper.
Behavior changes compared to
mainCAST(c1:Int32 AS Int64) < 10c1 < Int32(10)CAST(c2:Int64 AS Int32) = 5CAST(c1:Int32 AS UInt32) = 5CAST(c1:Int32 AS Utf8) = '123'c1 = Int32(123)CAST(c1:Int32 AS Utf8) = '0123''0123' -> 123 -> '123'does not round-trip.CAST(c1:Int32 AS Utf8) < '123'CAST(c1:Int32 AS Decimal(12,2)) = 123.00c1 = Int32(123)CAST(c1:Int32 AS Decimal(10,2)) = 123.00CAST(c3:Decimal(10,2) AS Decimal(18,4)) = 123.0000CAST(c3:Decimal(18,2) AS Decimal(18,1)) = 123.0CAST(ts_ns AS timestamp(ms)) = 1000msts_ns >= 1_000_000_000ns AND ts_ns < 1_001_000_000nsCAST(ts_ns AS timestamp(ms)) > 1000msts_ns >= 1_001_000_000nsCAST(ts_ns AS timestamp(ms)) <= 0msts_ns < 1_000_000nsCAST(ts_ns AS timestamp(ms)) = -1msts_ns >= -1_999_999ns AND ts_ns < -999_999nsCAST(ts_ns AS timestamp(ms)) != 1000msCAST(ts_ns AS timestamp(ms)) IN (1000ms)INcurrently supports only singleton exact preimages.CAST(ts_ms AS timestamp(ns)) = 123_000_000nsts_ms = 123msCAST(ts_ms AS timestamp(ns)) = 123_456_789nsCAST(ts_ms AS timestamp(ns)) >= 123_456_789nsts_ms >= 124ms123_456_789ns < TRY_CAST(ts_ms AS timestamp(ns))ts_ms > 123msCAST(Date64_col AS Date32) = ...CAST(Date32_col AS Date64) = aligned_midnightAre these changes tested?
Yes. Tests cover:
CAST,TRY_CAST, and literal-left forms,i64boundary literals,INremaining unchanged where required,Validated locally with:
Are there any user-facing changes?
There are no public API changes. Optimized plans may now use exact source-domain
ranges or boundaries for cast predicates, and previously unsafe exact rewrites
may remain unchanged. Ordered timestamp-widening comparisons also follow the
explicit overflow policy described above.