This is an automated email from the ASF dual-hosted git repository.
MaxGekk pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 71b1aca729e2 [SPARK-57459][SQL] Support nanosecond-precision timestamp
types in the Avro datasource (v1 and v2)
71b1aca729e2 is described below
commit 71b1aca729e287cd83d94869a10c48b8d65d1acc
Author: Maxim Gekk <[email protected]>
AuthorDate: Sat Jun 27 21:56:47 2026 +0200
[SPARK-57459][SQL] Support nanosecond-precision timestamp types in the Avro
datasource (v1 and v2)
### What changes were proposed in this pull request?
Umbrella: [SPARK-56822](https://issues.apache.org/jira/browse/SPARK-56822)
(Timestamps with nanosecond precision).
This PR adds read and write support for the nanosecond-capable timestamp
types `TIMESTAMP_NTZ(p)` and `TIMESTAMP_LTZ(p)` (`p` in 7-9) to the Avro
datasource (v1 `AvroFileFormat` and v2 `AvroTable`), reaching parity with the
microsecond `TimestampType` / `TimestampNTZType`, and removes the
[SPARK-57166](https://issues.apache.org/jira/browse/SPARK-57166) rejection
guardrail.
- `SchemaConverters`: map `TimestampLTZNanosType` / `TimestampNTZNanosType`
to the Avro `timestamp-nanos` / `local-timestamp-nanos` logical types
(available in the bundled Avro 1.12.1, on a `long` storing epoch-nanoseconds),
carrying the fractional-second precision via the `spark.sql.catalyst.type`
property. The reverse direction maps these logical types back, defaulting to
nanosecond precision (9) for files written by external tools that lack the
property.
- `AvroSerializer`: pack the internal `(epochMicros, nanosWithinMicro)`
value into a single epoch-nanoseconds `Long`
(`DateTimeUtils.timestampNanosToEpochNanos`), surfacing values outside the
signed-int64 epoch-nanos range (~1677-09-21 .. 2262-04-11) as a
`DATETIME_OVERFLOW` error.
- `AvroDeserializer`: unpack epoch-nanoseconds via `floorDiv` / `floorMod`
and truncate the sub-microsecond digits to the declared precision.
- `AvroUtils.supportsDataType`: drop the `AnyTimestampNanoType` rejection
so the types are accepted by both the v1 and v2 write/read paths.
Like the Parquet path, nanosecond timestamps are always proleptic Gregorian
and are therefore exempt from datetime rebasing.
### Why are the changes needed?
To extend nanosecond-precision timestamp support (umbrella SPARK-56822) to
the Avro datasource so it can read and write `TIMESTAMP_NTZ(p)` /
`TIMESTAMP_LTZ(p)` with `p` in 7-9, matching the existing microsecond timestamp
behavior and the Parquet/ORC nanosecond support.
### Does this PR introduce _any_ user-facing change?
Yes. With `spark.sql.timestampNanosTypes.enabled=true`, columns of type
`TIMESTAMP_NTZ(7-9)` / `TIMESTAMP_LTZ(7-9)` can now be written to and read from
Avro files. Previously such columns were rejected with
`UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE`. This is a change within the unreleased
master/branch only.
### How was this patch tested?
Added tests in `AvroSuite`:
- round-trip for precisions 7-9 for both NTZ and LTZ, across the v1 and v2
sources, including nulls and inferred-schema precision preservation;
- external-reader unit-correctness: decode the written file with a plain
Avro `GenericDatumReader` and assert the stored epoch-nanoseconds and the
logical-type name;
- reading a plain Avro file produced without the `spark.sql.catalyst.type`
property (defaults to nanosecond precision);
- writing an out-of-range value fails loudly with `DATETIME_OVERFLOW`.
Ran `AvroV1Suite` / `AvroV2Suite` (new tests pass on both) plus
`AvroSerdeSuite`, `AvroV1/V2LogicalTypeSuite`, and
`AvroCatalystDataConversionSuite` (no regressions), and `sql` / `avro`
scalastyle.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor 2.1, Claude Opus 4.8
Closes #56825 from MaxGekk/nanos-avro.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
(cherry picked from commit 5798db67062c8aa1943273eee944efec63f3f396)
Signed-off-by: Max Gekk <[email protected]>
---
.../org/apache/spark/sql/avro/AvroSuite.scala | 362 +++++++++++++++++++--
docs/sql-data-sources-avro.md | 20 ++
.../spark/sql/catalyst/util/DateTimeUtils.scala | 30 ++
.../sql/catalyst/util/DateTimeUtilsSuite.scala | 46 +++
.../apache/spark/sql/avro/AvroDeserializer.scala | 24 +-
.../org/apache/spark/sql/avro/AvroSerializer.scala | 22 +-
.../org/apache/spark/sql/avro/AvroUtils.scala | 3 -
.../apache/spark/sql/avro/SchemaConverters.scala | 36 ++
.../types/ops/TimestampNanosParquetOps.scala | 28 +-
.../types/ops/TimestampNanosParquetOpsSuite.scala | 9 +-
10 files changed, 511 insertions(+), 69 deletions(-)
diff --git
a/connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala
b/connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala
index 588d7e26206f..813401773834 100644
--- a/connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala
+++ b/connector/avro/src/test/scala/org/apache/spark/sql/avro/AvroSuite.scala
@@ -33,12 +33,12 @@ import org.apache.avro.file.{DataFileReader, DataFileWriter}
import org.apache.avro.generic.{GenericData, GenericDatumReader,
GenericDatumWriter, GenericRecord}
import org.apache.avro.generic.GenericData.{EnumSymbol, Fixed}
-import org.apache.spark.{SPARK_VERSION_SHORT, SparkConf, SparkException,
SparkRuntimeException, SparkThrowable, SparkUpgradeException}
+import org.apache.spark.{SPARK_VERSION_SHORT, SparkArithmeticException,
SparkConf, SparkException, SparkRuntimeException, SparkThrowable,
SparkUpgradeException}
import org.apache.spark.TestUtils.assertExceptionMsg
import org.apache.spark.sql._
import org.apache.spark.sql.TestingUDT.IntervalData
import org.apache.spark.sql.avro.AvroCompressionCodec._
-import org.apache.spark.sql.catalyst.expressions.{AttributeReference, Literal}
+import org.apache.spark.sql.catalyst.expressions.AttributeReference
import org.apache.spark.sql.catalyst.plans.logical.Filter
import org.apache.spark.sql.catalyst.util.DateTimeTestUtils
import
org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{withDefaultTimeZone, LA,
UTC}
@@ -52,7 +52,6 @@ import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.test.SharedSparkSession
import org.apache.spark.sql.types._
import org.apache.spark.sql.v2.avro.AvroScan
-import org.apache.spark.unsafe.types.TimestampNanosVal
import org.apache.spark.util.Utils
abstract class AvroSuite
@@ -3192,46 +3191,337 @@ abstract class AvroSuite
}
}
- test("SPARK-57166: nanosecond timestamp types are not supported in Avro") {
- val nanosTypes = Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(9))
+ test("SPARK-57459: nanosecond timestamp types round-trip through Avro (v1
and v2)") {
withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
- nanosTypes.foreach { nanosType =>
- val expectedType = s""""${nanosType.sql}""""
- withTempDir { dir =>
- // Write path: a nanos-typed column cannot be written. The nanos
literal is built
- // directly from its internal value to avoid relying on cast/parser
support.
- val nanosLiteral = Literal.create(new TimestampNanosVal(0L,
0.toShort), nanosType)
- val df = spark.range(1).select(Column(nanosLiteral).as("ts"))
- val writeDir = new File(dir, "write").getCanonicalPath
- checkError(
- exception = intercept[AnalysisException] {
- df.write.format("avro").mode("overwrite").save(writeDir)
- },
- condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
- parameters = Map(
- "columnName" -> "`ts`",
- "columnType" -> expectedType,
- "format" -> "Avro"))
-
- // Read path: a user-specified nanos schema is rejected. Write a
benign file first
- // so schema validation (not file listing) is what fails.
- val readDir = new File(dir, "read").getCanonicalPath
-
Seq("a").toDF("ts").write.format("avro").mode("overwrite").save(readDir)
+ Seq(true, false).foreach { useV1 =>
+ val useV1List = if (useV1) "avro" else ""
+ withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> useV1List) {
+ Seq(7, 8, 9).foreach { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ withTempDir { dir =>
+ // Build the row from an external java.time value; the
column schema carries the
+ // precision and truncates the sub-microsecond digits,
matching the ORC suites.
+ val wallClock = LocalDateTime.of(1970, 1, 1, 0, 20, 34,
567890123)
+ val value: Any = nanosType match {
+ case _: TimestampNTZNanosType => wallClock
+ case _: TimestampLTZNanosType =>
wallClock.toInstant(java.time.ZoneOffset.UTC)
+ }
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value), Row(null))),
+ new StructType().add("ts", nanosType))
+ val path = new File(dir,
s"avro_nanos_${nanosType.typeName}").getCanonicalPath
+ df.write.format("avro").mode("overwrite").save(path)
+ // The inferred schema preserves the declared precision via
the catalyst prop.
+ val inferred = spark.read.format("avro").load(path)
+ assert(inferred.schema("ts").dataType == nanosType)
+ val readBack = spark.read.schema(new StructType().add("ts",
nanosType))
+ .format("avro").load(path)
+ checkAnswer(readBack, df)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: nanosecond timestamps are written as unit-correct
epoch-nanos") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ // LocalDateTime at the epoch plus 567890123 ns, truncated to each
precision.
+ val wallClock = LocalDateTime.of(1970, 1, 1, 0, 0, 0, 567890123)
+ val expectedNanos = Map(7 -> 567890100L, 8 -> 567890120L, 9 ->
567890123L)
+ Seq(
+ (false, "timestamp-nanos"),
+ (true, "local-timestamp-nanos")).foreach { case (isNtz, logicalName) =>
+ Seq(7, 8, 9).foreach { p =>
+ withTempPath { dir =>
+ val nanosType: DataType =
+ if (isNtz) TimestampNTZNanosType(p) else TimestampLTZNanosType(p)
+ val value: Any =
+ if (isNtz) wallClock else
wallClock.toInstant(java.time.ZoneOffset.UTC)
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value)), numSlices = 1),
+ new StructType().add("t", nanosType))
+ df.write.format("avro").save(dir.toString)
+
+ val avroFile = dir.listFiles()
+ .filter(f => f.isFile && f.getName.endsWith("avro"))
+ .head
+ val reader = new DataFileReader[GenericRecord](
+ avroFile, new GenericDatumReader[GenericRecord]())
+ try {
+ val fieldSchema = reader.getSchema.getField("t").schema()
+ val tsSchema = if (fieldSchema.getType == Type.UNION) {
+ fieldSchema.getTypes.asScala.find(_.getType == Type.LONG).get
+ } else {
+ fieldSchema
+ }
+ assert(tsSchema.getLogicalType.getName == logicalName,
+ s"$nanosType should be written with the $logicalName logical
type")
+ assert(reader.hasNext)
+ val stored = reader.next().get("t").asInstanceOf[Long]
+ assert(stored == expectedNanos(p),
+ s"$nanosType should store epoch-nanos ${expectedNanos(p)}, but
was $stored")
+ } finally {
+ reader.close()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: nanosecond timestamps read from a plain Avro file (no
catalyst prop)") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ // Build Avro files the way an external tool would: a nanos logical type
on a long with no
+ // `spark.sql.catalyst.type` property. Spark must default to nanosecond
precision and convert
+ // the stored epoch-nanoseconds back to its internal (epochMicros,
nanosWithinMicro) form.
+ val epochNanos = 567890123L
+ Seq(
+ ("timestamp-nanos", TimestampLTZNanosType(9),
+ java.time.Instant.ofEpochSecond(0L, epochNanos)),
+ ("local-timestamp-nanos", TimestampNTZNanosType(9),
+ LocalDateTime.of(1970, 1, 1, 0, 0, 0, epochNanos.toInt))).foreach {
+ case (logicalName, expectedType, expectedValue) =>
+ withTempDir { dir =>
+ val avroSchema = new Schema.Parser().parse(
+ s"""
+ |{
+ | "type": "record",
+ | "name": "top",
+ | "fields": [
+ | {"name": "t", "type": {"type": "long", "logicalType":
"$logicalName"}}
+ | ]
+ |}
+ """.stripMargin)
+ val avroFile = new File(dir, "external.avro")
+ val datumWriter = new GenericDatumWriter[GenericRecord](avroSchema)
+ val dataFileWriter = new DataFileWriter[GenericRecord](datumWriter)
+ dataFileWriter.create(avroSchema, avroFile)
+ try {
+ val record = new GenericData.Record(avroSchema)
+ record.put("t", epochNanos)
+ dataFileWriter.append(record)
+ } finally {
+ dataFileWriter.close()
+ }
+
+ val readDf = spark.read.format("avro").load(dir.toString)
+ assert(readDf.schema("t").dataType == expectedType)
+ checkAnswer(readDf, Row(expectedValue))
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: reading a foreign nanos Avro file with an explicit
lower-precision schema") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ // A full-precision (9-digit) foreign value read with an explicit
TIMESTAMP_*(7) schema must
+ // truncate the sub-microsecond digits (123 -> 100), mirroring
ParquetTimestampNanosSuite's
+ // "explicit lower-precision read schema" case.
+ val epochNanos = 567890123L
+ Seq(
+ ("timestamp-nanos", TimestampLTZNanosType(7),
+ java.time.Instant.ofEpochSecond(0L, 567890100L)),
+ ("local-timestamp-nanos", TimestampNTZNanosType(7),
+ LocalDateTime.of(1970, 1, 1, 0, 0, 0, 567890100))).foreach {
+ case (logicalName, readType, expectedValue) =>
+ withTempDir { dir =>
+ val avroSchema = new Schema.Parser().parse(
+ s"""
+ |{
+ | "type": "record",
+ | "name": "top",
+ | "fields": [
+ | {"name": "t", "type": {"type": "long", "logicalType":
"$logicalName"}}
+ | ]
+ |}
+ """.stripMargin)
+ val avroFile = new File(dir, "external.avro")
+ val datumWriter = new GenericDatumWriter[GenericRecord](avroSchema)
+ val dataFileWriter = new DataFileWriter[GenericRecord](datumWriter)
+ dataFileWriter.create(avroSchema, avroFile)
+ try {
+ val record = new GenericData.Record(avroSchema)
+ record.put("t", epochNanos)
+ dataFileWriter.append(record)
+ } finally {
+ dataFileWriter.close()
+ }
+
+ val readDf = spark.read.schema(new StructType().add("t", readType))
+ .format("avro").load(dir.toString)
+ assert(readDf.schema("t").dataType == readType)
+ checkAnswer(readDf, Row(expectedValue))
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: writing an out-of-range nanosecond timestamp fails
loudly") {
+ withSQLConf(
+ SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+ SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") {
+ Seq("TIMESTAMP_NTZ", "TIMESTAMP_LTZ").foreach { typeName =>
+ withTempPath { dir =>
+ // Year 9999 is far outside the signed-int64 epoch-nanos range
(~2262).
+ val df = spark.sql(s"SELECT $typeName '9999-12-31
23:59:59.999999999' AS ts")
+ val e = intercept[SparkException] {
+ df.write.format("avro").save(dir.getCanonicalPath)
+ }
+ var cause: Throwable = e
+ while (cause != null &&
!cause.isInstanceOf[SparkArithmeticException]) {
+ cause = cause.getCause
+ }
+ assert(cause != null,
+ s"Expected a DATETIME_OVERFLOW error for $typeName, but got:
${e.getMessage}")
+ // NTZ renders without a zone; LTZ renders as a UTC instant with a
trailing `Z`.
+ val renderedValue =
+ if (typeName == "TIMESTAMP_NTZ") "9999-12-31T23:59:59.999999999"
+ else "9999-12-31T23:59:59.999999999Z"
checkError(
- exception = intercept[AnalysisException] {
- spark.read.schema(new StructType().add("ts", nanosType))
- .format("avro").load(readDir).collect()
- },
- condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
- parameters = Map(
- "columnName" -> "`ts`",
- "columnType" -> expectedType,
- "format" -> "Avro"))
+ exception = cause.asInstanceOf[SparkArithmeticException],
+ condition = "DATETIME_OVERFLOW",
+ parameters = Map("operation" ->
+ (s"write the timestamp value $renderedValue as Avro
epoch-nanoseconds " +
+ "(supported range: 1677-09-21T00:12:43.145224192Z to " +
+ "2262-04-11T23:47:16.854775807Z)")))
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: nanosecond timestamps round-trip at the maximum supported
instant") {
+ withSQLConf(
+ SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+ SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") {
+ // Long.MaxValue epoch-nanoseconds = 2262-04-11T23:47:16.854775807Z, the
largest instant the
+ // INT64 epoch-nanos encoding can represent. The lower bound
Long.MinValue is intentionally
+ // not exercised: re-encoding its floored epoch-microseconds overflows
Long (see
+ // DateTimeUtilsSuite), so it is not writable.
+ val maxInstant = java.time.Instant.ofEpochSecond(9223372036L, 854775807L)
+ val maxLocal = LocalDateTime.ofEpochSecond(9223372036L, 854775807,
java.time.ZoneOffset.UTC)
+ Seq(
+ (TimestampLTZNanosType(9), maxInstant: Any),
+ (TimestampNTZNanosType(9), maxLocal: Any)).foreach { case (nanosType,
value) =>
+ withTempPath { dir =>
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value)), numSlices = 1),
+ new StructType().add("ts", nanosType))
+ df.write.format("avro").mode("overwrite").save(dir.getCanonicalPath)
+ val readBack = spark.read.schema(new StructType().add("ts",
nanosType))
+ .format("avro").load(dir.getCanonicalPath)
+ checkAnswer(readBack, df)
}
}
}
}
+ test("SPARK-57459: pre-epoch and minimum-instant nanosecond timestamp
round-trips") {
+ withSQLConf(
+ SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+ SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") {
+ // The smallest *writable* instant: Long.MinValue epoch-nanoseconds
floor to an epochMicros
+ // whose re-encode overflows Long, so the practical minimum is the next
whole microsecond up
+ // (~1677-09-21), the symmetric counterpart of the maximum-instant test
above.
+ val minEpochNanos = (Long.MinValue / 1000L) * 1000L
+ // Pre-epoch full-precision value: 1969-12-31T23:59:59.999999999Z (= -1
epoch-nanosecond),
+ // exercising the floor semantics end to end (mirrors the Parquet/ORC
suites).
+ Seq(-1L, minEpochNanos).foreach { epochNanos =>
+ val seconds = Math.floorDiv(epochNanos, 1000000000L)
+ val nanoOfSecond = Math.floorMod(epochNanos, 1000000000L).toInt
+ val instant = java.time.Instant.ofEpochSecond(seconds,
nanoOfSecond.toLong)
+ val localDateTime =
+ LocalDateTime.ofEpochSecond(seconds, nanoOfSecond,
java.time.ZoneOffset.UTC)
+ Seq(
+ (TimestampLTZNanosType(9), instant: Any),
+ (TimestampNTZNanosType(9), localDateTime: Any)).foreach { case
(nanosType, value) =>
+ withTempPath { dir =>
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value)), numSlices = 1),
+ new StructType().add("ts", nanosType))
+
df.write.format("avro").mode("overwrite").save(dir.getCanonicalPath)
+ val readBack = spark.read.schema(new StructType().add("ts",
nanosType))
+ .format("avro").load(dir.getCanonicalPath)
+ checkAnswer(readBack, df)
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: TIMESTAMP_LTZ nanos on-disk value is independent of the
session time zone") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ // TIMESTAMP_LTZ is an absolute instant, so the stored epoch-nanoseconds
and the read-back
+ // value must not depend on the session time zone.
+ val instant = LocalDateTime.of(2024, 6, 15, 12, 34, 56, 789012345)
+ .toInstant(java.time.ZoneOffset.UTC)
+ val schema = new StructType().add("ts", TimestampLTZNanosType(9))
+
+ def writeAndReadStored(zone: String, dir: File): Long = {
+ withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> zone) {
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(instant: Any)), numSlices =
1), schema)
+ df.write.format("avro").mode("overwrite").save(dir.getCanonicalPath)
+ // The instant reads back unchanged regardless of the session zone.
+ val readBack =
spark.read.schema(schema).format("avro").load(dir.getCanonicalPath)
+ checkAnswer(readBack, Row(instant))
+ }
+ val avroFile = dir.listFiles()
+ .filter(f => f.isFile && f.getName.endsWith("avro"))
+ .head
+ val reader = new DataFileReader[GenericRecord](
+ avroFile, new GenericDatumReader[GenericRecord]())
+ try {
+ val fieldSchema = reader.getSchema.getField("ts").schema()
+ val tsSchema = if (fieldSchema.getType == Type.UNION) {
+ fieldSchema.getTypes.asScala.find(_.getType == Type.LONG).get
+ } else {
+ fieldSchema
+ }
+ assert(tsSchema.getLogicalType.getName == "timestamp-nanos")
+ reader.next().get("ts").asInstanceOf[Long]
+ } finally {
+ reader.close()
+ }
+ }
+
+ withTempPath { utcDir =>
+ withTempPath { laDir =>
+ val storedUtc = writeAndReadStored("UTC", utcDir)
+ val storedLa = writeAndReadStored("America/Los_Angeles", laDir)
+ assert(storedUtc === storedLa,
+ "TIMESTAMP_LTZ epoch-nanoseconds must not depend on the session
time zone")
+ }
+ }
+ }
+ }
+
+ test("SPARK-57459: nanosecond timestamp types in nested and complex Avro
structures") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val ntz = LocalDateTime.of(2024, 1, 2, 3, 4, 5, 123456789)
+ val ltz = ntz.toInstant(java.time.ZoneOffset.UTC)
+ val schema = new StructType()
+ .add("s", new StructType()
+ .add("a", TimestampNTZNanosType(9))
+ .add("b", TimestampLTZNanosType(8)))
+ .add("arr", ArrayType(TimestampNTZNanosType(7)))
+ .add("m", MapType(StringType, TimestampLTZNanosType(9)))
+ val row = Row(
+ Row(ntz, ltz),
+ Seq(ntz, null, ntz),
+ Map("k1" -> ltz, "k2" -> null))
+ withTempPath { dir =>
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(row), numSlices = 1), schema)
+ df.write.format("avro").mode("overwrite").save(dir.getCanonicalPath)
+ val readBack =
spark.read.schema(schema).format("avro").load(dir.getCanonicalPath)
+ checkAnswer(readBack, df)
+ }
+ }
+ }
+
test("TIME type read/write with Avro format") {
withTempPath { dir =>
// Test boundary values and NULL handling
diff --git a/docs/sql-data-sources-avro.md b/docs/sql-data-sources-avro.md
index a55cb00f9bd0..e786e7f2b9ef 100644
--- a/docs/sql-data-sources-avro.md
+++ b/docs/sql-data-sources-avro.md
@@ -565,6 +565,16 @@ It also supports reading the following Avro [logical
types](https://avro.apache.
<td>long</td>
<td>TimeType</td>
</tr>
+ <tr>
+ <td>timestamp-nanos</td>
+ <td>long</td>
+ <td>TimestampType(p) (with p in 7-9, requires
<code>spark.sql.timestampNanosTypes.enabled=true</code>)</td>
+ </tr>
+ <tr>
+ <td>local-timestamp-nanos</td>
+ <td>long</td>
+ <td>TimestampNTZType(p) (with p in 7-9, requires
<code>spark.sql.timestampNanosTypes.enabled=true</code>)</td>
+ </tr>
<tr>
<td>decimal</td>
<td>fixed</td>
@@ -613,6 +623,16 @@ Spark supports writing of all Spark SQL types into Avro.
For most types, the map
<td>long</td>
<td>time-micros</td>
</tr>
+ <tr>
+ <td>TimestampType(p) (with p in 7-9)</td>
+ <td>long</td>
+ <td>timestamp-nanos</td>
+ </tr>
+ <tr>
+ <td>TimestampNTZType(p) (with p in 7-9)</td>
+ <td>long</td>
+ <td>local-timestamp-nanos</td>
+ </tr>
<tr>
<td>DecimalType</td>
<td>fixed</td>
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
index 86cd01a0f3b5..6fcb6c3075ed 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
@@ -331,6 +331,36 @@ object DateTimeUtils extends SparkDateTimeUtils {
value.nanosWithinMicro.toLong)
}
+ /**
+ * Packs a [[TimestampNanosVal]] into a single int64 of epoch-nanoseconds
for a `sink` that uses
+ * that encoding (the Parquet INT64 and Avro `timestamp-nanos` /
`local-timestamp-nanos` physical
+ * types), translating the int64 overflow thrown by
[[timestampNanosToEpochNanos]] into a
+ * `DATETIME_OVERFLOW` error that names the `sink`. `isNtz` selects how the
offending value is
+ * rendered in that error (a zone-less local date-time vs. a UTC instant).
+ */
+ def timestampNanosToEpochNanos(value: TimestampNanosVal, isNtz: Boolean,
sink: String): Long = {
+ try {
+ timestampNanosToEpochNanos(value)
+ } catch {
+ case _: ArithmeticException =>
+ throw
QueryExecutionErrors.timestampNanosEpochNanosOverflowError(value, isNtz, sink)
+ }
+ }
+
+ /**
+ * Unpacks a single int64 of nanoseconds since the epoch (the representation
used by the Arrow
+ * nanosecond timestamp vectors and the Parquet / Avro INT64
epoch-nanoseconds encodings) back
+ * into a [[TimestampNanosVal]], truncating the sub-microsecond digits to
the given `precision`
+ * (in [7, 9]). This is the inverse of [[timestampNanosToEpochNanos]].
`floorDiv` / `floorMod`
+ * keep `nanosWithinMicro` in [0, 999] for pre-epoch (negative) values too.
+ */
+ def epochNanosToTimestampNanos(epochNanos: Long, precision: Int):
TimestampNanosVal = {
+ val epochMicros = Math.floorDiv(epochNanos, NANOS_PER_MICROS)
+ val rawNanosWithinMicro = Math.floorMod(epochNanos, NANOS_PER_MICROS).toInt
+ val nanosWithinMicro =
truncateNanosWithinMicroToPrecision(rawNanosWithinMicro, precision)
+ TimestampNanosVal.fromParts(epochMicros, nanosWithinMicro.toShort)
+ }
+
/**
* Adds a full interval (months, days, microseconds) to a timestamp
represented as the number of
* microseconds since 1970-01-01 00:00:00Z.
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
index 34fc5286722c..d07265c26e97 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
@@ -1232,6 +1232,52 @@ class DateTimeUtilsSuite extends SparkFunSuite with
Matchers with SQLHelper {
}
}
+ test("SPARK-57459: epochNanosToTimestampNanos unpacks int64
epoch-nanoseconds") {
+ def nanos(epochMicros: Long, nanosWithinMicro: Int): TimestampNanosVal =
+ TimestampNanosVal.fromParts(epochMicros, nanosWithinMicro.toShort)
+
+ // At full (nanosecond) precision it is the exact inverse of
timestampNanosToEpochNanos.
+ assert(epochNanosToTimestampNanos(0L, 9) === nanos(0L, 0))
+ assert(epochNanosToTimestampNanos(999L, 9) === nanos(0L, 999))
+ assert(epochNanosToTimestampNanos(NANOS_PER_MICROS, 9) === nanos(1L, 0))
+ assert(epochNanosToTimestampNanos(1234567L * NANOS_PER_MICROS + 7L, 9) ===
nanos(1234567L, 7))
+ // Pre-epoch values use floor semantics, keeping nanosWithinMicro in [0,
999].
+ assert(epochNanosToTimestampNanos(-1L, 9) === nanos(-1L, 999))
+ assert(epochNanosToTimestampNanos(-NANOS_PER_MICROS, 9) === nanos(-1L, 0))
+
+ // Lower precisions truncate the sub-microsecond digits.
+ assert(epochNanosToTimestampNanos(123456789L, 9) === nanos(123456L, 789))
+ assert(epochNanosToTimestampNanos(123456789L, 8) === nanos(123456L, 780))
+ assert(epochNanosToTimestampNanos(123456789L, 7) === nanos(123456L, 700))
+
+ // Truncation operates on the floored nanosWithinMicro, so it composes
with floor semantics for
+ // pre-epoch values too (-123456211 -> floor (-123457, 789) -> truncate
the 789).
+ assert(epochNanosToTimestampNanos(-123456211L, 9) === nanos(-123457L, 789))
+ assert(epochNanosToTimestampNanos(-123456211L, 8) === nanos(-123457L, 780))
+ assert(epochNanosToTimestampNanos(-123456211L, 7) === nanos(-123457L, 700))
+
+ // The int64 extremes decode without overflow: floor keeps
nanosWithinMicro in [0, 999].
+ assert(epochNanosToTimestampNanos(Long.MaxValue, 9) ===
nanos(9223372036854775L, 807))
+ assert(epochNanosToTimestampNanos(Long.MinValue, 9) ===
nanos(-9223372036854776L, 192))
+
+ // Round-trips with timestampNanosToEpochNanos at full precision.
Long.MinValue is excluded: its
+ // decode (-9223372036854776, 192) re-encodes through an intermediate
epochMicros * 1000 that
+ // overflows Long, an existing limitation of the multiplyExact-based pack
path.
+ Seq(
+ 0L, 999L, 1234567L * NANOS_PER_MICROS + 7L, -1234567L * NANOS_PER_MICROS
+ 13L,
+ Long.MaxValue).foreach { epochNanos =>
+ assert(timestampNanosToEpochNanos(epochNanosToTimestampNanos(epochNanos,
9)) === epochNanos)
+ }
+
+ // Precision outside [7, 9] is an internal-only contract violation and
fails loudly.
+ checkError(
+ exception = intercept[SparkException] {
+ epochNanosToTimestampNanos(123456789L, 6)
+ },
+ condition = "INTERNAL_ERROR",
+ parameters = Map("message" -> "Fractional second precision 6 is out of
range [7, 9]."))
+ }
+
test("SPARK-34903: subtract timestamps") {
DateTimeTestUtils.outstandingZoneIds.foreach { zid =>
Seq(
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala
b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala
index ba574c091dae..ce16c4a2cc3a 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroDeserializer.scala
@@ -25,7 +25,7 @@ import scala.jdk.CollectionConverters._
import org.apache.avro.{LogicalTypes, Schema, SchemaBuilder}
import org.apache.avro.Conversions.DecimalConversion
-import org.apache.avro.LogicalTypes.{LocalTimestampMicros,
LocalTimestampMillis, TimestampMicros, TimestampMillis}
+import org.apache.avro.LogicalTypes.{LocalTimestampMicros,
LocalTimestampMillis, LocalTimestampNanos, TimestampMicros, TimestampMillis,
TimestampNanos}
import org.apache.avro.Schema.Type._
import org.apache.avro.generic._
import org.apache.avro.util.Utf8
@@ -213,6 +213,28 @@ private[sql] class AvroDeserializer(
s"Avro logical type $other cannot be converted to SQL type
${TimeType().sql}.")
}
+ case (LONG, t: TimestampLTZNanosType) => avroType.getLogicalType match {
+ // The timestamp-nanos logical type stores epoch-nanoseconds (Long),
while the value is
+ // represented internally as (epochMicros, nanosWithinMicro). Floor
semantics keep
+ // nanosWithinMicro in [0, 999] for pre-epoch values. Nanos timestamps
are always proleptic
+ // Gregorian, so they are exempt from datetime rebasing.
+ case _: TimestampNanos => (updater, ordinal, value) =>
+ updater.set(ordinal,
+ DateTimeUtils.epochNanosToTimestampNanos(value.asInstanceOf[Long],
t.precision))
+ case other => throw new IncompatibleSchemaException(errorPrefix +
+ s"Avro logical type $other cannot be converted to SQL type " +
+ s"${TimestampLTZNanosType().sql}.")
+ }
+
+ case (LONG, t: TimestampNTZNanosType) => avroType.getLogicalType match {
+ case _: LocalTimestampNanos => (updater, ordinal, value) =>
+ updater.set(ordinal,
+ DateTimeUtils.epochNanosToTimestampNanos(value.asInstanceOf[Long],
t.precision))
+ case other => throw new IncompatibleSchemaException(errorPrefix +
+ s"Avro logical type $other cannot be converted to SQL type " +
+ s"${TimestampNTZNanosType().sql}.")
+ }
+
// Before we upgrade Avro to 1.8 for logical type support, spark-avro
converts Long to Date.
// For backward compatibility, we still keep this conversion.
case (LONG, DateType) => (updater, ordinal, value) =>
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroSerializer.scala
b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroSerializer.scala
index aacf8dc9f347..d5405f69ff05 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroSerializer.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroSerializer.scala
@@ -23,7 +23,7 @@ import scala.jdk.CollectionConverters._
import org.apache.avro.{LogicalTypes, Schema}
import org.apache.avro.Conversions.DecimalConversion
-import org.apache.avro.LogicalTypes.{LocalTimestampMicros,
LocalTimestampMillis, TimestampMicros, TimestampMillis}
+import org.apache.avro.LogicalTypes.{LocalTimestampMicros,
LocalTimestampMillis, LocalTimestampNanos, TimestampMicros, TimestampMillis,
TimestampNanos}
import org.apache.avro.Schema.Type
import org.apache.avro.Schema.Type._
import org.apache.avro.generic.GenericData.{EnumSymbol, Fixed, Record}
@@ -201,6 +201,26 @@ private[sql] class AvroSerializer(
s"SQL type ${TimeType().sql} cannot be converted to Avro logical
type $other")
}
+ case (_: TimestampLTZNanosType, LONG) => avroType.getLogicalType match {
+ // Nanosecond-precision timestamps are stored as epoch-nanoseconds
(Long). They are always
+ // proleptic Gregorian, so they are exempt from datetime rebasing.
+ case _: TimestampNanos => (getter, ordinal) =>
+ DateTimeUtils.timestampNanosToEpochNanos(
+ getter.getTimestampLTZNanos(ordinal), isNtz = false, sink = "Avro")
+ case other => throw new IncompatibleSchemaException(errorPrefix +
+ s"SQL type ${TimestampLTZNanosType().sql} cannot be converted to " +
+ s"Avro logical type $other")
+ }
+
+ case (_: TimestampNTZNanosType, LONG) => avroType.getLogicalType match {
+ case _: LocalTimestampNanos => (getter, ordinal) =>
+ DateTimeUtils.timestampNanosToEpochNanos(
+ getter.getTimestampNTZNanos(ordinal), isNtz = true, sink = "Avro")
+ case other => throw new IncompatibleSchemaException(errorPrefix +
+ s"SQL type ${TimestampNTZNanosType().sql} cannot be converted to " +
+ s"Avro logical type $other")
+ }
+
case (ArrayType(et, containsNull), ARRAY) =>
val elementConverter = newConverter(
et, resolveNullableType(avroType.getElementType, containsNull),
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroUtils.scala
b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroUtils.scala
index b8d2c30b0838..9b6ed4d77a6d 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroUtils.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/avro/AvroUtils.scala
@@ -123,9 +123,6 @@ private[sql] object AvroUtils extends Logging {
case _: GeometryType | _: GeographyType => false
- // Nanosecond-capable timestamps are not yet supported by this datasource.
- case _: AnyTimestampNanoType => false
-
case _: AtomicType => true
case st: StructType => st.forall { f => supportsDataType(f.dataType) }
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/avro/SchemaConverters.scala
b/sql/core/src/main/scala/org/apache/spark/sql/avro/SchemaConverters.scala
index 590eaeac6008..c6fba163309f 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/avro/SchemaConverters.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/avro/SchemaConverters.scala
@@ -131,6 +131,26 @@ object SchemaConverters extends Logging {
case _: TimestampMillis | _: TimestampMicros =>
SchemaType(TimestampType, nullable = false)
case _: LocalTimestampMillis | _: LocalTimestampMicros =>
SchemaType(TimestampNTZType, nullable = false)
+ case _: TimestampNanos =>
+ // Avro stores nanoseconds-since-epoch in a long. The precision
(7-9) is carried via the
+ // spark.sql.catalyst.type property; external files without it
default to nanoseconds.
+ val catalystTypeAttrValue =
avroSchema.getProp(CATALYST_TYPE_PROP_NAME)
+ val nanosType = if (catalystTypeAttrValue == null) {
+ TimestampLTZNanosType()
+ } else {
+ CatalystSqlParser.parseDataType(catalystTypeAttrValue)
+ .asInstanceOf[TimestampLTZNanosType]
+ }
+ SchemaType(nanosType, nullable = false)
+ case _: LocalTimestampNanos =>
+ val catalystTypeAttrValue =
avroSchema.getProp(CATALYST_TYPE_PROP_NAME)
+ val nanosType = if (catalystTypeAttrValue == null) {
+ TimestampNTZNanosType()
+ } else {
+ CatalystSqlParser.parseDataType(catalystTypeAttrValue)
+ .asInstanceOf[TimestampNTZNanosType]
+ }
+ SchemaType(nanosType, nullable = false)
case _: LogicalTypes.TimeMicros =>
// Falls back to default precision for backward compatibility with
// Avro files written by external tools.
@@ -334,6 +354,14 @@ object SchemaConverters extends Logging {
LogicalTypes.timestampMicros().addToSchema(builder.longType())
case TimestampNTZType =>
LogicalTypes.localTimestampMicros().addToSchema(builder.longType())
+ case t: TimestampLTZNanosType =>
+ val tsSchema =
LogicalTypes.timestampNanos().addToSchema(builder.longType())
+ tsSchema.addProp(CATALYST_TYPE_PROP_NAME, t.typeName)
+ tsSchema
+ case t: TimestampNTZNanosType =>
+ val tsSchema =
LogicalTypes.localTimestampNanos().addToSchema(builder.longType())
+ tsSchema.addProp(CATALYST_TYPE_PROP_NAME, t.typeName)
+ tsSchema
case t: TimeType =>
val timeSchema =
LogicalTypes.timeMicros().addToSchema(builder.longType())
timeSchema.addProp(CATALYST_TYPE_PROP_NAME, t.typeName)
@@ -466,6 +494,14 @@ object SchemaConverters extends Logging {
case DateType => LogicalTypes.date().addToSchema(builder.intType())
case TimestampType =>
LogicalTypes.timestampMicros().addToSchema(builder.longType())
case TimestampNTZType =>
LogicalTypes.localTimestampMicros().addToSchema(builder.longType())
+ case t: TimestampLTZNanosType =>
+ val tsSchema =
LogicalTypes.timestampNanos().addToSchema(builder.longType())
+ tsSchema.addProp(CATALYST_TYPE_PROP_NAME, t.typeName)
+ tsSchema
+ case t: TimestampNTZNanosType =>
+ val tsSchema =
LogicalTypes.localTimestampNanos().addToSchema(builder.longType())
+ tsSchema.addProp(CATALYST_TYPE_PROP_NAME, t.typeName)
+ tsSchema
case d: DecimalType =>
val avroType = LogicalTypes.decimal(d.precision, d.scale)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala
index 3359ba390809..4b5f01d17374 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala
@@ -24,7 +24,7 @@ import
org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64
import org.apache.parquet.schema.Type.Repetition
import org.apache.spark.sql.catalyst.expressions.SpecializedGetters
-import org.apache.spark.sql.catalyst.util.{DateTimeConstants, DateTimeUtils}
+import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.errors.QueryExecutionErrors
import
org.apache.spark.sql.execution.datasources.parquet.{HasParentContainerUpdater,
ParentContainerUpdater, ParquetPrimitiveConverter}
import org.apache.spark.sql.types.{DataType, TimestampLTZNanosType,
TimestampNTZNanosType}
@@ -96,7 +96,8 @@ private[parquet] trait TimestampNanosParquetOps extends
ParquetTypeOps {
// RecordConsumer is null during init() and set later in prepareForWrite().
(row: SpecializedGetters, ordinal: Int) =>
recordConsumer().addLong(
- TimestampNanosParquetOps.timestampNanosToEpochNanos(getNanos(row,
ordinal), isNtz))
+ DateTimeUtils.timestampNanosToEpochNanos(
+ getNanos(row, ordinal), isNtz, sink = "Parquet INT64"))
// ==================== Row-Based Read ====================
@@ -115,12 +116,7 @@ private[parquet] trait TimestampNanosParquetOps extends
ParquetTypeOps {
val p = precision
new ParquetPrimitiveConverter(updater) {
override def addLong(value: Long): Unit = {
- val epochMicros = Math.floorDiv(value,
DateTimeConstants.NANOS_PER_MICROS)
- val rawNanosWithinMicro =
- Math.floorMod(value, DateTimeConstants.NANOS_PER_MICROS).toInt
- val nanosWithinMicro =
-
DateTimeUtils.truncateNanosWithinMicroToPrecision(rawNanosWithinMicro, p)
- this.updater.set(TimestampNanosVal.fromParts(epochMicros,
nanosWithinMicro.toShort))
+ this.updater.set(DateTimeUtils.epochNanosToTimestampNanos(value, p))
}
}
}
@@ -170,20 +166,4 @@ private[ops] object TimestampNanosParquetOps {
case ts: TimestampLogicalTypeAnnotation => ts.getUnit == TimeUnit.NANOS
case _ => false
})
-
- /**
- * Combines the `(epochMicros, nanosWithinMicro)` pair into a single INT64
epoch-nanoseconds
- * value for Parquet storage. Delegates the exact-arithmetic packing to
- * [[DateTimeUtils.timestampNanosToEpochNanos]]; values outside the
signed-int64 epoch-nanos
- * range (~1677-09-21 .. 2262-04-11) throw
`timestampNanosEpochNanosOverflowError`.
- */
- private[ops] def timestampNanosToEpochNanos(value: TimestampNanosVal, isNtz:
Boolean): Long = {
- try {
- DateTimeUtils.timestampNanosToEpochNanos(value)
- } catch {
- case _: ArithmeticException =>
- throw QueryExecutionErrors.timestampNanosEpochNanosOverflowError(
- value, isNtz, sink = "Parquet INT64")
- }
- }
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOpsSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOpsSuite.scala
index 4d08efb77c93..deade76358fe 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOpsSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOpsSuite.scala
@@ -24,7 +24,7 @@ import
org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64
import org.apache.parquet.schema.Type.Repetition.REQUIRED
import org.apache.spark.{SparkArithmeticException, SparkFunSuite,
SparkRuntimeException}
-import org.apache.spark.sql.catalyst.util.DateTimeConstants
+import org.apache.spark.sql.catalyst.util.{DateTimeConstants, DateTimeUtils}
import
org.apache.spark.sql.execution.datasources.parquet.ParentContainerUpdater
import org.apache.spark.sql.types.{TimestampLTZNanosType,
TimestampNTZNanosType}
import org.apache.spark.unsafe.types.TimestampNanosVal
@@ -118,8 +118,9 @@ class TimestampNanosParquetOpsSuite extends SparkFunSuite {
test("timestampNanosToEpochNanos combines micros and sub-micro nanos") {
val value = TimestampNanosVal.fromParts(1000000L, 500.toShort)
- assert(TimestampNanosParquetOps.timestampNanosToEpochNanos(value, isNtz =
false) ===
- 1000000L * DateTimeConstants.NANOS_PER_MICROS + 500L)
+ val packed = DateTimeUtils.timestampNanosToEpochNanos(
+ value, isNtz = false, sink = "Parquet INT64")
+ assert(packed === 1000000L * DateTimeConstants.NANOS_PER_MICROS + 500L)
}
test("timestampNanosToEpochNanos throws DATETIME_OVERFLOW outside the INT64
epoch-nanos range") {
@@ -128,7 +129,7 @@ class TimestampNanosParquetOpsSuite extends SparkFunSuite {
val tooLarge = TimestampNanosVal.fromParts(100000000000000000L, 0.toShort)
Seq(true, false).foreach { isNtz =>
val ex = intercept[SparkArithmeticException] {
- TimestampNanosParquetOps.timestampNanosToEpochNanos(tooLarge, isNtz)
+ DateTimeUtils.timestampNanosToEpochNanos(tooLarge, isNtz, sink =
"Parquet INT64")
}
assert(ex.getCondition === "DATETIME_OVERFLOW")
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]