mbutrovich commented on code in PR #6447:
URL: https://github.com/apache/datafusion-comet/pull/6447#discussion_r4190054555
##########
native/spark-expr/src/array_funcs/nested_comparison.rs:
##########
@@ -219,31 +217,83 @@ impl PhysicalExpr for NestedPredicate {
}
}
-/// Build equality after the planner has reconciled nested operand nullability.
+/// How [`spark_comparison`] treats floating-point operands.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FloatOperands {
+ /// Normalize them, so that the comparison follows Spark's SQL ordering.
+ Normalize,
+ /// Leave a Float32 or Float64 column compared with a literal as it is,
and normalize every
+ /// other operand. Only a scan's pushed-down data filters use this, and
only when the Parquet
+ /// reader prunes with them but does not filter rows: Parquet pruning
recognizes a column
+ /// compared with a literal but not a normalized column, and Spark's
Filter above the scan
+ /// applies Spark's semantics to every row. With row-level pushdown the
reader would drop the
+ /// rows such a comparison rejects, including a stored NaN whose bits
differ from the
+ /// normalized literal, so the data filters use
[`FloatOperands::Normalize`] there instead.
+ Raw,
+}
+
+/// Builds a comparison with Spark's SQL ordering for floats, in which `-0.0`
equals `0.0`, all
+/// NaNs are equal and NaN sorts above every other value, at any depth of a
list or struct.
+///
+/// Arrow compares floats by IEEE 754 total order instead, so each float
operand is normalized
+/// first with [`normalize_comparison_operand`], after which the two orders
agree. Nested `=` and
+/// `<>` compare with `spark_equality` instead, without building normalized
copies of the nested
+/// values. Any other operator, such as `AND`, builds a plain [`BinaryExpr`].
+///
+/// The planner reconciles the nullability of nested operands before calling
this.
pub fn spark_comparison(
left: Arc<dyn PhysicalExpr>,
op: Operator,
right: Arc<dyn PhysicalExpr>,
schema: &Schema,
+ float_operands: FloatOperands,
) -> Result<Arc<dyn PhysicalExpr>> {
+ use Operator::*;
+ if !matches!(
+ op,
+ Eq | NotEq | Lt | LtEq | Gt | GtEq | IsDistinctFrom | IsNotDistinctFrom
+ ) {
+ return Ok(Arc::new(BinaryExpr::new(left, op, right)));
+ }
// An operand whose type does not resolve against this schema falls back
to the plain
// comparison, the way `reconcile_nested_comparison_types` already leaves
such operands alone.
- let nested = matches!(op, Operator::Eq | Operator::NotEq)
- && match (left.data_type(schema), right.data_type(schema)) {
- (Ok(lt), Ok(_)) => needs_spark_equality(<),
- _ => false,
- };
- if nested {
+ let (Ok(left_type), Ok(_)) = (left.data_type(schema),
right.data_type(schema)) else {
+ return Ok(Arc::new(BinaryExpr::new(left, op, right)));
+ };
+ if matches!(op, Eq | NotEq) && is_nested_with_float_leaf(&left_type) {
validate_types(&left, std::slice::from_ref(&right), schema)?;
- Ok(Arc::new(NestedPredicate {
+ return Ok(Arc::new(NestedPredicate {
value: left,
candidates: vec![right],
- negated: op == Operator::NotEq,
+ negated: op == NotEq,
membership: false,
- }))
- } else {
- Ok(Arc::new(BinaryExpr::new(left, op, right)))
+ }));
}
+ let raw = float_operands == FloatOperands::Raw;
+ let (left, right) = if raw && is_float_column(&left, schema) &&
is_literal(&right) {
+ (left, normalize_comparison_operand(right, schema)?)
+ } else if raw && is_literal(&left) && is_float_column(&right, schema) {
+ (normalize_comparison_operand(left, schema)?, right)
Review Comment:
With `FloatOperands::Raw`, the literal is still normalized. Since 2f003c2,
`Raw` is used only when the reader prunes and doesn't filter rows. Parquet
bloom filter pruning probes the literal's exact bits
([`check_scalar`](https://github.com/apache/datafusion/blob/7d3835c71f30cbd3c3ae4041732267f1f453097a/datafusion/datasource-parquet/src/bloom_filter.rs#L102-L103)
hashes the `f64` as is). So `d = -0.0D` now probes for `0.0`.
What happens to a row group whose `d` values are all `-0.0`? I wrote a file
with arrow-rs, with a bloom filter on `d` and two `-0.0` rows, and scanned it
with the `scan_parquet_file` test helper in `schema_adapter.rs`. With main's
data filter (`d@0 = -0`) the scan returns both rows. With the filter
`spark_comparison(..., FloatOperands::Raw)` builds at 2f003c2 (`d@0 = 0`) it
returns none, with or without column statistics. The Filter above the scan
can't bring those rows back, and Spark matches them, since `-0.0 = -0.0`.
Should `Raw` leave a zero literal as it is, as main does, or expand it into
both signed zeros the way the `IN` path does for constant lists? Leaving it as
it is goes back to main's behavior, where a row group holding only `0.0` is
still pruned for `d = -0.0D`. Expanding it covers both cases. Could you also
add a test with a bloom filter on a `DOUBLE` column, for example in
`CometNativeReaderSuite` with `parquet.bloom.filter.enabled#d`, that checks
`WHERE d = -0.0D` returns the `-0.0` rows?
##########
docs/source/user-guide/latest/compatibility/floating-point.md:
##########
@@ -23,20 +23,34 @@ Spark normalizes NaN and zero for floating point numbers
for several cases. See
However, one exception is comparison. Spark does not normalize NaN and zero
when comparing values
because they are handled well in Spark (e.g.,
`SQLOrderingUtil.compareFloats`). But the comparison
functions of arrow-rs used by DataFusion do not normalize NaN and zero (e.g.,
[arrow::compute::kernels::cmp::eq](https://docs.rs/arrow/latest/arrow/compute/kernels/cmp/fn.eq.html#)).
-For top-level `FLOAT` and `DOUBLE` comparisons, Comet normalizes both operands
before native
-execution, including noncanonical NaN literals. Top-level `IN`, `InSet`, and
`NOT IN` membership
-also normalize dynamic candidates and lists containing NaN. When every
candidate is a non-NaN
-literal, Comet keeps DataFusion's static filter and pruning path, enumerating
both signed-zero
-forms when a list contains zero.
+For `FLOAT` and `DOUBLE` comparisons (`=`, `<>`, `<=>`, `<`, `<=`, `>` and
`>=`), Comet
+normalizes both operands before native execution, including noncanonical NaN
literals. This
+applies wherever a comparison appears: projections, filters, aggregate
arguments and `FILTER`
+clauses, join conditions, sort keys, and generator arguments.
+
+A native Parquet scan skips row groups with its data filters. So that
statistics pruning still
+applies, a `FLOAT` or `DOUBLE` column compared with a constant in a data
filter is compared
+without normalizing the column, and the filter above the scan evaluates the
comparison again with
+Spark's semantics. Every other comparison in a data filter is normalized. With
+`spark.comet.parquet.rowFilterPushdown.enabled=true` the scan also filters
rows with these
+comparisons, so a noncanonical NaN stored in the file, such as one with the
sign bit set, can be
+filtered differently from Spark when compared with a constant. Spark's Parquet
writer only writes
+canonical NaNs.
Review Comment:
This paragraph describes the data filters as they were before 2f003c2. As I
read `data_filter_float_operands`, with
`spark.comet.parquet.rowFilterPushdown.enabled=true` both operands are
normalized. A noncanonical NaN is no longer filtered differently in that mode,
and float comparisons stop pruning instead. What do you think about this
wording?
```suggestion
A native Parquet scan prunes row groups and pages with its data filters. So
that this pruning
still applies, a `FLOAT` or `DOUBLE` column compared with a constant in a
data filter is compared
without normalizing the column, and the filter above the scan evaluates the
comparison again with
Spark's semantics. Every other comparison in a data filter is normalized.
With
`spark.comet.parquet.rowFilterPushdown.enabled=true` the scan also drops the
rows its data filters
reject, so every comparison in a data filter is normalized, and a `FLOAT` or
`DOUBLE` comparison
in a data filter does not prune row groups or pages.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]