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 8bb224895e95 [SPARK-57455][SQL] Support nanosecond timestamp types in
ORC
8bb224895e95 is described below
commit 8bb224895e957f1cb4d279a213003c1d5ddd1aaa
Author: Maxim Gekk <[email protected]>
AuthorDate: Thu Jun 18 13:17:03 2026 +0200
[SPARK-57455][SQL] Support nanosecond timestamp types in ORC
### What changes were proposed in this pull request?
Add ORC read/write support for nanosecond timestamp types
(`TimestampNTZNanosType(p)` and `TimestampLTZNanosType(p)`, `p` in 7..9) across
native ORC (v1/v2, vectorized and non-vectorized) and the Hive ORC serde paths.
The Spark SQL types map onto ORC column types as follows:
| Spark SQL type | ORC column type | `spark.sql.catalyst.type` attribute |
|---|---|---|
| `TimestampType` (micros) | `TIMESTAMP` | `timestamp` |
| `TimestampNTZType` (micros) | `TIMESTAMP` | `timestamp_ntz` |
| `TimestampNTZNanosType(p)` | `TIMESTAMP` | `timestamp_ntz(p)` |
| `TimestampLTZNanosType(p)` | `TIMESTAMP_INSTANT` | `timestamp_ltz(p)` |
The ORC physical category is shared with the existing micros types
(wall-clock `TIMESTAMP` for NTZ, instant-based `TIMESTAMP_INSTANT` for LTZ),
and the exact Spark type/precision is carried in the `spark.sql.catalyst.type`
ORC attribute. On read, that attribute restores the nanos Catalyst type; a
plain ORC `TIMESTAMP` without it stays `TimestampType` for backward
compatibility.
### Why are the changes needed?
ORC rejected nanos timestamp types in its datasource capability checks and
lacked the conversions to round-trip them, so these columns could not be
written or read through ORC.
### Does this PR introduce _any_ user-facing change?
Yes. Users can write and read `TimestampNTZNanosType(p)` /
`TimestampLTZNanosType(p)` (`p` in 7..9) with ORC in datasource v1/v2 and Hive
ORC, including inside nested struct/array/map columns.
### How was this patch tested?
New tests in `OrcQuerySuite` (v1/v2) and `HiveOrcSourceSuite` covering
precisions 7/8/9:
- vectorized vs non-vectorized read parity;
- time-zone independence (NTZ wall clock and LTZ instant preserved across a
JVM default-zone change);
- schema inference from the ORC attribute;
- documented min/max range round-trip;
- nested/complex types (struct/array/map);
- backward compatibility (plain ORC `timestamp` stays `TimestampType`).
```
build/sbt 'sql/testOnly *.orc.OrcV1QuerySuite *.orc.OrcV2QuerySuite -- -z
"SPARK-57455"'
build/sbt 'hive/testOnly *.orc.HiveOrcSourceSuite -- -z "SPARK-57455"'
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Codex 5.3
Closes #56557 from MaxGekk/spark-57455-orc-nanos.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
(cherry picked from commit bdc42527b2045e87452cf307ab527fdbdecf152e)
Signed-off-by: Max Gekk <[email protected]>
---
.../spark/sql/errors/QueryExecutionErrors.scala | 8 ++
.../datasources/orc/OrcAtomicColumnVector.java | 29 +++++
.../datasources/orc/OrcDeserializer.scala | 10 ++
.../execution/datasources/orc/OrcFileFormat.scala | 3 -
.../execution/datasources/orc/OrcSerializer.scala | 14 ++
.../sql/execution/datasources/orc/OrcUtils.scala | 46 ++++++-
.../execution/datasources/v2/orc/OrcTable.scala | 3 -
.../spark/sql/FileBasedDataSourceSuite.scala | 42 +++++-
.../sql/errors/QueryExecutionErrorsSuite.scala | 50 ++++++-
.../execution/datasources/orc/OrcQuerySuite.scala | 143 +++++++++++++++++++++
.../execution/datasources/orc/OrcSourceSuite.scala | 26 ++++
.../sql/execution/datasources/orc/OrcTest.scala | 17 ++-
.../org/apache/spark/sql/hive/HiveInspectors.scala | 120 ++++++++++++++++-
.../org/apache/spark/sql/hive/TableReader.scala | 16 ++-
.../apache/spark/sql/hive/orc/OrcFileFormat.scala | 42 +++++-
.../spark/sql/hive/orc/HiveOrcSourceSuite.scala | 135 ++++++++++++++-----
16 files changed, 642 insertions(+), 62 deletions(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index 4b50a90a30f7..259dce745662 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -2543,6 +2543,14 @@ private[sql] object QueryExecutionErrors extends
QueryErrorsBase with ExecutionE
"toType" -> toSQLType(TimestampType)))
}
+ def cannotCastOrcTimestampError(orcType: DataType, toType: DataType):
Throwable = {
+ new SparkUnsupportedOperationException(
+ errorClass = "UNSUPPORTED_FEATURE.ORC_TYPE_CAST",
+ messageParameters = Map(
+ "orcType" -> toSQLType(orcType),
+ "toType" -> toSQLType(toType)))
+ }
+
def writePartitionExceedConfigSizeWhenDynamicPartitionError(
numWrittenParts: Int,
maxDynamicPartitions: Int,
diff --git
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/orc/OrcAtomicColumnVector.java
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/orc/OrcAtomicColumnVector.java
index 36e5da64bb75..3ea79f83ac53 100644
---
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/orc/OrcAtomicColumnVector.java
+++
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/orc/OrcAtomicColumnVector.java
@@ -27,9 +27,12 @@ import org.apache.spark.sql.catalyst.util.RebaseDateTime;
import org.apache.spark.sql.types.DataType;
import org.apache.spark.sql.types.DateType;
import org.apache.spark.sql.types.Decimal;
+import org.apache.spark.sql.types.TimestampLTZNanosType;
+import org.apache.spark.sql.types.TimestampNTZNanosType;
import org.apache.spark.sql.types.TimestampType;
import org.apache.spark.sql.vectorized.ColumnarArray;
import org.apache.spark.sql.vectorized.ColumnarMap;
+import org.apache.spark.unsafe.types.TimestampNanosVal;
import org.apache.spark.unsafe.types.UTF8String;
/**
@@ -37,6 +40,8 @@ import org.apache.spark.unsafe.types.UTF8String;
*/
public class OrcAtomicColumnVector extends OrcColumnVector {
private final boolean isTimestamp;
+ private final boolean isTimestampNTZNanos;
+ private final boolean isTimestampLTZNanos;
private final boolean isDate;
// Column vector for each type. Only 1 is populated for any type.
@@ -54,6 +59,8 @@ public class OrcAtomicColumnVector extends OrcColumnVector {
} else {
isTimestamp = false;
}
+ isTimestampNTZNanos = type instanceof TimestampNTZNanosType;
+ isTimestampLTZNanos = type instanceof TimestampLTZNanosType;
if (type instanceof DateType) {
isDate = true;
@@ -111,6 +118,28 @@ public class OrcAtomicColumnVector extends OrcColumnVector
{
}
}
+ @Override
+ public TimestampNanosVal getTimestampNTZNanos(int rowId) {
+ if (!isTimestampNTZNanos || isNullAt(rowId)) {
+ return null;
+ }
+ int index = getRowIndex(rowId);
+ return DateTimeUtils.localDateTimeToTimestampNanos(
+ timestampData.asScratchTimestamp(index).toLocalDateTime(),
+ ((TimestampNTZNanosType) type).precision());
+ }
+
+ @Override
+ public TimestampNanosVal getTimestampLTZNanos(int rowId) {
+ if (!isTimestampLTZNanos || isNullAt(rowId)) {
+ return null;
+ }
+ int index = getRowIndex(rowId);
+ return DateTimeUtils.instantToTimestampNanos(
+ timestampData.asScratchTimestamp(index).toInstant(),
+ ((TimestampLTZNanosType) type).precision());
+ }
+
@Override
public float getFloat(int rowId) {
return (float) doubleData.vector[getRowIndex(rowId)];
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcDeserializer.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcDeserializer.scala
index 861e271acd3c..04dd37dec50d 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcDeserializer.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcDeserializer.scala
@@ -149,6 +149,16 @@ class OrcDeserializer(
case TimestampType => (ordinal, value) =>
updater.setLong(ordinal,
DateTimeUtils.fromJavaTimestamp(value.asInstanceOf[OrcTimestamp]))
+ case t: TimestampLTZNanosType => (ordinal, value) =>
+ val ts = value.asInstanceOf[OrcTimestamp]
+ val instant = ts.toInstant
+ updater.set(ordinal, DateTimeUtils.instantToTimestampNanos(instant,
t.precision))
+ case t: TimestampNTZNanosType => (ordinal, value) =>
+ val ts = value.asInstanceOf[OrcTimestamp]
+ val localDateTime = ts.toLocalDateTime
+ updater.set(
+ ordinal,
+ DateTimeUtils.localDateTimeToTimestampNanos(localDateTime,
t.precision))
case DecimalType.Fixed(precision, scale) => (ordinal, value) =>
val v = OrcShimUtils.getDecimal(value)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcFileFormat.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcFileFormat.scala
index 7065693e59ca..633ac8107f32 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcFileFormat.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcFileFormat.scala
@@ -251,9 +251,6 @@ class OrcFileFormat
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 => supportDataType(f.dataType) }
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcSerializer.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcSerializer.scala
index dc124dc2f7c9..64bc3e429212 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcSerializer.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcSerializer.scala
@@ -28,6 +28,7 @@ import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.SpecializedGetters
import org.apache.spark.sql.catalyst.util._
import org.apache.spark.sql.types._
+import org.apache.spark.unsafe.types.TimestampNanosVal
/**
* A serializer to serialize Spark rows to ORC structs.
@@ -154,6 +155,19 @@ class OrcSerializer(dataSchema: StructType) {
val result = new OrcTimestamp(ts.getTime)
result.setNanos(ts.getNanos)
result
+ case t: TimestampLTZNanosType => (getter, ordinal) =>
+ val v = getter.get(ordinal, t).asInstanceOf[TimestampNanosVal]
+ val instant = DateTimeUtils.timestampNanosToInstant(v)
+ val result = new OrcTimestamp(instant.toEpochMilli)
+ result.setNanos(instant.getNano)
+ result
+ case t: TimestampNTZNanosType => (getter, ordinal) =>
+ val v = getter.get(ordinal, t).asInstanceOf[TimestampNanosVal]
+ val localDateTime = DateTimeUtils.timestampNanosToLocalDateTime(v)
+ val ts = java.sql.Timestamp.valueOf(localDateTime)
+ val result = new OrcTimestamp(ts.getTime)
+ result.setNanos(ts.getNanos)
+ result
case DecimalType.Fixed(precision, scale) =>
OrcShimUtils.getHiveDecimalWritable(precision, scale)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala
index 72c7ce5232e8..2c1e8beeb2b1 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/orc/OrcUtils.scala
@@ -196,15 +196,45 @@ object OrcUtils extends Logging {
requiredSchema: StructType,
orcSchema: TypeDescription,
conf: Configuration): Option[(Array[Int], Boolean)] = {
- def checkTimestampCompatibility(orcCatalystSchema: StructType, dataSchema:
StructType): Unit = {
-
orcCatalystSchema.fields.map(_.dataType).zip(dataSchema.fields.map(_.dataType)).foreach
{
+ def isOrcTimestamp(dt: DataType): Boolean = dt match {
+ case TimestampType | TimestampNTZType | _: AnyTimestampNanoType => true
+ case _ => false
+ }
+
+ // The ORC reader does not coerce between timestamp families/precisions,
except between
+ // nanos timestamps of the same kind (NTZ or LTZ), which share an ORC
physical category and
+ // only differ by the precision applied on read. Any other mismatch (zone
or micros<->nanos)
+ // would otherwise fail obscurely, so reject it with a clear error.
+ def timestampReadCompatible(orcType: DataType, dataType: DataType):
Boolean =
+ (orcType, dataType) match {
+ case _ if orcType == dataType => true
+ case (_: TimestampNTZNanosType, _: TimestampNTZNanosType) => true
+ case (_: TimestampLTZNanosType, _: TimestampLTZNanosType) => true
+ case _ => false
+ }
+
+ // Recurse into struct/array/map so timestamp mismatches nested inside
containers are caught
+ // too, not just top-level and struct fields.
+ def checkTypeCompatibility(orcType: DataType, dataType: DataType): Unit =
+ (orcType, dataType) match {
case (TimestampType, TimestampNTZType) =>
throw
QueryExecutionErrors.cannotConvertOrcTimestampToTimestampNTZError()
case (TimestampNTZType, TimestampType) =>
throw
QueryExecutionErrors.cannotConvertOrcTimestampNTZToTimestampLTZError()
- case (t1: StructType, t2: StructType) =>
checkTimestampCompatibility(t1, t2)
+ case (o, d) if isOrcTimestamp(o) && isOrcTimestamp(d) &&
!timestampReadCompatible(o, d) =>
+ throw QueryExecutionErrors.cannotCastOrcTimestampError(o, d)
+ case (o: StructType, d: StructType) => checkTimestampCompatibility(o,
d)
+ case (ArrayType(o, _), ArrayType(d, _)) => checkTypeCompatibility(o, d)
+ case (MapType(ok, ov, _), MapType(dk, dv, _)) =>
+ checkTypeCompatibility(ok, dk)
+ checkTypeCompatibility(ov, dv)
case _ =>
}
+
+ def checkTimestampCompatibility(orcCatalystSchema: StructType, dataSchema:
StructType): Unit = {
+
orcCatalystSchema.fields.map(_.dataType).zip(dataSchema.fields.map(_.dataType)).foreach
{
+ case (orcType, dataType) => checkTypeCompatibility(orcType, dataType)
+ }
}
checkTimestampCompatibility(toCatalystSchema(orcSchema), dataSchema)
@@ -291,6 +321,8 @@ object OrcUtils extends Logging {
case m: MapType =>
s"map<${getOrcSchemaString(m.keyType)},${getOrcSchemaString(m.valueType)}>"
case _: DayTimeIntervalType | _: TimestampNTZType | _: TimeType =>
LongType.catalogString
+ case _: TimestampLTZNanosType => "timestamp with local time zone"
+ case _: TimestampNTZNanosType => "timestamp"
case _: YearMonthIntervalType => IntegerType.catalogString
case _ => dt.catalogString
}
@@ -318,6 +350,14 @@ object OrcUtils extends Logging {
val typeDesc = new
TypeDescription(TypeDescription.Category.TIMESTAMP)
typeDesc.setAttribute(CATALYST_TYPE_ATTRIBUTE_NAME, t.typeName)
Some(typeDesc)
+ case t: TimestampLTZNanosType =>
+ val typeDesc = new
TypeDescription(TypeDescription.Category.TIMESTAMP_INSTANT)
+ typeDesc.setAttribute(CATALYST_TYPE_ATTRIBUTE_NAME, t.typeName)
+ Some(typeDesc)
+ case t: TimestampNTZNanosType =>
+ val typeDesc = new
TypeDescription(TypeDescription.Category.TIMESTAMP)
+ typeDesc.setAttribute(CATALYST_TYPE_ATTRIBUTE_NAME, t.typeName)
+ Some(typeDesc)
case _: StringType =>
val typeDesc = new TypeDescription(TypeDescription.Category.STRING)
typeDesc.setAttribute(CATALYST_TYPE_ATTRIBUTE_NAME,
StringType.typeName)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/orc/OrcTable.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/orc/OrcTable.scala
index 24cd5e60321e..08cd89fdacc6 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/orc/OrcTable.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/orc/OrcTable.scala
@@ -53,9 +53,6 @@ case class OrcTable(
override def supportsDataType(dataType: DataType): Boolean = dataType match {
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/test/scala/org/apache/spark/sql/FileBasedDataSourceSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/FileBasedDataSourceSuite.scala
index dbf5dee4146c..22f3ae6126ee 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/FileBasedDataSourceSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/FileBasedDataSourceSuite.scala
@@ -20,6 +20,7 @@ package org.apache.spark.sql
import java.io.{File, FileNotFoundException}
import java.net.URI
import java.nio.file.{Files, StandardOpenOption}
+import java.time.{LocalDateTime, ZoneOffset}
import scala.collection.mutable
@@ -33,6 +34,7 @@ import
org.apache.spark.sql.catalyst.expressions.{AttributeReference, GreaterTha
import
org.apache.spark.sql.catalyst.expressions.IntegralLiteralTestUtils.{negativeInt,
positiveInt}
import org.apache.spark.sql.catalyst.plans.logical.Filter
import org.apache.spark.sql.catalyst.types.DataTypeUtils
+import
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
import org.apache.spark.sql.execution.{FileSourceScanLike, SimpleMode}
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
import org.apache.spark.sql.execution.datasources.FilePartition
@@ -1336,10 +1338,9 @@ class FileBasedDataSourceSuite extends SharedSparkSession
}
}
- test("SPARK-57166: nanosecond timestamp types are not supported in
non-Parquet file sources") {
- // These built-in file formats do not support nanosecond-capable
timestamps. Parquet support is
- // covered separately in ParquetTimestampNanosSuite.
- val unsupportedDataSources = Seq("orc", "json", "csv", "xml")
+ test("SPARK-57166: nanosecond timestamp types are not supported in selected
file data sources") {
+ // Parquet and ORC support nanosecond-capable timestamps, while these
formats still reject them.
+ val unsupportedDataSources = Seq("json", "csv", "xml")
val nanosTypes = Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(9))
withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
// Test both v1 and v2 data sources.
@@ -1395,6 +1396,39 @@ class FileBasedDataSourceSuite extends SharedSparkSession
}
}
}
+
+ test("SPARK-57166: ORC supports nanosecond timestamp types in v1 and v2") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ // Validate both v1 and v2 ORC paths.
+ Seq(true, false).foreach { useV1 =>
+ val useV1List = if (useV1) "orc" else ""
+ withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> useV1List) {
+ foreachNanosPrecision { 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(ZoneOffset.UTC)
+ }
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value))),
+ new StructType().add("ts", nanosType))
+ val path = new File(dir,
s"orc_nanos_${nanosType.typeName}").getCanonicalPath
+ df.write.format("orc").mode("overwrite").save(path)
+ val readBack = spark.read.schema(new StructType().add("ts",
nanosType))
+ .format("orc").load(path)
+ checkAnswer(readBack, df)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
object TestingUDT {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionErrorsSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionErrorsSuite.scala
index ce549da03b46..058b47ee4f67 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionErrorsSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionErrorsSuite.scala
@@ -20,6 +20,7 @@ package org.apache.spark.sql.errors
import java.io.{File, IOException}
import java.net.{URI, URL}
import java.sql.{Connection, DatabaseMetaData, Driver, DriverManager,
PreparedStatement, ResultSet, ResultSetMetaData}
+import java.time.LocalDateTime
import java.util.{Locale, Properties, ServiceConfigurationError}
import scala.jdk.CollectionConverters._
@@ -54,7 +55,7 @@ import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.jdbc.{JdbcDialect, JdbcDialects}
import org.apache.spark.sql.streaming.StreamingQueryException
import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{ArrayType, BooleanType, DataType,
DecimalType, IntegerType, LongType, MetadataBuilder, StructField, StructType}
+import org.apache.spark.sql.types.{ArrayType, BooleanType, DataType,
DecimalType, IntegerType, LongType, MetadataBuilder, StructField, StructType,
TimestampNTZNanosType}
import org.apache.spark.sql.vectorized.ColumnarArray
import org.apache.spark.unsafe.array.ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH
import org.apache.spark.util.ThreadUtils
@@ -322,6 +323,53 @@ class QueryExecutionErrorsSuite
}
}
+ test("UNSUPPORTED_FEATURE - SPARK-57455: can't read nanos timestamp as
micros timestamp") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ withTempPath { file =>
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(LocalDateTime.of(1970, 1, 1,
0, 0, 0, 1)))),
+ new StructType().add("ts", TimestampNTZNanosType(9)))
+ df.write.orc(file.getCanonicalPath)
+ withAllNativeOrcReaders {
+ val ex = intercept[SparkException] {
+ spark.read.schema("ts
timestamp_ntz").orc(file.getCanonicalPath).collect()
+ }
+ assert(ex.getCondition.startsWith("FAILED_READ_FILE"))
+ checkError(
+ exception =
ex.getCause.asInstanceOf[SparkUnsupportedOperationException],
+ condition = "UNSUPPORTED_FEATURE.ORC_TYPE_CAST",
+ parameters = Map("orcType" -> "\"TIMESTAMP_NTZ(9)\"",
+ "toType" -> "\"TIMESTAMP_NTZ\""),
+ sqlState = "0A000")
+ }
+ }
+ }
+ }
+
+ test("UNSUPPORTED_FEATURE - SPARK-57455: nanos timestamp mismatch nested in
array is caught") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ withTempPath { file =>
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(
+ Seq(Row(Seq(LocalDateTime.of(1970, 1, 1, 0, 0, 0, 1))))),
+ new StructType().add("ts", ArrayType(TimestampNTZNanosType(9))))
+ df.write.orc(file.getCanonicalPath)
+ withAllNativeOrcReaders {
+ val ex = intercept[SparkException] {
+ spark.read.schema("ts
array<timestamp_ntz>").orc(file.getCanonicalPath).collect()
+ }
+ assert(ex.getCondition.startsWith("FAILED_READ_FILE"))
+ checkError(
+ exception =
ex.getCause.asInstanceOf[SparkUnsupportedOperationException],
+ condition = "UNSUPPORTED_FEATURE.ORC_TYPE_CAST",
+ parameters = Map("orcType" -> "\"TIMESTAMP_NTZ(9)\"",
+ "toType" -> "\"TIMESTAMP_NTZ\""),
+ sqlState = "0A000")
+ }
+ }
+ }
+ }
+
test("SPARK-42290: NotEnoughMemory error can't be create") {
QueryExecutionErrors.notEnoughMemoryToBuildAndBroadcastTableError(new
OutOfMemoryError(), Seq())
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcQuerySuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcQuerySuite.scala
index a42c004e3aaf..c4e041e13ebd 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcQuerySuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcQuerySuite.scala
@@ -36,6 +36,7 @@ import org.apache.spark.{SparkConf, SparkException}
import org.apache.spark.sql._
import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.catalyst.util.DateTimeTestUtils
+import
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
import org.apache.spark.sql.execution.FileSourceScanExec
import org.apache.spark.sql.execution.datasources.{HadoopFsRelation,
LogicalRelation, RecordReaderIterator}
import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
@@ -917,6 +918,148 @@ abstract class OrcQuerySuite extends OrcQueryTest with
SharedSparkSession {
}
}
+ test("SPARK-57455: ORC reads nanos timestamps with vectorized and
non-vectorized readers") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val wallClocks = Seq(
+ LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0),
+ LocalDateTime.of(1970, 1, 1, 0, 0, 1, 900),
+ LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ val inputDf = nanosTimestampDf(nanosType, wallClocks)
+ val expected = inputDf.collect()
+ withTempPath { dir =>
+ val path = dir.getCanonicalPath
+ inputDf.write.mode("overwrite").orc(path)
+ Seq(true, false).foreach { vectorized =>
+ withSQLConf(SQLConf.ORC_VECTORIZED_READER_ENABLED.key ->
vectorized.toString) {
+ checkAnswer(
+ spark.read.schema(new StructType().add("ts",
nanosType)).orc(path),
+ expected)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: nanos timestamps survive a time-zone change across ORC
write/read") {
+ // The NTZ wall clock stays zone-independent and the LTZ instant is
preserved even when the
+ // writer's and reader's JVM default time zones differ: NTZ goes through
an ORC TIMESTAMP
+ // (which preserves the local wall-clock fields) via the default-zone
valueOf/toLocalDateTime
+ // on both ends, and LTZ goes through an ORC TIMESTAMP_INSTANT
(instant-preserving) via
+ // epoch-based conversions, so neither depends on the default zone
matching across the round
+ // trip.
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val wallClock = Seq(LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ val inputDf = nanosTimestampDf(nanosType, wallClock)
+ val expected = inputDf.collect()
+ withTempPath { dir =>
+ val path = dir.getCanonicalPath
+ DateTimeTestUtils.withDefaultTimeZone(DateTimeTestUtils.LA) {
+ inputDf.write.mode("overwrite").orc(path)
+ }
+ Seq(true, false).foreach { vectorized =>
+ withSQLConf(SQLConf.ORC_VECTORIZED_READER_ENABLED.key ->
vectorized.toString) {
+ DateTimeTestUtils.withDefaultTimeZone(DateTimeTestUtils.UTC)
{
+ checkAnswer(
+ spark.read.schema(new StructType().add("ts",
nanosType)).orc(path),
+ expected)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: nanos timestamp type is inferred from ORC without an
explicit schema") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val wallClock = Seq(LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ val inputDf = nanosTimestampDf(nanosType, wallClock)
+ withTempPath { dir =>
+ val path = dir.getCanonicalPath
+ inputDf.write.mode("overwrite").orc(path)
+ // Read without supplying a schema: the nanos type is recovered
from the
+ // spark.sql.catalyst.type attribute stored in the ORC type
description.
+ val readBack = spark.read.orc(path)
+ assert(readBack.schema("ts").dataType === nanosType)
+ checkAnswer(readBack, inputDf)
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: ORC round-trips the min/max values of nanos timestamp
types") {
+ // The documented range is [0001-01-01T00:00:00.000000000,
9999-12-31T23:59:59.999999999]
+ // (at UTC for LTZ); check both ends round-trip through ORC with both
readers, at every
+ // supported precision (the max's sub-micro digits are truncated to the
precision).
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val bounds = Seq(
+ LocalDateTime.of(1, 1, 1, 0, 0, 0, 0),
+ LocalDateTime.of(9999, 12, 31, 23, 59, 59, 999999999))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ val inputDf = nanosTimestampDf(nanosType, bounds)
+ val expected = inputDf.collect()
+ withTempPath { dir =>
+ val path = dir.getCanonicalPath
+ inputDf.write.mode("overwrite").orc(path)
+ Seq(true, false).foreach { vectorized =>
+ withSQLConf(SQLConf.ORC_VECTORIZED_READER_ENABLED.key ->
vectorized.toString) {
+ checkAnswer(
+ spark.read.schema(new StructType().add("ts",
nanosType)).orc(path),
+ expected)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: ORC round-trips nanos timestamps in nested/complex
types") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ val wallClocks = Seq(LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ withTempView("nanos_input") {
+ nanosTimestampDf(nanosType,
wallClocks).createOrReplaceTempView("nanos_input")
+ val nested = sql(
+ """SELECT
+ | named_struct('ts', ts) AS struct_ts,
+ | array(ts) AS array_ts,
+ | map('k', ts) AS map_ts
+ |FROM nanos_input
+ |""".stripMargin)
+ withTempPath { dir =>
+ val path = dir.getCanonicalPath
+ nested.write.mode("overwrite").orc(path)
+ Seq(true, false).foreach { vectorized =>
+ withSQLConf(SQLConf.ORC_VECTORIZED_READER_ENABLED.key ->
vectorized.toString) {
+ val readBack = spark.read.schema(nested.schema).orc(path)
+ checkAnswer(readBack, nested)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
// SPARK-39519: Ignore this case because it requires more than 4g heap
memory to ensure test
// stability when use Java 11. Should test it manually when upgrading
`hive-storage-api`
ignore("SPARK-39387: BytesColumnVector should not throw RuntimeException due
to overflow") {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcSourceSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcSourceSuite.scala
index a65eb5fa43fc..3e7d28fb8843 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcSourceSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcSourceSuite.scala
@@ -25,11 +25,13 @@ import java.util.Locale
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileStatus, FileSystem, Path}
+import org.apache.hadoop.hive.ql.exec.vector.TimestampColumnVector
import org.apache.logging.log4j.Level
import org.apache.orc.OrcConf.COMPRESS
import org.apache.orc.OrcFile
import org.apache.orc.OrcProto.ColumnEncoding.Kind.{DICTIONARY_V2, DIRECT,
DIRECT_V2}
import org.apache.orc.OrcProto.Stream.Kind
+import org.apache.orc.TypeDescription
import org.apache.orc.impl.RecordReaderImpl
import org.apache.spark.{SPARK_VERSION_SHORT, SparkConf, SparkException}
@@ -875,6 +877,30 @@ abstract class OrcSourceSuite extends OrcSuite with
SharedSparkSession {
}
}
+ test("SPARK-57455: plain ORC timestamp stays TimestampType without Spark
nanos metadata") {
+ withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
+ withTempPath { dir =>
+ val outputFile = new Path(new File(dir,
"part-00000.orc").getCanonicalPath)
+ val schema = TypeDescription.fromString("struct<ts:timestamp>")
+ val writerOptions = OrcFile
+ .writerOptions(spark.sessionState.newHadoopConf())
+ .setSchema(schema)
+ Utils.tryWithResource(OrcFile.createWriter(outputFile, writerOptions))
{ writer =>
+ val batch = schema.createRowBatch()
+ val tsCol = batch.cols(0).asInstanceOf[TimestampColumnVector]
+ batch.size = 1
+ tsCol.time(0) = 0L
+ tsCol.nanos(0) = 123000000
+ writer.addRowBatch(batch)
+ }
+
+ val readBack = spark.read.orc(dir.getCanonicalPath)
+ assert(readBack.schema("ts").dataType === TimestampType)
+ assert(readBack.count() === 1)
+ }
+ }
+ }
+
withAllNativeOrcReaders {
Seq(true, false).foreach { vecReaderNestedColEnabled =>
val vecReaderEnabled = SQLConf.get.orcVectorizedReaderEnabled
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcTest.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcTest.scala
index 7c340746413c..da6e6b11969a 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcTest.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/orc/OrcTest.scala
@@ -18,11 +18,12 @@
package org.apache.spark.sql.execution.datasources.orc
import java.io.File
+import java.time.{LocalDateTime, ZoneOffset}
import scala.reflect.ClassTag
import scala.reflect.runtime.universe.TypeTag
-import org.apache.spark.sql.{Column, DataFrame, QueryTest}
+import org.apache.spark.sql.{Column, DataFrame, QueryTest, Row}
import org.apache.spark.sql.catalyst.expressions.{Attribute, Predicate}
import org.apache.spark.sql.catalyst.planning.PhysicalOperation
import org.apache.spark.sql.classic.ClassicConversions._
@@ -31,6 +32,7 @@ import
org.apache.spark.sql.execution.datasources.v2.ExtractV2Scan
import org.apache.spark.sql.execution.datasources.v2.orc.OrcScan
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.internal.SQLConf.ORC_IMPLEMENTATION
+import org.apache.spark.sql.types.{DataType, StructType,
TimestampLTZNanosType, TimestampNTZNanosType}
import org.apache.spark.util.ArrayImplicits._
import org.apache.spark.util.Utils
@@ -152,6 +154,19 @@ trait OrcTest extends QueryTest with
FileBasedDataSourceTest {
withSQLConf(SQLConf.ORC_VECTORIZED_READER_ENABLED.key -> "true")(code)
}
+ // Builds a single-column ("ts") DataFrame from external java.time values,
letting the schema
+ // precision truncate the sub-microsecond digits: an NTZ column takes
java.time.LocalDateTime
+ // values, an LTZ column takes the same wall clocks as java.time.Instant at
UTC.
+ protected def nanosTimestampDf(nanosType: DataType, wallClocks:
Seq[LocalDateTime]): DataFrame = {
+ val values: Seq[Any] = nanosType match {
+ case _: TimestampNTZNanosType => wallClocks
+ case _: TimestampLTZNanosType =>
wallClocks.map(_.toInstant(ZoneOffset.UTC))
+ }
+ spark.createDataFrame(
+ spark.sparkContext.parallelize(values.map(Row(_))),
+ new StructType().add("ts", nanosType))
+ }
+
/**
* Takes a sequence of products `data` to generate multi-level nested
* dataframes as new test data. It tests both non-nested and nested
dataframes
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
index bf40327bd991..49b9cc798a1b 100644
--- a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
+++ b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
@@ -38,6 +38,7 @@ import org.apache.spark.sql.errors.DataTypeErrors.toSQLType
import org.apache.spark.sql.execution.datasources.DaysWritable
import org.apache.spark.sql.types
import org.apache.spark.sql.types._
+import org.apache.spark.unsafe.types.TimestampNanosVal
import org.apache.spark.unsafe.types.UTF8String
/**
@@ -330,8 +331,22 @@ private[hive] trait HiveInspectors {
withNullSafe(o =>
DateTimeUtils.toJavaDate(o.asInstanceOf[Int]))
case _: JavaTimestampObjectInspector =>
- withNullSafe(o =>
- DateTimeUtils.toJavaTimestamp(o.asInstanceOf[Long]))
+ withNullSafe(o => dataType match {
+ case _: TimestampLTZNanosType =>
+ o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.from(
+ DateTimeUtils.timestampNanosToInstant(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ }
+ case _: TimestampNTZNanosType =>
+ o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.valueOf(
+ DateTimeUtils.timestampNanosToLocalDateTime(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ }
+ case _ =>
+ DateTimeUtils.toJavaTimestamp(o.asInstanceOf[Long])
+ })
case _: HiveDecimalObjectInspector if x.preferWritable() =>
withNullSafe(o => getDecimalWritable(o.asInstanceOf[Decimal]))
case _: HiveDecimalObjectInspector =>
@@ -346,9 +361,39 @@ private[hive] trait HiveInspectors {
case _: DateObjectInspector =>
withNullSafe(o => DateTimeUtils.toJavaDate(o.asInstanceOf[Int]))
case _: TimestampObjectInspector if x.preferWritable() =>
- withNullSafe(o => getTimestampWritable(o))
+ withNullSafe(o => dataType match {
+ case _: TimestampLTZNanosType =>
+ new hiveIo.TimestampWritable(o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.from(
+ DateTimeUtils.timestampNanosToInstant(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ })
+ case _: TimestampNTZNanosType =>
+ new hiveIo.TimestampWritable(o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.valueOf(
+ DateTimeUtils.timestampNanosToLocalDateTime(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ })
+ case _ =>
+ getTimestampWritable(o)
+ })
case _: TimestampObjectInspector =>
- withNullSafe(o => DateTimeUtils.toJavaTimestamp(o.asInstanceOf[Long]))
+ withNullSafe(o => dataType match {
+ case _: TimestampLTZNanosType =>
+ o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.from(
+ DateTimeUtils.timestampNanosToInstant(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ }
+ case _: TimestampNTZNanosType =>
+ o match {
+ case v: TimestampNanosVal => java.sql.Timestamp.valueOf(
+ DateTimeUtils.timestampNanosToLocalDateTime(v))
+ case micros: Long => DateTimeUtils.toJavaTimestamp(micros)
+ }
+ case _ =>
+ DateTimeUtils.toJavaTimestamp(o.asInstanceOf[Long])
+ })
case _: HiveIntervalDayTimeObjectInspector if x.preferWritable() =>
withNullSafe(o => getHiveIntervalDayTimeWritable(o))
case _: HiveIntervalDayTimeObjectInspector =>
@@ -752,6 +797,73 @@ private[hive] trait HiveInspectors {
}
}
+ /**
+ * Returns an unwrapper that converts a Hive value into a Catalyst value,
using the target
+ * Catalyst `dataType` to preserve nanosecond timestamp precision. The plain
+ * `unwrapperFor(ObjectInspector)` cannot do this because a Hive
`TimestampObjectInspector`
+ * maps to micros by default; here the nanos timestamp types are produced as
`TimestampNanosVal`,
+ * recursing through array/map/struct so nested nanos timestamps round-trip
correctly. Any other
+ * type is delegated to the `ObjectInspector`-only overload.
+ */
+ def unwrapperFor(objectInspector: ObjectInspector, dataType: DataType): Any
=> Any =
+ (objectInspector, dataType) match {
+ case (ti: TimestampObjectInspector, t: TimestampNTZNanosType) =>
+ data: Any => {
+ if (data != null) {
+ DateTimeUtils.localDateTimeToTimestampNanos(
+ ti.getPrimitiveJavaObject(data).toLocalDateTime, t.precision)
+ } else {
+ null
+ }
+ }
+ case (ti: TimestampObjectInspector, t: TimestampLTZNanosType) =>
+ data: Any => {
+ if (data != null) {
+ DateTimeUtils.instantToTimestampNanos(
+ ti.getPrimitiveJavaObject(data).toInstant, t.precision)
+ } else {
+ null
+ }
+ }
+ case (li: ListObjectInspector, ArrayType(elementType, _)) =>
+ val unwrapper = unwrapperFor(li.getListElementObjectInspector,
elementType)
+ data: Any => {
+ if (data != null) {
+ Option(li.getList(data))
+ .map(l => new GenericArrayData(l.asScala.map(unwrapper).toArray))
+ .orNull
+ } else {
+ null
+ }
+ }
+ case (mi: MapObjectInspector, MapType(keyType, valueType, _)) =>
+ val keyUnwrapper = unwrapperFor(mi.getMapKeyObjectInspector, keyType)
+ val valueUnwrapper = unwrapperFor(mi.getMapValueObjectInspector,
valueType)
+ data: Any => {
+ if (data != null) {
+ val map = mi.getMap(data)
+ if (map == null) null else ArrayBasedMapData(map, keyUnwrapper,
valueUnwrapper)
+ } else {
+ null
+ }
+ }
+ case (si: StructObjectInspector, st: StructType) =>
+ val fields = si.getAllStructFieldRefs.asScala
+ val unwrappers = fields.zip(st.fields).map { case (field, structField)
=>
+ val unwrapper = unwrapperFor(field.getFieldObjectInspector,
structField.dataType)
+ data: Any => unwrapper(si.getStructFieldData(data, field))
+ }
+ data: Any => {
+ if (data != null) {
+ new GenericInternalRow(unwrappers.map(_(data)).toArray)
+ } else {
+ null
+ }
+ }
+ case _ =>
+ unwrapperFor(objectInspector)
+ }
+
/**
* Builds unwrappers ahead of time according to object inspector
* types to avoid pattern matching and branching costs per row.
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/TableReader.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/TableReader.scala
index d6f62284d2e7..fedf2e51ffc3 100644
--- a/sql/hive/src/main/scala/org/apache/spark/sql/hive/TableReader.scala
+++ b/sql/hive/src/main/scala/org/apache/spark/sql/hive/TableReader.scala
@@ -45,6 +45,7 @@ import org.apache.spark.sql.catalyst.analysis.CastSupport
import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{TimestampLTZNanosType,
TimestampNTZNanosType}
import org.apache.spark.unsafe.types.UTF8String
import org.apache.spark.util.{SerializableConfiguration, Utils}
import org.apache.spark.util.ArrayImplicits._
@@ -461,8 +462,8 @@ private[hive] object HadoopTableReader extends
HiveInspectors with Logging {
* Builds specific unwrappers ahead of time according to object inspector
* types to avoid pattern matching and branching costs per row.
*/
- val unwrappers: Seq[(Any, InternalRow, Int) => Unit] = fieldRefs.map {
- _.getFieldObjectInspector match {
+ val unwrappers: Seq[(Any, InternalRow, Int) => Unit] =
fieldRefs.zip(nonPartitionKeyAttrs).map {
+ case (fieldRef, (attr, _)) => fieldRef.getFieldObjectInspector match {
case oi: BooleanObjectInspector =>
(value: Any, row: InternalRow, ordinal: Int) =>
row.setBoolean(ordinal, oi.get(value))
case oi: ByteObjectInspector =>
@@ -486,9 +487,18 @@ private[hive] object HadoopTableReader extends
HiveInspectors with Logging {
case oi: HiveDecimalObjectInspector =>
(value: Any, row: InternalRow, ordinal: Int) =>
row.update(ordinal, HiveShim.toCatalystDecimal(oi, value))
+ case oi: TimestampObjectInspector
+ if attr.dataType.isInstanceOf[TimestampLTZNanosType] ||
+ attr.dataType.isInstanceOf[TimestampNTZNanosType] =>
+ // Nanos timestamps need the target Catalyst type to produce
TimestampNanosVal; reuse
+ // the data-type-aware unwrapper instead of duplicating the
conversion here.
+ val unwrapper = unwrapperFor(oi, attr.dataType)
+ (value: Any, row: InternalRow, ordinal: Int) => row.update(ordinal,
unwrapper(value))
case oi: TimestampObjectInspector =>
(value: Any, row: InternalRow, ordinal: Int) =>
- row.setLong(ordinal,
DateTimeUtils.fromJavaTimestamp(oi.getPrimitiveJavaObject(value)))
+ row.setLong(
+ ordinal,
+
DateTimeUtils.fromJavaTimestamp(oi.getPrimitiveJavaObject(value)))
case oi: DateObjectInspector =>
(value: Any, row: InternalRow, ordinal: Int) =>
row.setInt(ordinal,
DateTimeUtils.fromJavaDate(oi.getPrimitiveJavaObject(value)))
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/orc/OrcFileFormat.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/orc/OrcFileFormat.scala
index 6c3fbb5a8314..04857cff9d9b 100644
--- a/sql/hive/src/main/scala/org/apache/spark/sql/hive/orc/OrcFileFormat.scala
+++ b/sql/hive/src/main/scala/org/apache/spark/sql/hive/orc/OrcFileFormat.scala
@@ -197,8 +197,6 @@ case class OrcFileFormat() extends FileFormat
case _: AnsiIntervalType => false
case _: TimeType => false
- // Nanosecond-capable timestamps are not yet supported by this datasource.
- case _: AnyTimestampNanoType => false
case _: AtomicType => true
case st: StructType => st.forall { f => supportDataType(f.dataType) }
@@ -226,6 +224,26 @@ case class OrcFileFormat() extends FileFormat
private[orc] class OrcSerializer(dataSchema: StructType, conf: Configuration)
extends HiveInspectors {
+ private def toHiveCompatibleDataType(dataType: DataType): DataType = {
+ dataType match {
+ case _: TimestampNTZNanosType | _: TimestampLTZNanosType =>
+ TimestampType
+ case StructType(fields) =>
+ StructType(fields.map(f => f.copy(dataType =
toHiveCompatibleDataType(f.dataType))))
+ case ArrayType(elementType, containsNull) =>
+ ArrayType(toHiveCompatibleDataType(elementType), containsNull)
+ case MapType(keyType, valueType, valueContainsNull) =>
+ MapType(
+ toHiveCompatibleDataType(keyType),
+ toHiveCompatibleDataType(valueType),
+ valueContainsNull)
+ case other =>
+ other
+ }
+ }
+
+ private[this] val hiveCompatibleSchema = StructType(
+ dataSchema.map(f => f.copy(dataType =
toHiveCompatibleDataType(f.dataType))))
def serialize(row: InternalRow): Writable = {
wrapOrcStruct(cachedOrcStruct, structOI, row)
@@ -235,7 +253,9 @@ private[orc] class OrcSerializer(dataSchema: StructType,
conf: Configuration)
private[this] val serializer = {
val table = new Properties()
table.setProperty("columns", dataSchema.fieldNames.mkString(","))
- table.setProperty("columns.types",
dataSchema.map(_.dataType.catalogString).mkString(":"))
+ table.setProperty(
+ "columns.types",
+ hiveCompatibleSchema.map(_.dataType.catalogString).mkString(":"))
val serde = new OrcSerde
serde.initialize(conf, table)
@@ -244,7 +264,7 @@ private[orc] class OrcSerializer(dataSchema: StructType,
conf: Configuration)
// Object inspector converted from the schema of the relation to be
serialized.
val structOI = {
- val typeInfo =
TypeInfoUtils.getTypeInfoFromTypeString(dataSchema.catalogString)
+ val typeInfo =
TypeInfoUtils.getTypeInfoFromTypeString(hiveCompatibleSchema.catalogString)
OrcStruct.createObjectInspector(typeInfo.asInstanceOf[StructTypeInfo])
.asInstanceOf[SettableStructObjectInspector]
}
@@ -337,6 +357,8 @@ private[orc] object OrcFileFormat extends HiveInspectors
with Logging {
val unsafeProjection = UnsafeProjection.create(requiredSchema)
val forcePositionalEvolution =
OrcConf.FORCE_POSITIONAL_EVOLUTION.getBoolean(conf)
+ def isNanosTimestamp(dt: DataType): Boolean =
dt.isInstanceOf[AnyTimestampNanoType]
+
def unwrap(oi: StructObjectInspector): Iterator[InternalRow] = {
val (fieldRefs, fieldOrdinals) = requiredSchema.zipWithIndex.map {
case (field, ordinal) =>
@@ -352,7 +374,17 @@ private[orc] object OrcFileFormat extends HiveInspectors
with Logging {
ref -> ordinal
}.unzip
- val unwrappers = fieldRefs.map(r => if (r == null) null else
unwrapperFor(r))
+ val unwrappers = fieldRefs.zip(requiredSchema).map {
+ case (null, _) => null
+ // Nanos timestamps (including those nested in struct/array/map) need
the target Catalyst
+ // type to produce TimestampNanosVal; the data-type-aware unwrapper
handles both the
+ // top-level and nested cases, while everything else keeps the
primitive fast paths.
+ case (fieldRef, field) if
field.dataType.existsRecursively(isNanosTimestamp) =>
+ val unwrapper = unwrapperFor(fieldRef.getFieldObjectInspector,
field.dataType)
+ (value: Any, row: InternalRow, ordinal: Int) => row.update(ordinal,
unwrapper(value))
+ case (fieldRef, _) =>
+ unwrapperFor(fieldRef)
+ }
iterator.map { value =>
val raw = deserializer.deserialize(value)
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/orc/HiveOrcSourceSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/orc/HiveOrcSourceSuite.scala
index 504d1a5881a0..96ac15893d07 100644
---
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/orc/HiveOrcSourceSuite.scala
+++
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/orc/HiveOrcSourceSuite.scala
@@ -18,17 +18,17 @@
package org.apache.spark.sql.hive.orc
import java.io.File
+import java.time.LocalDateTime
-import org.apache.spark.sql.{AnalysisException, Column, Row}
+import org.apache.spark.sql.{AnalysisException, Row}
import org.apache.spark.sql.TestingUDT.{IntervalData, IntervalUDT}
-import org.apache.spark.sql.catalyst.expressions.Literal
-import org.apache.spark.sql.classic.ClassicConversions._
+import org.apache.spark.sql.catalyst.util.DateTimeTestUtils
+import
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
import org.apache.spark.sql.execution.datasources.orc.OrcSuite
import org.apache.spark.sql.hive.HiveUtils
import org.apache.spark.sql.hive.test.TestHiveSingleton
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
-import org.apache.spark.unsafe.types.TimestampNanosVal
import org.apache.spark.util.Utils
class HiveOrcSourceSuite extends OrcSuite with TestHiveSingleton {
@@ -350,39 +350,104 @@ class HiveOrcSourceSuite extends OrcSuite with
TestHiveSingleton {
}
}
- test("SPARK-57166: nanosecond timestamp types are not supported in Hive
ORC") {
- val nanosTypes = Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(9))
+ test("SPARK-57166: nanosecond timestamp types are supported in Hive ORC") {
withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true") {
- nanosTypes.foreach { nanosType =>
- val expectedType = s""""${nanosType.sql}""""
+ val wallClock = Seq(LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0))
+ Seq(true, false).foreach { convertMetastore =>
+ withSQLConf(HiveUtils.CONVERT_METASTORE_ORC.key ->
s"$convertMetastore") {
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ withTempDir { dir =>
+ val df = nanosTimestampDf(nanosType, wallClock)
+ val path = new File(dir, "nanos").getCanonicalPath
+ df.write.format("orc").mode("overwrite").save(path)
+
+ val readBack = spark.read.schema(new StructType().add("ts",
nanosType))
+ .format("orc").load(path)
+ checkAnswer(readBack, df)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: Hive ORC serde nanos round-trip; NTZ time-zone
independent") {
+ // Force spark.sql.orc.impl=hive so the Hive serde write/read conversion
is exercised
+ // (HiveInspectors.wrapperFor on write, the hive OrcFileFormat unwrappers
on read) rather
+ // than the native datasource that the other nanos tests use.
+ withSQLConf(
+ SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+ SQLConf.ORC_IMPLEMENTATION.key -> "hive") {
+ // A mid-range value plus the documented min/max ends
+ // [0001-01-01T00:00:00, 9999-12-31T23:59:59.999999999] (at UTC for LTZ).
+ val wallClocks = Seq(
+ LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123),
+ LocalDateTime.of(1, 1, 1, 0, 0, 0, 0),
+ LocalDateTime.of(9999, 12, 31, 23, 59, 59, 999999999))
+ foreachNanosPrecision { precision =>
+ // Same-zone round trip through the Hive serde path for both nanos
types, including the
+ // min and max of the documented range.
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ val input = nanosTimestampDf(nanosType, wallClocks)
+ withTempDir { dir =>
+ val path = new File(dir, "nanos").getCanonicalPath
+ input.write.format("orc").mode("overwrite").save(path)
+ checkAnswer(
+ spark.read.schema(new StructType().add("ts",
nanosType)).format("orc").load(path),
+ input)
+ }
+ }
+
+ // The NTZ wall clock stays zone-independent across a JVM default
time-zone change. Hive
+ // ORC stores zone-naive wall-clock fields, so the instant-based LTZ
type is not
+ // zone-stable through the Hive serde path -- the same caveat as the
legacy TimestampType
+ // -- so LTZ is only round-tripped within a single zone above.
+ val ntzType = TimestampNTZNanosType(precision)
+ val ntzInput = nanosTimestampDf(ntzType, wallClocks.take(1))
+ val ntzExpected = ntzInput.collect()
withTempDir { dir =>
- // Write path
- 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("orc").mode("overwrite").save(writeDir)
- },
- condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
- parameters = Map(
- "columnName" -> "`ts`",
- "columnType" -> expectedType,
- "format" -> "ORC"))
-
- // Read path
- val readDir = new File(dir, "read").getCanonicalPath
- spark.range(1).write.format("orc").mode("overwrite").save(readDir)
- checkError(
- exception = intercept[AnalysisException] {
- spark.read.schema(new StructType().add("ts", nanosType))
- .format("orc").load(readDir).collect()
- },
- condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
- parameters = Map(
- "columnName" -> "`ts`",
- "columnType" -> expectedType,
- "format" -> "ORC"))
+ val path = new File(dir, "ntz-tz").getCanonicalPath
+ DateTimeTestUtils.withDefaultTimeZone(DateTimeTestUtils.LA) {
+ ntzInput.write.format("orc").mode("overwrite").save(path)
+ }
+ DateTimeTestUtils.withDefaultTimeZone(DateTimeTestUtils.UTC) {
+ checkAnswer(
+ spark.read.schema(new StructType().add("ts",
ntzType)).format("orc").load(path),
+ ntzExpected)
+ }
+ }
+ }
+ }
+ }
+
+ test("SPARK-57455: Hive ORC serde round-trips nanos timestamps in
nested/complex types") {
+ withSQLConf(
+ SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+ SQLConf.ORC_IMPLEMENTATION.key -> "hive") {
+ val wallClocks = Seq(LocalDateTime.of(1970, 1, 1, 0, 20, 34, 567890123))
+ foreachNanosPrecision { precision =>
+ Seq(TimestampNTZNanosType(precision),
TimestampLTZNanosType(precision)).foreach {
+ nanosType =>
+ withTempView("nanos_input") {
+ nanosTimestampDf(nanosType,
wallClocks).createOrReplaceTempView("nanos_input")
+ val nested = sql(
+ """SELECT
+ | named_struct('ts', ts) AS struct_ts,
+ | array(ts) AS array_ts,
+ | map('k', ts) AS map_ts
+ |FROM nanos_input
+ |""".stripMargin)
+ withTempDir { dir =>
+ val path = new File(dir, "nanos-nested").getCanonicalPath
+ nested.write.format("orc").mode("overwrite").save(path)
+ val readBack =
spark.read.schema(nested.schema).format("orc").load(path)
+ checkAnswer(readBack, nested)
+ }
+ }
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]