-
Notifications
You must be signed in to change notification settings - Fork 373
feat: route date and timestamp interval arithmetic through codegen dispatch #5864
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
a45bb87
2043cc5
495026f
6cb6e36
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 |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ package org.apache.comet.serde | |
|
|
||
| import java.util.Locale | ||
|
|
||
| import org.apache.spark.sql.catalyst.expressions.{AddMonths, Attribute, Cast, ConvertTimezone, DateAdd, DateDiff, DateFormatClass, DateFromUnixDate, DateSub, DayOfMonth, DayOfWeek, DayOfYear, Days, Expression, FromUTCTimestamp, GetDateField, GetTimestamp, Hour, Hours, LastDay, Literal, MakeDate, MakeDTInterval, MakeInterval, MakeTimestamp, MakeYMInterval, MicrosToTimestamp, MillisToTimestamp, Minute, Month, MonthsBetween, MultiplyDTInterval, NextDay, PreciseTimestampConversion, Quarter, Second, SecondsToTimestamp, TimestampAdd, TimestampDiff, ToUnixTimestamp, ToUTCTimestamp, TruncDate, TruncTimestamp, UnixDate, UnixMicros, UnixMillis, UnixSeconds, UnixTimestamp, WeekDay, WeekOfYear, Year} | ||
| import org.apache.spark.sql.catalyst.expressions.{AddMonths, Attribute, Cast, ConvertTimezone, DateAdd, DateAddInterval, DateAddYMInterval, DateDiff, DateFormatClass, DateFromUnixDate, DateSub, DayOfMonth, DayOfWeek, DayOfYear, Days, Expression, FromUTCTimestamp, GetDateField, GetTimestamp, Hour, Hours, LastDay, Literal, MakeDate, MakeDTInterval, MakeInterval, MakeTimestamp, MakeYMInterval, MicrosToTimestamp, MillisToTimestamp, Minute, Month, MonthsBetween, MultiplyDTInterval, NextDay, PreciseTimestampConversion, Quarter, Second, SecondsToTimestamp, SubtractDates, SubtractTimestamps, TimestampAdd, TimestampAddYMInterval, TimestampDiff, ToUnixTimestamp, ToUTCTimestamp, TruncDate, TruncTimestamp, UnixDate, UnixMicros, UnixMillis, UnixSeconds, UnixTimestamp, WeekDay, WeekOfYear, Year} | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types.{CalendarIntervalType, DataType, DateType, DoubleType, FloatType, IntegerType, LongType, StringType, TimestampNTZType, TimestampType} | ||
| import org.apache.spark.unsafe.types.UTF8String | ||
|
|
@@ -997,6 +997,40 @@ object CometTimestampAdd extends CometCodegenDispatch[TimestampAdd] | |
|
|
||
| object CometTimestampDiff extends CometCodegenDispatch[TimestampDiff] | ||
|
|
||
| // Date and timestamp interval arithmetic. `timestamp + day-time or calendar interval` resolves | ||
| // to `TimeAdd` on Spark 3.4 through 4.0 and to `TimestampAddInterval` on 4.1+, so that serde | ||
| // lives in the version shims. | ||
| object CometDateAddInterval extends CometCodegenDispatch[DateAddInterval] | ||
|
|
||
| object CometDateAddYMInterval extends CometCodegenDispatch[DateAddYMInterval] | ||
|
|
||
| object CometTimestampAddYMInterval extends CometCodegenDispatch[TimestampAddYMInterval] | ||
|
|
||
| // DateTimeUtils.subtractDates always writes microseconds 0 into the calendar interval, so | ||
| // the result fits the dispatcher's calendar-interval output in legacy mode as well and this | ||
| // expression needs no legacy guard, unlike CometSubtractTimestamps below. | ||
| object CometSubtractDates extends CometCodegenDispatch[SubtractDates] | ||
|
|
||
| object CometSubtractTimestamps extends CometCodegenDispatch[SubtractTimestamps] { | ||
| private val legacyIntervalReason = | ||
|
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. This is the same Worth a sentence on why the two take opposite approaches to the same bug, as well.
Contributor
Author
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. Done. The reason string links #5279, and the comment above the branch says why this one declines where |
||
| "In legacy interval mode (`spark.sql.legacy.interval.enabled=true`) the result is a" + | ||
| " `CalendarIntervalType`, and the JVM codegen dispatcher's calendar-interval output" + | ||
| " cannot carry a span past about 292 years (see" + | ||
| " https://github.com/apache/datafusion-comet/issues/5279), so the expression falls" + | ||
| " back to Spark" | ||
|
|
||
| override def getUnsupportedReasons(): Seq[String] = Seq(legacyIntervalReason) | ||
|
|
||
| // Same `Math.multiplyExact(microseconds, 1000L)` limit `CometMakeInterval` documents as a | ||
| // compatible note. That one only overflows on extreme arguments; `ts - ts` produces an | ||
| // arbitrary span from ordinary data, and legacy mode is off by default, so decline it. | ||
| // Remove this branch once #5279 carries CalendarInterval across the boundary losslessly. | ||
| override def getSupportLevel(expr: SubtractTimestamps): SupportLevel = expr.dataType match { | ||
| case CalendarIntervalType => Unsupported(Some(legacyIntervalReason)) | ||
| case _ => Compatible() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Spark's internal `PreciseTimestampConversion` reinterprets a value between the timestamp types | ||
| * (`TimestampType` / `TimestampNTZType`) and `LongType` without losing microsecond precision. It | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * 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.comet.serde | ||
|
|
||
| import org.apache.spark.sql.catalyst.expressions.TimestampAddInterval | ||
|
|
||
| /** | ||
| * `timestamp + day-time or calendar interval` resolves to `TimestampAddInterval` on Spark 4.1+ | ||
| * (`TimeAdd` on earlier versions) and runs through the codegen dispatcher. | ||
| */ | ||
| object CometTimestampAddInterval extends CometCodegenDispatch[TimestampAddInterval] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| -- 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. | ||
|
|
||
| -- date + calendar interval resolves to DateAddInterval and runs through the codegen dispatcher | ||
| -- so results match Spark exactly. With ANSI off, an interval carrying a time part is applied | ||
| -- on the timestamp and the result truncated back to a date; the ANSI error case lives in | ||
| -- date_add_interval_ansi.sql. America/Los_Angeles is pinned so the 25-hour row crosses DST. | ||
| -- Config: spark.sql.session.timeZone=America/Los_Angeles | ||
| -- Config: spark.comet.exec.scalaUDF.codegen.enabled=true | ||
| -- Config: spark.comet.shuffle.mode=native | ||
|
|
||
| statement | ||
| CREATE TABLE test_date_add_interval(d date, y int, m int, dd int, h int, k int) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_date_add_interval VALUES | ||
| (date'2024-01-31', 0, 1, 0, 0, 1), | ||
| (date'2024-01-31', 1, 1, 1, 0, 1), | ||
| (date'2024-02-29', 1, 0, 0, 0, 2), | ||
| (date'2024-03-31', 0, -1, 0, 0, 2), | ||
| (date'2024-12-31', 0, 0, 1, 0, 3), | ||
| (date'2024-03-09', 0, 0, 1, 0, 3), | ||
| (date'2024-03-09', 0, 0, 0, 25, 4), | ||
| (date'1970-01-01', -1, -1, -1, -1, 4), | ||
| (date'2024-06-15', NULL, 1, 1, 0, 5), | ||
| (date'2024-06-15', 0, NULL, 1, 0, 5), | ||
| (date'2024-06-15', 0, 0, NULL, 0, 6), | ||
| (date'2024-06-15', 0, 0, 0, NULL, 6), | ||
| (NULL, 1, 1, 1, 0, 7) | ||
|
|
||
| -- column date plus a calendar interval built from columns. Month arithmetic clamps to the end | ||
| -- of the shorter month before the day part is added. | ||
| query expect_dispatch(dateaddinterval) | ||
| SELECT d, y, m, dd, d + make_interval(y, m, 0, dd) FROM test_date_add_interval | ||
|
|
||
| -- interval on the left | ||
| query | ||
| SELECT make_interval(y, m, 0, dd) + d FROM test_date_add_interval | ||
|
|
||
| -- with ANSI off the hour part is applied on the timestamp and truncated away again | ||
| query | ||
| SELECT d, h, d + make_interval(y, m, 0, dd, h) FROM test_date_add_interval | ||
|
|
||
| -- The parser rejects interval literals that mix year-month and day-time units unless | ||
| -- spark.sql.legacy.interval.enabled is set, so literal calendar intervals come from | ||
| -- make_interval. Subtraction rewrites to an addition of the negated interval. | ||
| query | ||
| SELECT | ||
| d + make_interval(1, 0, 0, 1), | ||
| d + make_interval(-1, 0, 0, -1), | ||
| d + make_interval(0, 1, 0, 1), | ||
| d + make_interval(0, 1, 0, 1, 12), | ||
| d - make_interval(1, 0, 0, 1), | ||
| d - make_interval(0, 1, 0, 1) | ||
| FROM test_date_add_interval | ||
|
|
||
| -- all-literal operands (constant folding is disabled by the test suite) | ||
| query | ||
| SELECT | ||
| date'2024-01-31' + make_interval(0, 1, 0, 1), | ||
| date'2024-02-29' + make_interval(1, 0, 0, 1), | ||
| date'2024-01-31' - make_interval(0, 1, 0, 1) | ||
|
|
||
| -- date output through native shuffle | ||
| query | ||
| SELECT k, d + make_interval(y, m, 0, dd) AS r | ||
| FROM test_date_add_interval | ||
| DISTRIBUTE BY k |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| -- 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. | ||
|
|
||
| -- With ANSI on, DateAddInterval keeps day-granular intervals on the date path and rejects an | ||
| -- interval that carries hours, minutes, seconds or fractions of a second. The parser rejects | ||
| -- interval literals that mix year-month and day-time units, so make_interval builds them. | ||
| -- Config: spark.sql.ansi.enabled=true | ||
| -- Config: spark.comet.exec.scalaUDF.codegen.enabled=true | ||
| -- MinSparkVersion: 4.0 | ||
|
|
||
| statement | ||
| CREATE TABLE test_date_add_interval_ansi(d date, m int, dd int) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_date_add_interval_ansi VALUES | ||
| (date'2024-01-31', 1, 1), | ||
| (date'2024-02-29', 12, 0), | ||
| (date'2024-03-31', -1, -1), | ||
| (date'2024-06-15', NULL, 1), | ||
| (NULL, 1, 1) | ||
|
|
||
| -- sentinel: a day-granular interval succeeds and asserts native execution. The row with a | ||
| -- NULL month builds a NULL interval inside the kernel and yields NULL rather than an error; | ||
| -- a literal NULL interval would be folded away by NullPropagation before reaching Comet. | ||
| query expect_dispatch(dateaddinterval) | ||
| SELECT d, m, dd, d + make_interval(0, m, 0, dd), d - make_interval(0, 1, 0, 1) | ||
| FROM test_date_add_interval_ansi | ||
|
|
||
| -- an interval with a time part is rejected | ||
| query expect_error(INVALID_INTERVAL_WITH_MICROSECONDS_ADDITION) | ||
| SELECT d + make_interval(0, 1, 0, 1, 12) FROM test_date_add_interval_ansi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| -- 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. | ||
|
|
||
| -- With ANSI on, DateAddInterval keeps day-granular intervals on the date path and rejects an | ||
| -- interval that carries hours, minutes, seconds or fractions of a second. The parser rejects | ||
| -- interval literals that mix year-month and day-time units, so make_interval builds them. | ||
| -- Config: spark.sql.ansi.enabled=true | ||
| -- Config: spark.comet.exec.scalaUDF.codegen.enabled=true | ||
| -- MaxSparkVersion: 3.5 | ||
|
|
||
| statement | ||
| CREATE TABLE test_date_add_interval_ansi(d date, m int, dd int) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_date_add_interval_ansi VALUES | ||
| (date'2024-01-31', 1, 1), | ||
| (date'2024-02-29', 12, 0), | ||
| (date'2024-03-31', -1, -1), | ||
| (date'2024-06-15', NULL, 1), | ||
| (NULL, 1, 1) | ||
|
|
||
| -- sentinel: a day-granular interval succeeds and asserts native execution. The row with a | ||
| -- NULL month builds a NULL interval inside the kernel and yields NULL rather than an error; | ||
| -- a literal NULL interval would be folded away by NullPropagation before reaching Comet. | ||
| query expect_dispatch(dateaddinterval) | ||
| SELECT d, m, dd, d + make_interval(0, m, 0, dd), d - make_interval(0, 1, 0, 1) | ||
| FROM test_date_add_interval_ansi | ||
|
|
||
| -- an interval with a time part is rejected | ||
| query expect_error(Cannot add hours, minutes or seconds, milliseconds, microseconds to a date) | ||
| SELECT d + make_interval(0, 1, 0, 1, 12) FROM test_date_add_interval_ansi |
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.
Reading this right above the
CometSubtractTimestampsguard, the obvious question is why this one does not need the same branch when its legacy result is also aCalendarIntervalType. The answer is insubtract_dates.sqlbut not here, and it is the invariant whoever adds the next calendar-interval-producing serde will need.Worth a line? Something like "Legacy mode returns a
CalendarIntervalType, butDateTimeUtils.subtractDatesalways sets microseconds to 0, so the dispatcher'smultiplyExactcannot overflow and both modes dispatch." I checked that on 3.4.3, 3.5.8, 4.0.1 and 4.1.3 and it holds on all four.