tlfua commented on code in PR #7457:
URL: https://github.com/apache/datafusion-comet/pull/7457#discussion_r4462332554
##########
spark/src/main/scala/org/apache/spark/sql/comet/CometTakeOrderedAndProjectExec.scala:
##########
@@ -63,6 +63,13 @@ object CometTakeOrderedAndProjectExec extends
CometSink[TakeOrderedAndProjectExe
if (!supportedSortType(op, op.sortOrder)) {
return Unsupported(Some("unsupported data type in sort order"))
}
+ // Spark's ordering evaluates a sort key only for the rows whose earlier
keys tie, but native
+ // execution evaluates every key over the whole batch, so a later key that
can fail may raise
+ // on a row that Spark skips (#7246).
+ op.sortOrder.drop(1).find(s => !cannotFail(s.child)).foreach { s =>
+ return Incompatible(
+ Some(s"sort key ${s.child.sql} can fail on rows that Spark's ordering
skips"))
+ }
Review Comment:
Thanks. I checked each plain-sort shape against Spark with ANSI on and filed
https://github.com/apache/datafusion-comet/issues/7470 .
Global `ORDER BY a, b + 1` and window ordering (`rank`, `row_number` with
`PARTITION BY`) throw in Spark too, because the range exchange or the window
pre-projection evaluates every key. The divergence shows only when nothing
beneath the sort evaluates all keys first, for example `SORT BY` or a sort over
a single-partition child such as `LIMIT`. I extracted the check into
`QueryPlanSerde.laterSortKeyThatCanFail(Seq[SortOrder])` so the plain sort can
reuse it in that follow-up.
##########
spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala:
##########
@@ -1394,6 +1395,38 @@ object QueryPlanSerde extends Logging with CometExprShim
with CometTypeShim {
}
}
+ /**
+ * Whether `expr` cannot raise an error for any row, so native execution may
evaluate it for
+ * rows that Spark skips. Like the native `cannot_fail`, it is conservative:
columns, literals,
+ * and comparisons, boolean logic, null checks, conditionals, substrings,
case conversion and
+ * length, numeric arithmetic, negation and absolute value outside ANSI mode
and casts that
+ * cannot fail over them. Anything else is treated as able to fail.
+ */
+ def cannotFail(expr: Expression): Boolean = {
+ def numeric: Boolean = expr.dataType.isInstanceOf[NumericType]
+ def nonAnsiNumeric(evalMode: EvalMode.Value): Boolean = evalMode !=
EvalMode.ANSI && numeric
+ val nodeCannotFail = expr match {
+ case _: Attribute | _: BoundReference | _: Literal => true
+ case _: BinaryComparison | _: And | _: Or | _: Not | _: IsNull | _:
IsNotNull => true
+ case _: If | _: CaseWhen | _: Coalesce => true
+ case _: KnownFloatingPointNormalized | _: NormalizeNaNAndZero => true
+ case _: Substring | _: Upper | _: Lower | _: Length => true
+ case c: Cast => CometCast.cannotFail(c)
+ case _: UnaryPositive => true
+ case u: UnaryMinus => !u.failOnError && numeric
+ case a: Abs => !a.failOnError && numeric
+ case a: Add => nonAnsiNumeric(a.evalMode)
+ case s: Subtract => nonAnsiNumeric(s.evalMode)
+ case m: Multiply => nonAnsiNumeric(m.evalMode)
+ case d: Divide => nonAnsiNumeric(d.evalMode)
+ case d: IntegralDivide => nonAnsiNumeric(d.evalMode)
+ case r: Remainder => nonAnsiNumeric(r.evalMode)
+ case p: Pmod => nonAnsiNumeric(p.evalMode)
+ case _ => false
Review Comment:
Widened the list after reading the Spark source for 3.4.3, 3.5.8, 4.0.1 and
4.1.1. Added:
- String: `octet_length`, `bit_length`, `trim`/`ltrim`/`rtrim`, `replace`,
`initcap`, `concat_ws`, `concat` (string, at least one argument), `reverse`
(string)
- Datetime: `year`, `quarter`, `month`, `day`, `dayofweek`, `weekday`,
`dayofyear`, `hour`, `minute`, `second`
- Math: `floor`, `ceil`, `sqrt`, `exp`, `log`, `signum`, `greatest`,
`least`, `isnan`, `nanvl`, `IN`; `round`/`bround` for float/double always, and
for integral types only when ANSI is off
- Hash: `hash`, `xxhash64`, `md5`, `sha1`, `sha2`, `crc32`
- Cast: string to binary (inserted implicitly by `md5(s)` and friends)
I also checked each one's native implementation (the Comet kernels plus
DataFusion / datafusion-spark 55.2) for errors that depend on the data. The
only real ones were timestamp casts: they go through chrono, which rejects
years Spark accepts, and raise even for TRY (filed as
https://github.com/apache/datafusion-comet/issues/7471 ). So any cast to or
from `TIMESTAMP`/`TIMESTAMP_NTZ` now counts as able to fail. The rest error
only at planning time or beyond 2GB of string offsets per batch.
Left out on purpose: timestamp casts (above), decimal `round` (the
negative-scale path builds a `Decimal` that can fail the precision check),
integral `round` under ANSI (overflow), `lpad`/`rpad`/`repeat` (size limits),
`element_at`, `date_add`, `to_date`, and anything that parses input.
On upkeep: the list fails safe. A function missing from it only falls back
to Spark, so leaving one out costs performance, never correctness. Each entry
was checked against Spark 3.4.3 to 4.1.1 and the native kernel, and
`topk_later_key_ansi.sql` runs each one natively over NULL and boundary values.
##########
spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala:
##########
@@ -1394,6 +1395,38 @@ object QueryPlanSerde extends Logging with CometExprShim
with CometTypeShim {
}
}
+ /**
+ * Whether `expr` cannot raise an error for any row, so native execution may
evaluate it for
+ * rows that Spark skips. Like the native `cannot_fail`, it is conservative:
columns, literals,
+ * and comparisons, boolean logic, null checks, conditionals, substrings,
case conversion and
+ * length, numeric arithmetic, negation and absolute value outside ANSI mode
and casts that
+ * cannot fail over them. Anything else is treated as able to fail.
+ */
Review Comment:
Good catch. I reworded the comment: the list now states that it answers from
Spark semantics alone and is independent of the native `cannot_fail` in
`case_when.rs`. I left `cannotThrow` in `arrays.scala` as is, because switching
it to this list would change when `array_append` runs natively; I'd rather do
that in a separate PR with its own tests.
##########
spark/src/test/resources/sql-tests/operators/topk_later_key_ansi.sql:
##########
@@ -0,0 +1,80 @@
+-- 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.
+
+-- Spark's top-K ordering evaluates a later sort key only for rows whose
earlier keys tie, so a
+-- later key that fails under ANSI mode does not fail on a row Spark skips.
Comet evaluates every
+-- key over the whole batch, so a top-K whose later key can fail runs on Spark
(#7246).
+-- Each table is one batch.
+
+-- Config: spark.sql.ansi.enabled=true
+-- Config: spark.sql.adaptive.enabled=false
+
+statement
+CREATE TABLE topk(a int, b int) USING parquet
+
+statement
+INSERT INTO topk SELECT /*+ REPARTITION(1) */ * FROM VALUES
+ (1, 2147483647), (2, 5), (3, 2147483647), (4, 7), (5, 9) AS t(a, b)
+
+-- No two rows tie on a, so Spark never evaluates b + 1
+query expect_fallback(can fail on rows that Spark's ordering skips)
+SELECT a, b FROM topk ORDER BY a, b + 1 LIMIT 2
+
+query expect_fallback(can fail on rows that Spark's ordering skips)
+SELECT a, b FROM topk ORDER BY a DESC, CAST(CAST(b AS STRING) AS INT) * 2
LIMIT 2
+
+query expect_fallback(can fail on rows that Spark's ordering skips)
+SELECT a, b FROM topk ORDER BY a, abs(b) LIMIT 2
+
+-- Later keys that cannot fail stay native
+query
+SELECT a, b FROM topk ORDER BY a, b LIMIT 2
+
+query
+SELECT a, b FROM topk ORDER BY a, b IS NULL, CAST(b AS BIGINT), try_add(b, 1)
LIMIT 2
+
+query
+SELECT a, b FROM topk ORDER BY a, substr(CAST(b AS STRING), 1, 3),
upper(CAST(b AS STRING)),
+ length(lower(CAST(b AS STRING))) LIMIT 2
Review Comment:
Added:
- Fallback: a fallible key nested under an allowed one (`coalesce(b + 1,
0)`, `IF(a > 3, b + 1, 0)`), and `round(b, -1)` under ANSI.
- Native: `try_cast`, and a `topk_types` table with NULL, NaN, -0.0,
int/long bounds, empty strings, and dates such as 1970-01-01, 2000-02-29 and
9999-12-31, run through the string, datetime, math and hash keys.
- ANSI off, running natively: a string-to-int cast in LEGACY mode, `round(b,
-1)`, `b / 0`, `b % 0`, `b DIV 0`, `l + 1` and `l * 2` as later keys.
##########
docs/source/user-guide/latest/compatibility/operators.md:
##########
@@ -94,6 +94,14 @@ TopK, and ranking window order keys run natively under both
default and nondefau
searches boundaries over a nullable nested key still runs on Spark
([#6477](https://github.com/apache/datafusion-comet/issues/6477)).
+Spark's TopK (`TakeOrderedAndProject`, an `ORDER BY` with a `LIMIT`) evaluates
a sort key after
+the first only for the rows whose earlier keys tie. Comet evaluates every key
over the whole batch,
+so under `spark.sql.ansi.enabled=true` a later key such as `b + 1` could raise
+`ARITHMETIC_OVERFLOW` on a row that Spark never evaluates it for. A TopK whose
later sort key can
+fail therefore runs on Spark, unless
+`spark.comet.operator.TakeOrderedAndProjectExec.allowIncompatible=true`
+([#7246](https://github.com/apache/datafusion-comet/issues/7246)).
Review Comment:
Reworded. The section now says the check does not depend on ANSI: Comet
falls back whenever it cannot show that a later key never fails. It also notes
that a sort without a LIMIT and with no exchange beneath it, such as `SORT BY`,
still runs natively and can raise, and links
https://github.com/apache/datafusion-comet/issues/7470 .
--
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]