Repository navigation
Conversation
185d327 to
40840f8
Compare
There was a problem hiding this comment.
Given it's a correctness issue, removing it ASAP and improving in a follow up makes sense. Thanks for catching this.
Edit: I see #36 tracks the follow up
The old code didn't handle the literal side correctly either. It stripped the cast, so Should we merge this PR with the |
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks @colinmarc for tracking this down, and for sqllogictests that check query results and not only plans.
resolve_nan_preserving_reference still unwraps casts (predicate.rs#L278-L287), so the isnan path doesn't follow the rule in the title yet. On the head commit, SELECT id FROM t WHERE NOT isnan(CAST(s AS DOUBLE)) with s VARCHAR pushes predicate=NOT (s IS NAN) and the query fails with DataInvalid => Expecting floating point type, but found string. Could we drop that Expr::Cast arm in this PR too, with this query as a sqllogictest? #59 makes the same change.
| `IcebergTableScan` converts the projection to column names and the filters to a single Iceberg `Predicate` (`physical_plan/expr_to_predicate.rs`). It applies `limit` in-stream, has one output partition, and delegates the actual read to iceberg's `TableScan::to_arrow()`. | ||
|
|
||
| Both providers report every filter as `Inexact`, so DataFusion re-applies the original filters after the scan. The pushed-down predicate therefore only has to be *implied by* the original filter. It may match extra rows but must never drop a row that matches. Unconvertible parts are dropped: an `AND` keeps whichever side converted, while an `OR` needs both sides. Preserve this soundness rule when extending `expr_to_predicate.rs`; for example, it is why date casts and some NaN arithmetic are deliberately not pushed down. | ||
| Both providers report every filter as `Inexact`, so DataFusion re-applies the original filters after the scan. The pushed-down predicate therefore only has to be *implied by* the original filter. It may match extra rows but must never drop a row that matches. Unconvertible parts are dropped: an `AND` keeps whichever side converted, while an `OR` needs both sides. Preserve this soundness rule when extending `expr_to_predicate.rs`; for example, it is why casts and some NaN arithmetic are deliberately not pushed down. DataFusion's simplifier already unwraps the casts that can be removed without changing results, so any cast that reaches the scan is one that can't. |
There was a problem hiding this comment.
Could we reword the last sentence? This PR's own sqllogictest shows a removable cast reaching the scan: CAST(d AS Timestamp(ns)) < TimestampNanosecond(...) can be rewritten exactly on the date column, but DataFusion doesn't do that yet (apache/datafusion#26289). An agent reading "any cast that reaches the scan is one that can't" could take it as a fact to rely on.
| Both providers report every filter as `Inexact`, so DataFusion re-applies the original filters after the scan. The pushed-down predicate therefore only has to be *implied by* the original filter. It may match extra rows but must never drop a row that matches. Unconvertible parts are dropped: an `AND` keeps whichever side converted, while an `OR` needs both sides. Preserve this soundness rule when extending `expr_to_predicate.rs`; for example, it is why casts and some NaN arithmetic are deliberately not pushed down. DataFusion's simplifier already unwraps the casts that can be removed without changing results, so any cast that reaches the scan is one that can't. | |
| Both providers report every filter as `Inexact`, so DataFusion re-applies the original filters after the scan. The pushed-down predicate therefore only has to be *implied by* the original filter. It may match extra rows but must never drop a row that matches. Unconvertible parts are dropped: an `AND` keeps whichever side converted, while an `OR` needs both sides. Preserve this soundness rule when extending `expr_to_predicate.rs`; for example, it is why casts and some NaN arithmetic are deliberately not pushed down. Removing a cast is left to DataFusion's simplifier (`unwrap_cast_in_comparison`), which rewrites a comparison only when the result doesn't change. |
The purpose of `expr_to_predicate` is to take an expr and return a
valid, equivalent push-down predicate. The existing behavior of just
dropping the cast ignored many boundary conditions that would lead to
incorrectly excluding partitions.
There was already a carveout for dates, because that is the most obvious
example. The following expressions are obviously not equivalent:
-- x is a date, eg '2026-01-22'
arrow_cast(x, 'timestamp(ns)') < '2026-01-22T12:00'
-- After dropping the cast, iceberg coerces the literal, so the
-- resulting expr is:
x < '2026-01-22T12:00'
Whether it's a bug depends on the type of `x`; if x is a date, or
broadly coarser than the literal, then dropping the cast creates a
bad predicate (specifically: the partition '2026-01-22' would be
dropped, even though the original expression should keep it).
Once I started looking, though, I found a few more weird cases:
-- x is int32. '10' < '5' (lexicographically)
arrow_cast(x, 'utf8') < '5'
-- Drop the cast and Datum::to('5', int32) gives you:
x < 5
And here's another:
-- x is f32. 0.1f32 > 0.1f64 (0.100000001490116 > 0.100000000000000005)
arrow_cast(x, 'float64') > 0.1
-- Dropping the cast, plus coercion to f32:
x > 0.1f32
It seems like there might be more, any time two conditions are
fulfilled:
- The cast is to a coarser type
- The literal would be coerced in a lossy way by `Datum::to`
Fixes apache#36.
40840f8 to
7903c38
Compare
|
Thanks for the quick response! I think I addressed both points. I didn't add any special handling for a cast on the literal. |
|
datafusion-iceberg depends on iceberg-rust at rev |
I removed the timestamp -> date conversion from that PR, so I think it should be fine either way. |
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks @colinmarc for the quick turnaround on both points. This looks good to me.
The purpose of
expr_to_predicateis to take an expr and return a valid, equivalent push-down predicate. The existing behavior of just dropping the cast ignored many boundary conditions that would lead to incorrectly excluding partitions. This has a lot to do with the fact that, once a binary predicate with a literal on the right side is passed to iceberg, that literal gets coerced to the same type as the partition key.There was already a carveout for dates, because that is the most obvious example. The following expressions are obviously not equivalent:
Whether it's a bug depends on the type of
x; if x is a date, or broadly coarser than the literal, then dropping the cast creates a bad predicate (specifically: the partition '2026-01-22' would be dropped, even though the original expression should keep it).Once I started looking, though (using an LLM), I found a few more weird cases:
And here's another:
It seems like there might be more, any time two conditions are fulfilled:
Datum::toContext
I discovered this because with apache/iceberg-rust#3574, the following query goes from an error to silently dropping partitions:
However, I think that PR is correct, at least in mimicking iceberg-java behavior. It's just that there's also a latent bug here.
The bad news is that this locks us out of partition pushdown for some queries today. The good news is that there's lots of work upstream in DF to simplify casts out anyway. I think that's the right place to push the complexity.
You can read more about that work here:
apache/datafusion#19946
apache/datafusion#26289