-
Notifications
You must be signed in to change notification settings - Fork 343
fix: normalize floating-point values in native collect_set #5166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,13 +22,14 @@ package org.apache.comet.serde | |
| import scala.jdk.CollectionConverters._ | ||
|
|
||
| import org.apache.spark.sql.catalyst.expressions.{Attribute, Cast, Literal} | ||
| import org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, ApproximatePercentile, Average, BitAndAgg, BitOrAgg, BitXorAgg, BloomFilterAggregate, CentralMomentAgg, CollectList, CollectSet, Corr, Count, Covariance, CovPopulation, CovSample, First, HyperLogLogPlusPlus, Last, Max, Min, Percentile, StddevPop, StddevSamp, Sum, VariancePop, VarianceSamp} | ||
| import org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, ApproximatePercentile, Average, BitAndAgg, BitOrAgg, BitXorAgg, BloomFilterAggregate, CentralMomentAgg, CollectList, CollectSet, Complete, Corr, Count, Covariance, CovPopulation, CovSample, First, HyperLogLogPlusPlus, Last, Max, Min, Partial, Percentile, StddevPop, StddevSamp, Sum, VariancePop, VarianceSamp} | ||
| import org.apache.spark.sql.catalyst.util.ArrayData | ||
| import org.apache.spark.sql.comet.CometExecUtils | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types.{BinaryType, BooleanType, ByteType, DataType, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, NumericType, ShortType, StringType, TimestampNTZType, TimestampType} | ||
|
|
||
| import org.apache.comet.CometConf.COMET_EXEC_STRICT_FLOATING_POINT | ||
| import org.apache.comet.CometSparkSessionExtensions.{isSpark41Plus, withFallbackReason} | ||
| import org.apache.comet.CometSparkSessionExtensions.{isSpark41Plus, isSpark42Plus, withFallbackReason} | ||
| import org.apache.comet.expressions.CometEvalMode | ||
| import org.apache.comet.serde.QueryPlanSerde.{evalModeToProto, exprToProto, serializeDataType} | ||
| import org.apache.comet.shims.{CometCollectShim, CometEvalModeUtil} | ||
|
|
@@ -830,11 +831,19 @@ object CometBloomFilterAggregate extends CometAggregateExpressionSerde[BloomFilt | |
|
|
||
| object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] { | ||
|
|
||
| override def getIncompatibleReasons(): Seq[String] = Seq( | ||
| "Comet deduplicates NaN values (treats `NaN == NaN`) while Spark treats each NaN as a" + | ||
| s" distinct value. When `${COMET_EXEC_STRICT_FLOATING_POINT.key}=true`, `collect_set`" + | ||
| " on floating-point types falls back to Spark unless" + | ||
| " `spark.comet.expression.CollectSet.allowIncompatible=true` is set.") | ||
| override def getIncompatibleReasons(): Seq[String] = { | ||
| if (isSpark42Plus) { | ||
| Nil | ||
| } else { | ||
| Seq( | ||
| "Before Spark 4.2, Comet deduplicates NaN values (treats `NaN == NaN`) while Spark" + | ||
| " treats each NaN as a distinct value. Comet treats -0.0 and 0.0 as distinct while" + | ||
| " Spark treats them as equal." + | ||
| s" When `${COMET_EXEC_STRICT_FLOATING_POINT.key}=true`, `collect_set` on" + | ||
| " floating-point types falls back to Spark on those versions unless" + | ||
| " `spark.comet.expression.CollectSet.allowIncompatible=true` is set.") | ||
| } | ||
| } | ||
|
|
||
| override def getSupportLevel(expr: CollectSet): SupportLevel = { | ||
| // The native path always drops null inputs. Spark 4.2 added an `ignoreNulls` field to | ||
|
|
@@ -844,12 +853,14 @@ object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] { | |
| // analysis time, and CometCollectShim.ignoreNulls hardcodes true, making this a no-op. | ||
| if (!CometCollectShim.ignoreNulls(expr)) { | ||
| Unsupported(Some("collect_set with RESPECT NULLS (ignoreNulls = false) is not supported")) | ||
| } else if (isSpark42Plus) { | ||
| Compatible() | ||
| } else { | ||
| SupportLevel | ||
| .strictFloatingPointReason( | ||
| expr.children.head.dataType, | ||
| "collect_set on floating-point types " + | ||
| "(Comet deduplicates NaN values while Spark treats each NaN as distinct)") | ||
| "(Comet deduplicates NaN values and distinguishes -0.0 from 0.0, unlike Spark)") | ||
| .map(reason => Incompatible(Some(reason))) | ||
| .getOrElse(Compatible()) | ||
| } | ||
|
|
@@ -861,7 +872,12 @@ object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] { | |
| inputs: Seq[Attribute], | ||
| binding: Boolean, | ||
| conf: SQLConf): Option[ExprOuterClass.AggExpr] = { | ||
| val child = expr.children.head | ||
| val child = aggExpr.mode match { | ||
| case Partial | Complete if isSpark42Plus => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you say a bit about why the normalization is gated on Normalizing NaN is a no-op for Comet on every version, since the native set already keys on The cost is that array-typed children would start paying the codegen dispatch from my other comment on the older versions as well, so I can see this going either way. If you would rather not take it on here, a tracking issue linked from this PR works. I would just like the reasoning captured somewhere rather than left implicit in the gate. |
||
| CometExecUtils.normalizeFloatingNumbers(expr.children.head) | ||
|
Comment on lines
+876
to
+877
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One consequence of reusing For any array-typed child it produces Two follow-ups. Could the PR description mention this so the execution-mode change is on the record? And would you file an issue to teach the native Worth noting in that issue that with |
||
| case _ => | ||
| expr.children.head | ||
| } | ||
| val childExpr = exprToProto(child, inputs, binding) | ||
| val dataType = serializeDataType(expr.dataType) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| -- Licensed to the Apache Software Foundation (ASF) under one | ||
| -- or more contributor license agreements. See the NOTICE file | ||
| -- distributed with this work for additional information | ||
| -- regarding copyright ownership. The ASF licenses this file | ||
| -- to you under the Apache License, Version 2.0 (the | ||
| -- "License"); you may not use this file except in compliance | ||
| -- with the License. You may obtain a copy of the License at | ||
| -- | ||
| -- http://www.apache.org/licenses/LICENSE-2.0 | ||
| -- | ||
| -- Unless required by applicable law or agreed to in writing, | ||
| -- software distributed under the License is distributed on an | ||
| -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| -- KIND, either express or implied. See the License for the | ||
| -- specific language governing permissions and limitations | ||
| -- under the License. | ||
|
|
||
| -- MaxSparkVersion: 4.1 | ||
| -- Config: spark.comet.exec.strictFloatingPoint=true | ||
|
|
||
| statement | ||
| CREATE TABLE cs_fallback_float(v float, grp string) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO cs_fallback_float VALUES | ||
| (1.5, 'a'), (2.5, 'a'), (1.5, 'a'), (NULL, 'a'), | ||
| (CAST('NaN' AS FLOAT), 'b'), (CAST('NaN' AS FLOAT), 'b'), (1.0, 'b'), | ||
| (CAST('Infinity' AS FLOAT), 'c'), (CAST('-Infinity' AS FLOAT), 'c'), | ||
| (CAST('Infinity' AS FLOAT), 'c'), | ||
| (CAST(0.0 AS FLOAT), 'd'), (CAST('-0.0' AS FLOAT), 'd'), (1.0, 'd'), (NULL, 'd') | ||
|
|
||
| query expect_fallback(not fully compatible with Spark) | ||
| SELECT grp, sort_array(collect_set(v)) | ||
| FROM cs_fallback_float GROUP BY grp ORDER BY grp | ||
|
|
||
| statement | ||
| CREATE TABLE cs_fallback_double(v double, grp string) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO cs_fallback_double VALUES | ||
| (1.1, 'a'), (2.2, 'a'), (1.1, 'a'), (NULL, 'a'), | ||
| (CAST('NaN' AS DOUBLE), 'b'), (CAST('NaN' AS DOUBLE), 'b'), (1.0, 'b'), | ||
| (CAST('Infinity' AS DOUBLE), 'c'), (CAST('-Infinity' AS DOUBLE), 'c'), | ||
| (CAST('Infinity' AS DOUBLE), 'c'), | ||
| (0.0, 'd'), (CAST('-0.0' AS DOUBLE), 'd'), (1.0, 'd'), (NULL, 'd') | ||
|
|
||
| query expect_fallback(not fully compatible with Spark) | ||
| SELECT grp, sort_array(collect_set(v)) | ||
| FROM cs_fallback_double GROUP BY grp ORDER BY grp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| -- Licensed to the Apache Software Foundation (ASF) under one | ||
| -- or more contributor license agreements. See the NOTICE file | ||
| -- distributed with this work for additional information | ||
| -- regarding copyright ownership. The ASF licenses this file | ||
| -- to you under the Apache License, Version 2.0 (the | ||
| -- "License"); you may not use this file except in compliance | ||
| -- with the License. You may obtain a copy of the License at | ||
| -- | ||
| -- http://www.apache.org/licenses/LICENSE-2.0 | ||
| -- | ||
| -- Unless required by applicable law or agreed to in writing, | ||
| -- software distributed under the License is distributed on an | ||
| -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| -- KIND, either express or implied. See the License for the | ||
| -- specific language governing permissions and limitations | ||
| -- under the License. | ||
|
|
||
| -- MinSparkVersion: 4.2 | ||
| -- Config: spark.comet.exec.strictFloatingPoint=true | ||
| -- ConfigMatrix: parquet.enable.dictionary=false,true | ||
|
|
||
| statement | ||
| CREATE TABLE cs_norm_scalar(grp string, f float, d double) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO cs_norm_scalar VALUES | ||
| ('ordinary', CAST(1.5 AS FLOAT), CAST(1.1 AS DOUBLE)), | ||
| ('ordinary', CAST(2.5 AS FLOAT), CAST(2.2 AS DOUBLE)), | ||
| ('ordinary', CAST(1.5 AS FLOAT), CAST(1.1 AS DOUBLE)), | ||
| ('ordinary', CAST(NULL AS FLOAT), CAST(NULL AS DOUBLE)), | ||
| ('nan', CAST('NaN' AS FLOAT), CAST('NaN' AS DOUBLE)), | ||
| ('nan', CAST('NaN' AS FLOAT), CAST('NaN' AS DOUBLE)), | ||
| ('nan', CAST(1.0 AS FLOAT), CAST(1.0 AS DOUBLE)), | ||
| ('infinity', CAST('Infinity' AS FLOAT), CAST('Infinity' AS DOUBLE)), | ||
| ('infinity', CAST('-Infinity' AS FLOAT), CAST('-Infinity' AS DOUBLE)), | ||
| ('infinity', CAST('Infinity' AS FLOAT), CAST('Infinity' AS DOUBLE)), | ||
| ('zero', CAST('-0.0' AS FLOAT), CAST('-0.0' AS DOUBLE)), | ||
| ('zero', CAST(0.0 AS FLOAT), CAST(0.0 AS DOUBLE)), | ||
| ('zero', CAST(1.0 AS FLOAT), CAST(1.0 AS DOUBLE)), | ||
| ('zero', CAST(NULL AS FLOAT), CAST(NULL AS DOUBLE)) | ||
|
|
||
| -- Repartition so equal values must also deduplicate across partial buffers. | ||
| query | ||
| SELECT grp, sort_array(collect_set(f)), sort_array(collect_set(d)) | ||
| FROM (SELECT /*+ REPARTITION(3) */ * FROM cs_norm_scalar) | ||
| GROUP BY grp | ||
| ORDER BY grp | ||
|
|
||
| -- Exercise the no-GROUP BY aggregate shape while merging partial buffers. | ||
| query | ||
| SELECT sort_array(collect_set(f)), sort_array(collect_set(d)) | ||
| FROM ( | ||
| SELECT /*+ REPARTITION(3) */ f, d | ||
| FROM cs_norm_scalar | ||
| WHERE grp IN ('nan', 'zero') | ||
| ) | ||
|
|
||
| statement | ||
| CREATE TABLE cs_norm_nested( | ||
| grp string, | ||
| s struct<v:double>, | ||
| a array<float>, | ||
| deep_a array<struct<v:double>>, | ||
| deep_s struct<a:array<double>>) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO cs_norm_nested VALUES | ||
| ('nan', | ||
| named_struct('v', CAST('NaN' AS DOUBLE)), | ||
| array(CAST('NaN' AS FLOAT)), | ||
| array(named_struct('v', CAST('NaN' AS DOUBLE))), | ||
| named_struct('a', array(CAST('NaN' AS DOUBLE)))), | ||
| ('nan', | ||
| named_struct('v', CAST('NaN' AS DOUBLE)), | ||
| array(CAST('NaN' AS FLOAT)), | ||
| array(named_struct('v', CAST('NaN' AS DOUBLE))), | ||
| named_struct('a', array(CAST('NaN' AS DOUBLE)))), | ||
| ('zero', | ||
| named_struct('v', CAST('-0.0' AS DOUBLE)), | ||
| array(CAST('-0.0' AS FLOAT)), | ||
| array(named_struct('v', CAST('-0.0' AS DOUBLE))), | ||
| named_struct('a', array(CAST('-0.0' AS DOUBLE)))), | ||
| ('zero', | ||
| named_struct('v', CAST(0.0 AS DOUBLE)), | ||
| array(CAST(0.0 AS FLOAT)), | ||
| array(named_struct('v', CAST(0.0 AS DOUBLE))), | ||
| named_struct('a', array(CAST(0.0 AS DOUBLE)))), | ||
| ('null', | ||
| CAST(NULL AS STRUCT<v:DOUBLE>), | ||
| CAST(NULL AS ARRAY<FLOAT>), | ||
| CAST(NULL AS ARRAY<STRUCT<v:DOUBLE>>), | ||
| CAST(NULL AS STRUCT<a:ARRAY<DOUBLE>>)), | ||
| ('null', | ||
| named_struct('v', CAST(NULL AS DOUBLE)), | ||
| array(CAST(NULL AS FLOAT)), | ||
| array(CAST(NULL AS STRUCT<v:DOUBLE>)), | ||
| named_struct('a', array(CAST(NULL AS DOUBLE)))), | ||
| ('null', | ||
| named_struct('v', CAST(NULL AS DOUBLE)), | ||
| array(CAST(NULL AS FLOAT)), | ||
| array(CAST(NULL AS STRUCT<v:DOUBLE>)), | ||
| named_struct('a', array(CAST(NULL AS DOUBLE)))) | ||
|
|
||
| query | ||
| SELECT grp, | ||
| sort_array(collect_set(s)), | ||
| sort_array(collect_set(a)), | ||
| sort_array(collect_set(deep_a)), | ||
| sort_array(collect_set(deep_s)) | ||
| FROM (SELECT /*+ REPARTITION(3) */ * FROM cs_norm_nested) | ||
| GROUP BY grp | ||
| ORDER BY grp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that
getIncompatibleReasons()returnsNilon 4.2+, there is nothing left to render on the Spark 4.2 compatibility page, socollect_setwill show up there as fully supported with no caveats at all. ButRESPECT NULLSstill falls back on this line.Could you add the matching
getUnsupportedReasons(), gated the same way so it does not appear on the 3.4 through 4.1 pages where the field does not exist?