This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new b13b8135f15e [SPARK-60545][SQL] Raise a clear error for
`TIMESTAMP_NTZ` columns in Hive serde tables
b13b8135f15e is described below
commit b13b8135f15e6dac6ad1be5a27613f9a2f6be376
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Mon Oct 26 09:42:26 2026 -0700
[SPARK-60545][SQL] Raise a clear error for `TIMESTAMP_NTZ` columns in Hive
serde tables
### What changes were proposed in this pull request?
This PR aims to raise a clear error for `TIMESTAMP_NTZ` columns in Hive
serde tables.
Hive has no `TIMESTAMP_NTZ` type, so Hive serde tables cannot store
`TIMESTAMP_NTZ` columns. However, Spark passes `timestamp_ntz` to Hive as it is
and fails with a Hive internal error.
Like SPARK-60493 for the partition columns, this PR adds
`HiveExternalCatalog.verifyHiveSerdeDataColumnTypes`, which rejects the
`TIMESTAMP_NTZ` data columns of a Hive serde table, including the nested types
(`STRUCT`, `ARRAY` and `MAP`). It raises the existing
`UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE` error with the `Hive` format. Spark
calls it where it creates or alters a Hive serde table in Hive metastore:
- `HiveExternalCatalog.createTable` for `CREATE TABLE`, `CREATE TABLE AS
SELECT`, `CREATE TABLE LIKE`, `DataFrameWriter.saveAsTable` and
`Catalog.createTable`
- `CreateHiveTableAsSelectCommand` before it drops the existing table for
`DataFrameWriter.saveAsTable` with `SaveMode.Overwrite`
- `HiveExternalCatalog.alterTableSchema` for `ALTER TABLE ADD COLUMNS`
`CREATE TABLE IF NOT EXISTS` is still a no-op for an existing table because
`HiveExternalCatalog.createTable` is called only when the table doesn't exist.
In addition, like the `TIME` type (SPARK-57556), the nanosecond timestamp
types (SPARK-57815) and the geospatial types (SPARK-60519),
`HiveFileFormat.supportDataType` rejects `TIMESTAMP_NTZ` for `INSERT OVERWRITE
DIRECTORY ... STORED AS` through the Hive serde.
### Why are the changes needed?
With `spark.sql.timestampType=TIMESTAMP_NTZ`, the `TIMESTAMP` keyword in
DDL statements is `TIMESTAMP_NTZ` by design. So, the simple DDL statement below
fails with a Hive internal error, which doesn't tell the users which column is
wrong or why.
```sql
SET spark.sql.timestampType=TIMESTAMP_NTZ;
CREATE TABLE t (ts TIMESTAMP) STORED AS PARQUET;
```
```
org.apache.hadoop.hive.ql.metadata.HiveException:
java.lang.IllegalArgumentException: Error: type expected at the position 0 of
'timestamp_ntz' but 'timestamp_ntz' is found.
```
This issue was found during the review of #59674.
### Does this PR introduce _any_ user-facing change?
Yes, only the error changes. These statements already failed before this
PR. For the above example, the error becomes the following.
```
[UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE] The Hive datasource doesn't support
the column `ts` of the type "TIMESTAMP_NTZ". SQLSTATE: 0A000
```
For `INSERT OVERWRITE DIRECTORY ... STORED AS`, this error is the cause of
the `SparkException` like the other unsupported types.
### How was this patch tested?
Pass the CIs with the newly added test cases in `HiveDDLSuite`,
`AlterTableAddColumnsSuite` and `InsertSuite`. The test cases fail without this
fix, except the `CREATE TABLE IF NOT EXISTS` test case, which checks that the
existing no-op behavior is kept.
In addition, `TIMESTAMP_NTZ` is removed from the SPARK-60515 test case in
`MetastoreDataSourcesSuite`. That test case uses the types that the Hive SerDe
rejects, and this PR now rejects `TIMESTAMP_NTZ` before Hive does.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5.5
Closes #59783 from dongjoon-hyun/SPARK-60545.
Authored-by: Dongjoon Hyun <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../spark/sql/hive/HiveExternalCatalog.scala | 17 +++++
.../execution/CreateHiveTableAsSelectCommand.scala | 1 +
.../spark/sql/hive/execution/HiveFileFormat.scala | 5 +-
.../org/apache/spark/sql/hive/InsertSuite.scala | 33 +++++++++
.../spark/sql/hive/MetastoreDataSourcesSuite.scala | 2 +-
.../spark/sql/hive/execution/HiveDDLSuite.scala | 84 ++++++++++++++++++++++
.../command/AlterTableAddColumnsSuite.scala | 24 +++++++
7 files changed, 164 insertions(+), 2 deletions(-)
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveExternalCatalog.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveExternalCatalog.scala
index 484bde04aeb3..0379fbffebaf 100644
---
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveExternalCatalog.scala
+++
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveExternalCatalog.scala
@@ -272,6 +272,9 @@ private[spark] class HiveExternalCatalog(conf: SparkConf,
hadoopConf: Configurat
ignoreIfExists)
} else {
verifyHiveSerdePartitionColumnTypes(tableDefinition)
+ if (!tableDefinition.isViewLike) {
+ verifyHiveSerdeDataColumnTypes(tableDefinition)
+ }
val hiveCompatibleSchema = schemaWithNoCollation match {
// Spark-created views do not have to be Hive compatible. If the data
type is not
// Hive compatible, we can set schema to empty so that Spark can still
read this
@@ -802,6 +805,7 @@ private[spark] class HiveExternalCatalog(conf: SparkConf,
hadoopConf: Configurat
client.alterTableDataSchema(db, table, EMPTY_DATA_SCHEMA,
schemaProps)
}
} else {
+ verifyHiveSerdeDataColumnTypes(oldTable.copy(schema = newSchema))
client.alterTableDataSchema(db, table, hiveSchema, schemaProps)
}
}
@@ -1587,4 +1591,17 @@ object HiveExternalCatalog {
throw
QueryCompilationErrors.dataTypeUnsupportedByDataSourceError("Hive", f)
}
}
+
+ /**
+ * Hive has no TIMESTAMP_NTZ type. So, this throws an error if the given
Hive serde table has a
+ * TIMESTAMP_NTZ data column, including the nested types.
+ */
+ private[hive] def verifyHiveSerdeDataColumnTypes(table: CatalogTable): Unit
= {
+ table.schema
+ .filterNot(f => table.partitionColumnNames.contains(f.name))
+ .find(_.dataType.existsRecursively(_.isInstanceOf[TimestampNTZType]))
+ .foreach { f =>
+ throw
QueryCompilationErrors.dataTypeUnsupportedByDataSourceError("Hive", f)
+ }
+ }
}
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/CreateHiveTableAsSelectCommand.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/CreateHiveTableAsSelectCommand.scala
index a01831553b7e..57e884de31c1 100644
---
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/CreateHiveTableAsSelectCommand.scala
+++
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/CreateHiveTableAsSelectCommand.scala
@@ -76,6 +76,7 @@ case class CreateHiveTableAsSelectCommand(
// table. Drop it here, after the analysis and the checks of the new
table, because the
// new table is created before the write.
HiveExternalCatalog.verifyHiveSerdePartitionColumnTypes(newTableDesc)
+ HiveExternalCatalog.verifyHiveSerdeDataColumnTypes(newTableDesc)
val qualifiedNewTableDesc =
newTableDesc.copy(identifier =
catalog.qualifyIdentifier(tableIdentifier))
sparkSession.sharedState.externalCatalog.unwrapped.asInstanceOf[HiveExternalCatalog]
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveFileFormat.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveFileFormat.scala
index 189d90235bc1..55cc0c9cf93e 100644
---
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveFileFormat.scala
+++
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveFileFormat.scala
@@ -41,7 +41,7 @@ import
org.apache.spark.sql.execution.datasources.{FileFormat, OutputWriter, Out
import org.apache.spark.sql.hive.{HiveInspectors, HiveTableUtil}
import org.apache.spark.sql.internal.SessionStateHelper
import org.apache.spark.sql.sources.DataSourceRegister
-import org.apache.spark.sql.types.{AnyTimestampNanoType, ArrayType, DataType,
GeographyType, GeometryType, MapType, StructType, TimeType, UserDefinedType}
+import org.apache.spark.sql.types.{AnyTimestampNanoType, ArrayType, DataType,
GeographyType, GeometryType, MapType, StructType, TimestampNTZType, TimeType,
UserDefinedType}
import org.apache.spark.util.SerializableJobConf
/**
@@ -127,6 +127,9 @@ case class HiveFileFormat(fileSinkConf: FileSinkDesc)
// Hive has no geospatial types, so they cannot be stored in a Hive serde
table.
case _: GeometryType | _: GeographyType => false
+ // Hive has no TIMESTAMP_NTZ type, so it cannot be stored in a Hive serde
table.
+ case _: TimestampNTZType => false
+
case st: StructType => st.forall { f => supportDataType(f.dataType) }
case ArrayType(elementType, _) => supportDataType(elementType)
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/InsertSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/InsertSuite.scala
index 1367041b18fd..b68a4c7d1db3 100644
--- a/sql/hive/src/test/scala/org/apache/spark/sql/hive/InsertSuite.scala
+++ b/sql/hive/src/test/scala/org/apache/spark/sql/hive/InsertSuite.scala
@@ -800,6 +800,39 @@ class InsertSuite extends QueryTest with TestHiveSingleton
with BeforeAndAfter {
}
}
+ test("SPARK-60545: TIMESTAMP_NTZ type is unsupported when writing to a Hive
serde directory") {
+ // Disable native data source conversion so that PARQUET and ORC also go
through the Hive
+ // serde path (HiveFileFormat) like TEXTFILE.
+ withSQLConf(HiveUtils.CONVERT_METASTORE_INSERT_DIR.key -> "false") {
+ Seq("TEXTFILE", "PARQUET", "ORC").foreach { fileFormat =>
+ Seq(
+ "TIMESTAMP_NTZ'2026-01-01 00:00:00'" -> "TIMESTAMP_NTZ",
+ "ARRAY(TIMESTAMP_NTZ'2026-01-01 00:00:00')" -> "ARRAY<TIMESTAMP_NTZ>"
+ ).foreach { case (expr, columnType) =>
+ withTempDir { dir =>
+ // InsertIntoHiveDirCommand wraps the failure in a SparkException,
so assert on the
+ // cause.
+ val e = intercept[SparkException] {
+ sql(
+ s"""
+ |INSERT OVERWRITE LOCAL DIRECTORY '${dir.toURI.getPath}'
+ |STORED AS $fileFormat
+ |SELECT $expr AS c
+ """.stripMargin)
+ }
+ checkError(
+ exception = e.getCause.asInstanceOf[AnalysisException],
+ condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
+ parameters = Map(
+ "columnName" -> "`c`",
+ "columnType" -> s""""$columnType"""",
+ "format" -> "Hive"))
+ }
+ }
+ }
+ }
+ }
+
test("insert overwrite to dir from temp table") {
withTempView("test_insert_table") {
spark.range(10).selectExpr("id", "id AS
str").createOrReplaceTempView("test_insert_table")
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/MetastoreDataSourcesSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/MetastoreDataSourcesSuite.scala
index 71fd3e9b28fc..476bd133aaf5 100644
---
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/MetastoreDataSourcesSuite.scala
+++
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/MetastoreDataSourcesSuite.scala
@@ -1343,7 +1343,7 @@ class MetastoreDataSourcesSuite extends QueryTest
}
}
// The Hive SerDe can't parse these types.
- Seq("TIME(6)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(9)", "VARIANT").foreach {
dataType =>
+ Seq("TIME(6)", "TIMESTAMP_NTZ(9)", "VARIANT").foreach { dataType =>
checkKeepsTable("textfile", "id", s"CAST(NULL AS $dataType) AS c")
}
// The Parquet Hive SerDe doesn't support the void type.
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveDDLSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveDDLSuite.scala
index ff07dd6b7020..9e0c1b988ac4 100644
---
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveDDLSuite.scala
+++
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveDDLSuite.scala
@@ -3295,6 +3295,90 @@ class HiveDDLSuite
}
}
+ test("SPARK-60545: Disallow TIMESTAMP_NTZ columns when the provider is
Hive") {
+ val tbl = "tbl_with_timestamp_ntz"
+ def checkUnsupported(columnType: String)(createTable: => Unit): Unit = {
+ checkError(
+ exception = intercept[AnalysisException](createTable),
+ condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
+ parameters = Map(
+ "columnName" -> "`c`",
+ "columnType" -> s""""$columnType"""",
+ "format" -> "Hive"))
+ assert(!spark.catalog.tableExists(tbl))
+ }
+ withTable(tbl, "src") {
+ withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> "TIMESTAMP_NTZ") {
+ checkUnsupported("TIMESTAMP_NTZ") {
+ sql(s"CREATE TABLE $tbl (c TIMESTAMP) STORED AS PARQUET")
+ }
+ checkUnsupported("TIMESTAMP_NTZ") {
+ sql(s"CREATE TABLE $tbl STORED AS ORC AS SELECT TIMESTAMP'2026-01-01
00:00:00' AS c")
+ }
+ }
+ Seq(
+ "TIMESTAMP_NTZ",
+ "STRUCT<a: TIMESTAMP_NTZ>",
+ "ARRAY<TIMESTAMP_NTZ>",
+ "MAP<STRING, TIMESTAMP_NTZ>").foreach { dataType =>
+ checkUnsupported(dataType) {
+ sql(s"CREATE TABLE $tbl (c $dataType) STORED AS TEXTFILE")
+ }
+ checkUnsupported(dataType) {
+ sql(s"CREATE TABLE $tbl (pt INT, c $dataType) USING HIVE PARTITIONED
BY (pt)")
+ }
+ checkUnsupported(dataType) {
+ sql(s"CREATE TABLE $tbl STORED AS PARQUET AS SELECT CAST(NULL AS
$dataType) AS c")
+ }
+ checkUnsupported(dataType) {
+ sql(s"SELECT CAST(NULL AS $dataType) AS
c").write.format("hive").saveAsTable(tbl)
+ }
+ sql(s"CREATE TABLE src (c $dataType) USING PARQUET")
+ checkUnsupported(dataType) {
+ sql(s"CREATE TABLE $tbl LIKE src STORED AS ORC")
+ }
+ sql("DROP TABLE src")
+ }
+ }
+ }
+
+ test("SPARK-60545: CREATE TABLE IF NOT EXISTS with TIMESTAMP_NTZ columns is
a no-op for " +
+ "an existing Hive serde table") {
+ val tbl = "tbl_with_timestamp_ntz"
+ withTable(tbl) {
+ sql(s"CREATE TABLE $tbl (c TIMESTAMP) STORED AS PARQUET")
+ sql(s"INSERT INTO $tbl VALUES (TIMESTAMP'2026-01-01 00:00:00')")
+ withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> "TIMESTAMP_NTZ") {
+ sql(s"CREATE TABLE IF NOT EXISTS $tbl (c TIMESTAMP) STORED AS PARQUET")
+ sql(s"CREATE TABLE IF NOT EXISTS $tbl STORED AS PARQUET " +
+ "AS SELECT TIMESTAMP'2026-01-01 00:00:00' AS c")
+ }
+ sql(s"CREATE TABLE IF NOT EXISTS $tbl (c TIMESTAMP_NTZ) STORED AS
PARQUET")
+ assert(spark.table(tbl).schema === new StructType().add("c",
TimestampType))
+ assert(spark.table(tbl).count() === 1)
+ }
+ }
+
+ test("SPARK-60545: saveAsTable with mode Overwrite keeps the existing table
when the new " +
+ "Hive serde table has a TIMESTAMP_NTZ column") {
+ val tbl = "tbl_with_timestamp_ntz"
+ withTable(tbl) {
+ sql(s"CREATE TABLE $tbl (c INT) STORED AS PARQUET")
+ sql(s"INSERT INTO $tbl VALUES (1)")
+ checkError(
+ exception = intercept[AnalysisException] {
+ sql("SELECT TIMESTAMP_NTZ'2026-01-01 00:00:00' AS c")
+ .write.format("hive").mode(SaveMode.Overwrite).saveAsTable(tbl)
+ },
+ condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
+ parameters = Map(
+ "columnName" -> "`c`",
+ "columnType" -> "\"TIMESTAMP_NTZ\"",
+ "format" -> "Hive"))
+ checkAnswer(spark.table(tbl), Row(1))
+ }
+ }
+
test("SPARK-37046: Alter view should preserve column case with view
definition change") {
withView("v") {
// Changing view definition should preserve column case
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/command/AlterTableAddColumnsSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/command/AlterTableAddColumnsSuite.scala
index 521ad759c302..3ce225941fa4 100644
---
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/command/AlterTableAddColumnsSuite.scala
+++
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/command/AlterTableAddColumnsSuite.scala
@@ -18,8 +18,10 @@
package org.apache.spark.sql.hive.execution.command
import org.apache.spark.SparkUnsupportedOperationException
+import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.util.TypeUtils.toSQLId
import org.apache.spark.sql.execution.command.v1
+import org.apache.spark.sql.internal.SQLConf
/**
* The class contains tests for the `ALTER TABLE .. ADD COLUMNS` command to
check
@@ -48,4 +50,26 @@ class AlterTableAddColumnsSuite
check(tbl)
}
}
+
+ test("SPARK-60545: Disallow TIMESTAMP_NTZ columns when the provider is
Hive") {
+ def check(tbl: String, dataType: String, columnType: String): Unit = {
+ checkError(
+ exception = intercept[AnalysisException] {
+ sql(s"ALTER TABLE $tbl ADD COLUMNS (c $dataType)")
+ },
+ condition = "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE",
+ parameters = Map(
+ "columnName" -> "`c`",
+ "columnType" -> s""""$columnType"""",
+ "format" -> "Hive"))
+ }
+ withNamespaceAndTable("ns", "tbl") { tbl =>
+ sql(s"CREATE TABLE $tbl (id INT) $defaultUsing")
+ check(tbl, "TIMESTAMP_NTZ", "TIMESTAMP_NTZ")
+ check(tbl, "ARRAY<TIMESTAMP_NTZ>", "ARRAY<TIMESTAMP_NTZ>")
+ withSQLConf(SQLConf.TIMESTAMP_TYPE.key -> "TIMESTAMP_NTZ") {
+ check(tbl, "TIMESTAMP", "TIMESTAMP_NTZ")
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]