andygrove commented on code in PR #5789:
URL: https://github.com/apache/datafusion-comet/pull/5789#discussion_r3996916794
##########
spark/src/main/scala/org/apache/comet/serde/datetime.scala:
##########
@@ -290,12 +290,14 @@ private[serde] object DatetimeCollation extends
CometTypeShim {
expr.children.exists(c => hasNonDefaultStringCollation(c.dataType))
}
-object CometUnixTimestamp extends CometExpressionSerde[UnixTimestamp] {
+object CometUnixTimestamp
+ extends CometExpressionSerde[UnixTimestamp]
+ with CodegenDispatchFallback {
private val collationReason = DatetimeCollation.reason("unix_timestamp")
override def getUnsupportedReasons(): Seq[String] = Seq(
- "Only `DateType`, `TimestampType`, and `TimestampNTZType` inputs are
supported.")
+ "Native execution only supports `DateType`, `TimestampType`, and
`TimestampNTZType` inputs.")
override def getIncompatibleReasons(): Seq[String] =
DatetimeCollation.incompatibleReasons("unix_timestamp")
Review Comment:
Now that string input always reports `Unsupported`, this collation check can
only be reached by a collated *format* argument over a date, timestamp or
timestamp-NTZ input. Spark ignores the format entirely for those types, since
`ToTimestamp.eval` and `doGenCode` only read `right` in the `StringType` branch
on 3.4 through 4.1, so the collation of the format cannot change the answer.
I confirmed that `SELECT unix_timestamp(d, 'unused' COLLATE UTF8_LCASE)` is
dispatched rather than run on the native kernel, and that the user also gets a
`[COMET-INFO]` suggesting
`spark.comet.expression.UnixTimestamp.allowIncompatible=true` for a difference
that cannot exist. That is still an improvement on the full fallback we had
before, so nothing regresses here. But would it be better to scope the check to
the input child, something like
`hasNonDefaultStringCollation(expr.children.head.dataType)`, so those inputs
stay native? The new "collated formats dispatch date and timestamp inputs by
default" test would then flip to asserting native execution, which reads as the
stronger claim.
##########
spark/src/test/scala/org/apache/comet/CometTemporalExpressionSuite.scala:
##########
@@ -394,19 +398,27 @@ class CometTemporalExpressionSuite extends CometTestBase
with AdaptiveSparkPlanH
.createDataFrame(spark.sparkContext.parallelize(data), schema)
.createOrReplaceTempView("string_tbl")
- // String input should fall back to Spark
- checkSparkAnswerAndFallbackReason(
- "SELECT ts_str, unix_timestamp(ts_str) from string_tbl order by
ts_str",
- "unix_timestamp does not support input type: StringType")
-
- // String input with custom format should also fall back
- checkSparkAnswerAndFallbackReason(
- "SELECT ts_str, unix_timestamp(ts_str, 'yyyy-MM-dd HH:mm:ss') from
string_tbl",
- "unix_timestamp does not support input type: StringType")
+ withSQLConf(
+ SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->
+ "org.apache.spark.sql.catalyst.optimizer.ConstantFolding") {
+ for (allowIncompatible <- Seq("false", "true")) {
+ withSQLConf(
+ CometConf.getExprAllowIncompatConfigKey("UnixTimestamp") ->
allowIncompatible) {
+ for (query <- Seq(
+ "SELECT unix_timestamp(ts_str) FROM string_tbl",
+ "SELECT unix_timestamp(ts_str, 'yyyy-MM-dd HH:mm:ss') FROM
string_tbl",
+ "SELECT unix_timestamp('2024-06-15', 'yyyy-MM-dd') FROM
string_tbl")) {
+ assertCodegenRan {
Review Comment:
#5610 landed `checkSparkAnswerAndImpl` after you branched, and it fits these
three tests better than `assertCodegenRan`. `checkSparkAnswerAndOperator`
passes whether the expression ran natively or through the dispatcher, so on its
own it cannot say which one happened, and `assertCodegenRan` only says that
something somewhere in the JVM dispatched.
Would you replace these with `checkSparkAnswerAndImpl(query, native =
Seq.empty, dispatched = Seq("unix_timestamp"))`? I tried it locally and it
passes as written. The same applies to the
`CometScalaUDFCodegen.stats().totalLookups == 0` assertion at line 462, where
`native = Seq("unix_timestamp"), dispatched = Seq.empty` makes the same
statement per plan instead of leaning on a JVM-wide counter that unrelated
dispatcher activity could perturb. Doing both would also let the
`CometCodegenAssertions` mixin and the `CometScalaUDFCodegen` import come back
out of this file.
##########
docs/source/contributor-guide/expression-audits/datetime_funcs.md:
##########
@@ -95,4 +95,12 @@
- Rewrites to `Cast(..., EvalMode.LEGACY)` (no format, native) or
`GetTimestamp(..., failOnError = false)` (with format, via the codegen
dispatcher) before Comet sees the plan. In non-ANSI mode the rewritten tree is
identical to `to_timestamp`; invalid inputs return NULL to match Spark.
+## unix_timestamp
+
+- Spark 3.4.3 (audited 2026-09-09): string input uses Spark's generated parser
through codegen dispatch. Literal and column formats preserve null handling,
ANSI errors, parser policy, and session time zone.
Review Comment:
The version bullets in this file record what changed in Spark between
versions, with the Comet-side behavior on the unversioned bullets.
`from_utc_timestamp` and `to_utc_timestamp` just above are the pattern. Three
of the four bullets here describe Comet's routing rather than a Spark
difference, and they say the same thing for every version.
Could the routing move down into the unversioned bullet, leaving the version
bullets for the things that genuinely differ, such as 3.5's structured parsing
errors and 4.0's collated `inputTypes`?
##########
spark/src/test/resources/sql-tests/expressions/datetime/unix_timestamp_strings.sql:
##########
@@ -0,0 +1,58 @@
+-- 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.
+
+-- Config: spark.sql.legacy.timeParserPolicy=CORRECTED
+-- ConfigMatrix: parquet.enable.dictionary=false,true
+-- ConfigMatrix: spark.sql.session.timeZone=UTC,America/Los_Angeles
+
+statement
+CREATE TABLE test_unix_ts_strings(s string, fmt string) USING parquet
+
+statement
+INSERT INTO test_unix_ts_strings VALUES
+ ('1970-01-01 00:00:00', 'yyyy-MM-dd HH:mm:ss'),
+ ('1969-12-31 23:59:59', 'yyyy-MM-dd HH:mm:ss'),
+ ('2024-02-29 12:30:45', 'yyyy-MM-dd HH:mm:ss'),
+ ('2024-03-10 02:30:00', 'yyyy-MM-dd HH:mm:ss'),
+ ('2024-11-03 01:30:00', 'yyyy-MM-dd HH:mm:ss'),
+ ('1969-12-31 23:59:59.999999', 'yyyy-MM-dd HH:mm:ss.SSSSSS'),
+ ('2024/06/15', 'yyyy/MM/dd'),
+ ('2024-06-15T10:30:45+05:30', "yyyy-MM-dd'T'HH:mm:ssXXX"),
+ ('1582-10-04', 'yyyy-MM-dd'),
+ ('0001-01-01', 'yyyy-MM-dd'),
+ ('9999-12-31', 'yyyy-MM-dd'),
+ ('not a date', 'yyyy-MM-dd'),
+ ('2024-02-30', 'yyyy-MM-dd'),
+ ('', 'yyyy-MM-dd'),
+ (NULL, 'yyyy-MM-dd'),
+ ('2024-06-15', NULL),
+ ('2024-06-15', ''),
+ (NULL, NULL)
+
+-- Exercise both the cached literal formatter and the per-row formatter.
+query
Review Comment:
Routing is what this PR changes, so would you use `query
expect_dispatch(unix_timestamp)` here and on the parser-policy queries? A plain
`query` only asserts that the projection stayed in Comet. A later change that
kept the expression in Comet but moved it back onto a native kernel with
different parsing would still pass, as long as the answers happened to agree on
these particular inputs.
To be clear about the severity, I mutation-tested the fixtures as they stand
and they do catch removal of the mixin, 12 of 22 fail. This is about locking
the direction in rather than filling a hole.
##########
spark/src/test/resources/sql-tests/expressions/datetime/unix_timestamp_fallback.sql:
##########
@@ -0,0 +1,42 @@
+-- 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.
+
+-- Config: spark.comet.exec.scalaUDF.codegen.enabled=false
+
+statement
+CREATE TABLE test_unix_ts_fallback(s string, fmt string, d date, ts timestamp,
ntz timestamp_ntz) USING parquet
+
+statement
+INSERT INTO test_unix_ts_fallback VALUES
+ ('2024-06-15', 'yyyy-MM-dd', date('2024-06-15'), timestamp('2024-06-15
10:30:45'), CAST('2024-06-15 10:30:45' AS TIMESTAMP_NTZ)),
+ (NULL, NULL, NULL, NULL, NULL)
+
+query expect_fallback(spark.comet.exec.scalaUDF.codegen.enabled)
+SELECT unix_timestamp(s) FROM test_unix_ts_fallback
+
+query expect_fallback(spark.comet.exec.scalaUDF.codegen.enabled)
+SELECT unix_timestamp(s, fmt) FROM test_unix_ts_fallback
+
+query expect_fallback(spark.comet.exec.scalaUDF.codegen.enabled)
+SELECT unix_timestamp('2024-06-15', 'yyyy-MM-dd')
+
+-- Date and timestamp inputs keep their native path and ignore the format.
Review Comment:
The comment claims a native path but the assertion below cannot see it.
Because this file disables the dispatcher, a flip to dispatch would surface as
a fallback and the test would fail anyway, so it is covered today. `query
expect_native(unix_timestamp)` would state it directly and keep holding if
someone later changes the config header at the top of the file.
##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometUnixTimestampBenchmark.scala:
##########
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.benchmark
+
+/**
+ * Compares string parsing through codegen dispatch with Spark and native
timestamp input. Run
+ * with:
+ * {{{
+ * make benchmark-org.apache.spark.sql.benchmark.CometUnixTimestampBenchmark
+ * }}}
+ */
+object CometUnixTimestampBenchmark extends CometBenchmarkBase {
+ override def runCometBenchmark(mainArgs: Array[String]): Unit = {
+ val rows = 1024 * 1024
+ withTempPath { dir =>
+ withTempTable("parquetV1Table") {
+ prepareTable(
+ dir,
+ spark
+ .range(rows)
+ .selectExpr(
+ "timestamp_seconds(id) AS ts",
+ "date_format(timestamp_seconds(id), 'yyyy-MM-dd HH:mm:ss') AS s",
+ "CASE WHEN id % 2 = 0 THEN 'yyyy-MM-dd HH:mm:ss' " +
+ "ELSE 'yyyy-MM-dd H:m:s' END AS fmt"))
+ for ((shape, arguments) <- Seq(
Review Comment:
I ran this locally on an M3 Max at `local[1]`, release build, same row
count, and got 351 vs 340 ms for the default format and 1036 vs 1002 ms for the
format column, with the native timestamp control at 37 vs 24 ms. That lines up
with your runner. The two string cases are at parity rather than faster, which
makes sense, because Spark's per-row `TimestampFormatter.parse` dominates both
arms and the dispatcher neither helps nor hurts it.
The benefit of this change is structural instead. The projection and
everything above it stay on Comet rather than paying a columnar-to-row round
trip and demoting the surrounding operators. A scan into project into noop
shape cannot show that, because the old fallback cost exactly one
`CometColumnarToRow` here and nothing downstream. I measured the pre-change
routing by accident at one point and got 361 and 995 ms for the same two cases,
which is inside the noise.
Could you add a case where the projection feeds a Comet operator, something
like `SELECT unix_timestamp(s) AS u, count(*) FROM parquetV1Table GROUP BY u`?
It would also be worth saying in the description that the string cases are a
no-regression check rather than a speedup, because the table as written reads
as evidence that dispatch is faster than Spark's whole-stage codegen for this
expression, and it is not.
--
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]