This is an automated email from the ASF dual-hosted git repository.
MaxGekk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 0d36e259581a [SPARK-57103][SQL][TEST][FOLLOWUP] Add test coverage for
MIN/MAX over nanosecond-precision timestamp types
0d36e259581a is described below
commit 0d36e259581ae26b6b759987ef0c9c20f7745c3c
Author: Stevo Mitric <[email protected]>
AuthorDate: Fri Jun 19 12:47:37 2026 +0200
[SPARK-57103][SQL][TEST][FOLLOWUP] Add test coverage for MIN/MAX over
nanosecond-precision timestamp types
### What changes were proposed in this pull request?
This PR adds end-to-end, Catalyst-level, and golden-file test coverage for
the `MIN` and `MAX` aggregates (and the closely related `min_by` / `max_by` /
`greatest` / `least`) over the nanosecond-precision timestamp types
`TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` (`p ∈ [7, 9]`), part of the nanosecond
timestamp preview (SPARK-56822).
No production code is changed. `Max`/`Min` are type-agnostic
`DeclarativeAggregate`s whose only type gate is
`TypeUtils.checkForOrderingExpr`, which already returns `TypeCheckSuccess` for
the nanosecond types because they are orderable `AtomicType`s `Comparable` were
wired in SPARK-57103, the physical value is UnsafeRow-mutable (SPARK-56981),
and `CodeGenerator.genComp` already has a dedicated `case _:
AnyTimestampNanoType => compareTo` arm. Since `dataType = child.dataType`, the
resu [...]
Added tests:
- `TimestampNanosFunctionsSuiteBase` (runs under ANSI on and off):
precision/type preservation; sub-microsecond ordering on both the codegen and
interpreted paths (two values sharing the same `epochMicros` but differing
within the microsecond); all-NULL and empty input → NULL; `GROUP BY` a
nanosecond key whose sub-microsecond remainder must keep distinct keys from
collapsing; `min_by`/`max_by`/`greatest`/`least` over same-precision columns;
and equivalence with the microsecond path wh [...]
- `ExpressionTypeCheckingSuite`: asserts `Max`/`Min`
`checkInputDataTypes()` succeeds for `TimestampNTZNanosType` /
`TimestampLTZNanosType` and that the result type preserves the precision.
- `timestamp-ntz-nanos.sql` / `timestamp-ltz-nanos.sql`: `MAX`/`MIN` and
`GROUP BY` queries, with regenerated `results/` and `analyzer-results/` golden
files.
Mixed-precision inputs (e.g. `greatest(ts_p7, ts_p9)`, a `UNION` of two
precisions, or comparing an aggregate against a micros literal) route through
`findWiderDateTimeType`, which has no nanosecond arm yet, so they are
intentionally **out of scope** here and tracked by SPARK-57454; every column in
these tests is strictly same-precision.
### Why are the changes needed?
`MIN`/`MAX` over nanosecond timestamps are a core, expected operation, but
the nanosecond preview had no aggregate test coverage.
### Does this PR introduce any user-facing change?
No. Tests only;
### How was this patch tested?
New tests in this PR.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor
Closes #56592 from stevomitric/stevomitric/min-max.
Authored-by: Stevo Mitric <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
---
.../analysis/ExpressionTypeCheckingSuite.scala | 14 ++
.../analyzer-results/timestamp-ltz-nanos.sql.out | 24 +++
.../analyzer-results/timestamp-ntz-nanos.sql.out | 24 +++
.../sql-tests/inputs/timestamp-ltz-nanos.sql | 15 ++
.../sql-tests/inputs/timestamp-ntz-nanos.sql | 15 ++
.../sql-tests/results/timestamp-ltz-nanos.sql.out | 24 +++
.../sql-tests/results/timestamp-ntz-nanos.sql.out | 24 +++
.../sql/TimestampNanosFunctionsSuiteBase.scala | 170 ++++++++++++++++++++-
8 files changed, 306 insertions(+), 4 deletions(-)
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/ExpressionTypeCheckingSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/ExpressionTypeCheckingSuite.scala
index 95e118a30771..9b5db986153e 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/ExpressionTypeCheckingSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/ExpressionTypeCheckingSuite.scala
@@ -394,6 +394,20 @@ class ExpressionTypeCheckingSuite extends SparkFunSuite
with SQLHelper with Quer
)
}
+ test("SPARK-57103: Min/Max accept nanosecond-precision timestamp types and
preserve them") {
+ // Min/Max gate only on orderability (TypeUtils.checkForOrderingExpr), and
the nanosecond
+ // timestamp types are orderable AtomicTypes (SPARK-57103), so the
analysis gate passes and the
+ // result type preserves the input precision (dataType = child.dataType).
No inputTypes / type
+ // matcher is involved, so no production change to Min/Max is needed.
+ Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(7)).foreach { dt =>
+ val a = AttributeReference("c", dt)()
+ assert(Max(a).checkInputDataTypes() == TypeCheckResult.TypeCheckSuccess)
+ assert(Min(a).checkInputDataTypes() == TypeCheckResult.TypeCheckSuccess)
+ assert(Max(a).dataType == dt)
+ assert(Min(a).dataType == dt)
+ }
+ }
+
test("check types for aggregates") {
// We use AggregateFunction directly at here because the error will be
thrown from it
// instead of from AggregateExpression, which is the wrapper of an
AggregateFunction.
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
index bd98d67a3c5f..6ce540f2fcbc 100644
---
a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
+++
b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
@@ -649,6 +649,30 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
}
+-- !query
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (CAST(NULL AS timestamp_ltz(9))) AS t(c)
+-- !query analysis
+Aggregate [max(c#x) AS max(c)#x, min(c#x) AS min(c)#x]
++- SubqueryAlias t
+ +- LocalRelation [c#x]
+
+
+-- !query
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC') AS t(c)
+ GROUP BY c ORDER BY c
+-- !query analysis
+Sort [c#x ASC NULLS FIRST], true
++- Aggregate [c#x], [c#x, count(1) AS count(1)#xL]
+ +- SubqueryAlias t
+ +- LocalRelation [c#x]
+
+
-- !query
SELECT unix_timestamp(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
-- !query analysis
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
index 5bd6f7e6a76f..85fe657d155a 100644
---
a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
+++
b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
@@ -576,6 +576,30 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
}
+-- !query
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (CAST(NULL AS timestamp_ntz(9))) AS t(c)
+-- !query analysis
+Aggregate [max(c#x) AS max(c)#x, min(c#x) AS min(c)#x]
++- SubqueryAlias t
+ +- LocalRelation [c#x]
+
+
+-- !query
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001') AS t(c)
+ GROUP BY c ORDER BY c
+-- !query analysis
+Sort [c#x ASC NULLS FIRST], true
++- Aggregate [c#x], [c#x, count(1) AS count(1)#xL]
+ +- SubqueryAlias t
+ +- LocalRelation [c#x]
+
+
-- !query
SELECT unix_timestamp(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
-- !query analysis
diff --git
a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
index 73c6022e354e..eb14e3b240d1 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
@@ -174,6 +174,21 @@ SELECT TIMESTAMP_LTZ '1960-01-02 03:04:05.123456789 UTC' +
SELECT TIMESTAMP_LTZ '2020-01-02 03:04:05.123456789 UTC' + make_interval(0, 1,
0, 2, 0, 0, 0);
SELECT TIMESTAMP_LTZ '2020-01-02 03:04:05.123456789 UTC' + INTERVAL '1' MONTH;
+-- SPARK-57103: MAX / MIN over nanosecond-precision TIMESTAMP_LTZ. The
aggregate preserves the
+-- nanosecond type and orders by the sub-microsecond remainder; NULLs are
ignored. Values are
+-- rendered in the session time zone (America/Los_Angeles).
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (CAST(NULL AS timestamp_ltz(9))) AS t(c);
+-- GROUP BY a nanosecond key: two keys that share epochMicros but differ
within the microsecond
+-- must not collapse into one group.
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC') AS t(c)
+ GROUP BY c ORDER BY c;
+
-- SPARK-57528: unix_timestamp / to_unix_timestamp over nanosecond-precision
values. The result is
-- whole-second BIGINT; the sub-second digits are dropped. A literal without
an explicit zone is
-- read in the session time zone (America/Los_Angeles, UTC-08:00); an
explicit-zone literal fixes
diff --git
a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
index 016af6b24763..b34cff5396b9 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
@@ -150,6 +150,21 @@ SELECT TIMESTAMP_NTZ '1960-01-02 03:04:05.123456789' +
INTERVAL '0 00:00:00.0000
SELECT TIMESTAMP_NTZ '2020-01-02 03:04:05.123456789' + make_interval(0, 1, 0,
2, 0, 0, 0);
SELECT TIMESTAMP_NTZ '2020-01-02 03:04:05.123456789' + INTERVAL '1' MONTH;
+-- SPARK-57103: MAX / MIN over nanosecond-precision TIMESTAMP_NTZ. The
aggregate preserves the
+-- nanosecond type and orders by the sub-microsecond remainder (two values
share the same
+-- microsecond and differ only within it); NULLs are ignored.
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (CAST(NULL AS timestamp_ntz(9))) AS t(c);
+-- GROUP BY a nanosecond key: two keys that share epochMicros but differ
within the microsecond
+-- must not collapse into one group.
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001') AS t(c)
+ GROUP BY c ORDER BY c;
+
-- SPARK-57528: unix_timestamp / to_unix_timestamp over nanosecond-precision
values. The result is
-- whole-second BIGINT; the sub-second digits are dropped and NTZ applies no
zone shift, so the
-- wall-clock value is read as the epoch instant.
diff --git
a/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
b/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
index b5eef6a1ac93..8a24b41f4a3e 100644
--- a/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
@@ -728,6 +728,30 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
}
+-- !query
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (CAST(NULL AS timestamp_ltz(9))) AS t(c)
+-- !query schema
+struct<max(c):timestamp_ltz(9),min(c):timestamp_ltz(9)>
+-- !query output
+2019-12-31 16:00:00.000000999 2019-12-31 16:00:00.000000001
+
+
+-- !query
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000999 UTC'),
+ (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC') AS t(c)
+ GROUP BY c ORDER BY c
+-- !query schema
+struct<c:timestamp_ltz(9),count(1):bigint>
+-- !query output
+2019-12-31 16:00:00.000000001 2
+2019-12-31 16:00:00.000000999 1
+
+
-- !query
SELECT unix_timestamp(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
-- !query schema
diff --git
a/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
b/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
index a3d0d237e818..6227e2cd214c 100644
--- a/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
@@ -646,6 +646,30 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
}
+-- !query
+SELECT max(c), min(c) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (CAST(NULL AS timestamp_ntz(9))) AS t(c)
+-- !query schema
+struct<max(c):timestamp_ntz(9),min(c):timestamp_ntz(9)>
+-- !query output
+2020-01-01 00:00:00.000000999 2020-01-01 00:00:00.000000001
+
+
+-- !query
+SELECT c, count(*) FROM VALUES
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000999'),
+ (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001') AS t(c)
+ GROUP BY c ORDER BY c
+-- !query schema
+struct<c:timestamp_ntz(9),count(1):bigint>
+-- !query output
+2020-01-01 00:00:00.000000001 2
+2020-01-01 00:00:00.000000999 1
+
+
-- !query
SELECT unix_timestamp(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
-- !query schema
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
b/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
index cfda0007fe93..cbb40a8fcced 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
@@ -26,10 +26,12 @@ import org.apache.spark.sql.test.SharedSparkSession
import org.apache.spark.sql.types._
/**
- * End-to-end tests for the `hour`, `minute` and `second` functions over the
nanosecond-precision
- * timestamp types `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` (`p` in `[7, 9]`),
part of the
- * nanosecond timestamp preview (SPARK-56822). Each test exercises both the
SQL path
- * (`selectExpr`) and the Scala `Column` API (`functions.hour` / `minute` /
`second`).
+ * End-to-end tests over the nanosecond-precision timestamp types
`TIMESTAMP_NTZ(p)` /
+ * `TIMESTAMP_LTZ(p)` (`p` in `[7, 9]`), part of the nanosecond timestamp
preview (SPARK-56822).
+ * Covers the datetime functions (`hour`/`minute`/`second`,
`EXTRACT`/`date_part`, the date-field
+ * functions) and the `MIN`/`MAX` aggregates (plus
`min_by`/`max_by`/`greatest`/`least`). Most
+ * tests use the SQL path (`selectExpr`); several also cross-check the Scala
`Column` API. The two
+ * subclasses run every test with ANSI mode on and off.
*/
abstract class TimestampNanosFunctionsSuiteBase extends SharedSparkSession {
@@ -240,6 +242,166 @@ abstract class TimestampNanosFunctionsSuiteBase extends
SharedSparkSession {
}
}
+ // ===== MIN / MAX aggregates over nanosecond-precision timestamps
(SPARK-56822) =====
+ // `Min`/`Max` are type-agnostic `DeclarativeAggregate`s gated only on
orderability
+ // (`TypeUtils.checkForOrderingExpr`); the nanosecond timestamp types became
orderable in
+ // SPARK-57103, so MIN/MAX (and `min_by`/`max_by`/`greatest`/`least`, which
ride the same gate)
+ // work without any change to the aggregates themselves. These end-to-end
tests lock that in,
+ // mirroring the TimeType precedent (SPARK-52626 group-by, SPARK-52660
codegen split). The result
+ // type preserves the input precision (`dataType = child.dataType`).
Mixed-precision inputs route
+ // through `findWiderDateTimeType`, which has no nanos arm yet, so they are
out of scope here
+ // (SPARK-57454); every column below is strictly same-precision.
+
+ test("SPARK-57103: max/min over nanosecond-precision timestamps preserve the
input type") {
+ Seq(7, 8, 9).foreach { p =>
+ val schema = new StructType()
+ .add("ntz", TimestampNTZNanosType(p))
+ .add("ltz", TimestampLTZNanosType(p))
+ val data = Seq(
+ Row(LocalDateTime.parse("2020-01-01T00:00:01.100000000"),
+ Instant.parse("2020-01-01T00:00:01.100000000Z")),
+ Row(LocalDateTime.parse("2020-01-01T00:00:02.200000000"),
+ Instant.parse("2020-01-01T00:00:02.200000000Z")),
+ Row(LocalDateTime.parse("2020-01-01T00:00:00.300000000"),
+ Instant.parse("2020-01-01T00:00:00.300000000Z")),
+ Row(null, null))
+ val df = spark.createDataFrame(spark.sparkContext.parallelize(data),
schema)
+ val sqlRes = df.selectExpr("max(ntz)", "min(ntz)", "max(ltz)",
"min(ltz)")
+ val colRes = df.select(
+ max(col("ntz")), min(col("ntz")), max(col("ltz")), min(col("ltz")))
+ // The SQL and the Scala Column API agree.
+ checkAnswer(sqlRes, colRes)
+ // Absolute values (NTZ collects to LocalDateTime, LTZ to Instant;
SPARK-57033).
+ checkAnswer(sqlRes, Row(
+ LocalDateTime.parse("2020-01-01T00:00:02.200000000"),
+ LocalDateTime.parse("2020-01-01T00:00:00.300000000"),
+ Instant.parse("2020-01-01T00:00:02.200000000Z"),
+ Instant.parse("2020-01-01T00:00:00.300000000Z")))
+ // The result keeps both the family (NTZ/LTZ) and the precision of the
input.
+ assert(sqlRes.schema.map(_.dataType) === Seq(
+ TimestampNTZNanosType(p), TimestampNTZNanosType(p),
+ TimestampLTZNanosType(p), TimestampLTZNanosType(p)))
+ }
+ }
+
+ test("SPARK-57103: max/min over nanos order by the sub-microsecond
remainder") {
+ // Two values share the same epochMicros and differ only within the
microsecond, so a correct
+ // result must use the full `TimestampNanosVal` comparison and never
truncate to micros.
+ // Run on both the codegen (`CodeGenerator.genComp` AnyTimestampNanoType
arm) and the
+ // interpreted (`Ordering[TimestampNanosVal]`) paths.
+ Seq(
+ Seq(SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+ SQLConf.CODEGEN_FACTORY_MODE.key -> "CODEGEN_ONLY"),
+ Seq(SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "false",
+ SQLConf.CODEGEN_FACTORY_MODE.key -> "NO_CODEGEN")
+ ).foreach { conf =>
+ withSQLConf(conf: _*) {
+ val ntz = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(
+ Row(LocalDateTime.parse("2020-01-01T00:00:00.000000001")),
+ Row(LocalDateTime.parse("2020-01-01T00:00:00.000000999")),
+ Row(null))),
+ new StructType().add("c", TimestampNTZNanosType(9)))
+ checkAnswer(ntz.selectExpr("max(c)", "min(c)"),
+ Row(LocalDateTime.parse("2020-01-01T00:00:00.000000999"),
+ LocalDateTime.parse("2020-01-01T00:00:00.000000001")))
+
+ val ltz = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(
+ Row(Instant.parse("2020-01-01T00:00:00.000000001Z")),
+ Row(Instant.parse("2020-01-01T00:00:00.000000999Z")),
+ Row(null))),
+ new StructType().add("c", TimestampLTZNanosType(9)))
+ checkAnswer(ltz.selectExpr("max(c)", "min(c)"),
+ Row(Instant.parse("2020-01-01T00:00:00.000000999Z"),
+ Instant.parse("2020-01-01T00:00:00.000000001Z")))
+ }
+ }
+ }
+
+ test("SPARK-57103: max/min over all-NULL or empty nanos input return NULL") {
+ Seq(7, 8, 9).foreach { p =>
+ val ntz = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(null), Row(null))),
+ new StructType().add("c", TimestampNTZNanosType(p)))
+ checkAnswer(ntz.selectExpr("max(c)", "min(c)"), Row(null, null))
+ // Global aggregate over zero rows still produces one all-NULL row.
+ checkAnswer(ntz.filter(lit(false)).selectExpr("max(c)", "min(c)"),
Row(null, null))
+
+ val ltz = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(null), Row(null))),
+ new StructType().add("c", TimestampLTZNanosType(p)))
+ checkAnswer(ltz.selectExpr("max(c)", "min(c)"), Row(null, null))
+ }
+ }
+
+ test("SPARK-57103: group by a nanosecond key with per-group max/min") {
+ // The grouping keys k1/k2 share their epochMicros but differ within the
microsecond, so
+ // hashing/grouping (SPARK-57103) must distinguish sub-microsecond keys;
the per-group max/min
+ // then order by the remainder.
+ val schema = new StructType()
+ .add("k", TimestampNTZNanosType(9))
+ .add("v", TimestampLTZNanosType(9))
+ val k1 = "2020-01-01T00:00:00.000000001"
+ val k2 = "2020-01-01T00:00:00.000000002"
+ val data = Seq(
+ Row(LocalDateTime.parse(k1),
Instant.parse("2020-01-01T10:00:00.000000111Z")),
+ Row(LocalDateTime.parse(k1),
Instant.parse("2020-01-01T10:00:00.000000999Z")),
+ Row(LocalDateTime.parse(k2),
Instant.parse("2020-01-01T10:00:00.000000500Z")))
+ val df = spark.createDataFrame(spark.sparkContext.parallelize(data),
schema)
+ val res = df.groupBy("k").agg(max("v").as("mx"),
min("v").as("mn")).orderBy("k")
+ checkAnswer(res, Seq(
+ Row(LocalDateTime.parse(k1),
+ Instant.parse("2020-01-01T10:00:00.000000999Z"),
+ Instant.parse("2020-01-01T10:00:00.000000111Z")),
+ Row(LocalDateTime.parse(k2),
+ Instant.parse("2020-01-01T10:00:00.000000500Z"),
+ Instant.parse("2020-01-01T10:00:00.000000500Z"))))
+ // The two sub-microsecond-distinct keys do not collapse into one group.
+ assert(res.count() === 2)
+ assert(res.schema("k").dataType === TimestampNTZNanosType(9))
+ }
+
+ test("SPARK-57103: min_by/max_by and greatest/least over same-precision
nanos") {
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(
+ Row("early", LocalDateTime.parse("2020-01-01T00:00:00.000000001")),
+ Row("late", LocalDateTime.parse("2020-01-01T00:00:00.000000999")))),
+ new StructType().add("label", StringType).add("ts",
TimestampNTZNanosType(9)))
+ checkAnswer(df.selectExpr("max_by(label, ts)", "min_by(label, ts)"),
Row("late", "early"))
+
+ val df2 = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(
+ LocalDateTime.parse("2020-01-01T00:00:00.000000001"),
+ LocalDateTime.parse("2020-01-01T00:00:00.000000999")))),
+ new StructType()
+ .add("a", TimestampNTZNanosType(9))
+ .add("b", TimestampNTZNanosType(9)))
+ checkAnswer(df2.selectExpr("greatest(a, b)", "least(a, b)"),
+ Row(LocalDateTime.parse("2020-01-01T00:00:00.000000999"),
+ LocalDateTime.parse("2020-01-01T00:00:00.000000001")))
+ }
+
+ test("SPARK-57103: max/min over nanos agree with the micros path when
sub-micro digits are 0") {
+ Seq(7, 8, 9).foreach { p =>
+ val ldts = Seq(
+ "2020-01-01T00:00:01.100000000",
+ "2020-01-01T00:00:02.200000000",
+ "2020-01-01T00:00:00.300000000")
+ val nanos = spark.createDataFrame(
+ spark.sparkContext.parallelize(ldts.map(s =>
Row(LocalDateTime.parse(s)))),
+ new StructType().add("c", TimestampNTZNanosType(p)))
+ val micro = spark.createDataFrame(
+ spark.sparkContext.parallelize(ldts.map(s =>
Row(LocalDateTime.parse(s)))),
+ new StructType().add("c", TimestampNTZType))
+ // Compare via the string rendering so the differing result types (nanos
vs micros) do not
+ // matter; the sub-microsecond digits are all zero, so the values agree.
+ checkAnswer(
+ nanos.selectExpr("cast(max(c) as string)", "cast(min(c) as string)"),
+ micro.selectExpr("cast(max(c) as string)", "cast(min(c) as string)"))
+ }
+ }
+
test("SPARK-57528: unix_timestamp / to_unix_timestamp over
nanosecond-precision timestamps") {
// unix_timestamp returns whole-second BIGINT and applies no zone shift to
a timestamp
// argument, so the sub-second digits are dropped and the nanos result
equals the
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]