Skip to content

Commit 16dacbb

Browse files
committed
[SPARK-57463][SQL] Render nanosecond-precision timestamp types in the Thrift server via the Types Framework
### What changes were proposed in this pull request? This PR makes the nanosecond-capable timestamp types `TIMESTAMP_NTZ(p)` and `TIMESTAMP_LTZ(p)` (`p` in 7-9) usable over the Spark Thrift / JDBC server, reaching parity with the microsecond `TimestampType` / `TimestampNTZType`. 1. Implement the Types Framework `thriftTypeName` hook. `SparkExecuteStatementOperation` resolves a column's Thrift `TTypeId` via `TypeApiOps(typ).flatMap(_.thriftTypeName)`. `TimestampNanosTypeApiOps` did not override it (defaulted to `None`), and the nanos types are not in the `toTTypeIdDefault` fallback, so a nanos column hit `case other => throw new IllegalArgumentException("Unrecognized type name: ...")`. The fix overrides `thriftTypeName` in the abstract base `TimestampNanosTypeApiOps` (inherited by both NTZ and LTZ subclasses) to return `Some("STRING_TYPE")`, mirroring the reference `TimeTypeApiOps`. `STRING_TYPE` is correct because `RowSetUtils` already serializes these values as a string column (`TStringColumn`), rendered at the column precision by `HiveResult.toHiveString`. 2. Enable the nanosecond golden-file tests in `ThriftServerQueryTestSuite` by removing `timestamp-ntz-nanos.sql` and `timestamp-ltz-nanos.sql` from its ignore list (they were skipped only because nanos types were not yet mapped by the Thrift server). 3. Drop the now-unnecessary `cast(... as string)` workarounds for the micros -> nanos widening cases in `cast.sql` (SPARK-57293 section), which existed only because a bare nanos result column was not serializable over JDBC/thrift. They now produce bare `TIMESTAMP_NTZ(9)` / `TIMESTAMP_LTZ(9)` result columns; golden files are regenerated and the output values are unchanged. No changes were needed in `SparkExecuteStatementOperation` or `RowSetUtils`. Related Hive CLI rendering through the framework is tracked separately by SPARK-57386. ### Why are the changes needed? To be able to retrieve nanosecond-precision timestamps via the Hive Thrift server. Before this change, with the preview flag enabled, such a query fails: ``` 0: jdbc:hive2://localhost:10000/default> SET spark.sql.timestampNanosTypes.enabled=true; 0: jdbc:hive2://localhost:10000/default> SELECT timestamp_ntz'2021-01-01 01:02:03.000000001'; Error: java.lang.IllegalArgumentException: Unrecognized type name: timestamp_ntz(9) (state=,code=0) ``` This is analogous to the ANSI-interval issue fixed by SPARK-35017 (`Unrecognized type name: day-time interval`) and the TIME support added by SPARK-51516. ### Does this PR introduce _any_ user-facing change? Yes. After the changes, nanosecond timestamp columns are returned over JDBC as strings rendered at the column precision (the nanos types are a preview feature gated by `spark.sql.timestampNanosTypes.enabled`): ``` 0: jdbc:hive2://localhost:10000/default> SET spark.sql.timestampNanosTypes.enabled=true; 0: jdbc:hive2://localhost:10000/default> SELECT timestamp_ntz'2021-01-01 01:02:03.000000001' AS ntz9; +--------------------------------+ | ntz9 | +--------------------------------+ | 2021-01-01 01:02:03.000000001 | +--------------------------------+ 0: jdbc:hive2://localhost:10000/default> SELECT timestamp_ltz'2021-01-01 01:02:03.123456789' AS ltz9; +--------------------------------+ | ltz9 | +--------------------------------+ | 2021-01-01 01:02:03.123456789 | +--------------------------------+ 0: jdbc:hive2://localhost:10000/default> SELECT CAST('2021-01-01 01:02:03.123456789' AS TIMESTAMP_NTZ(7)) AS ntz7; +------------------------------+ | ntz7 | +------------------------------+ | 2021-01-01 01:02:03.1234567 | +------------------------------+ 0: jdbc:hive2://localhost:10000/default> SELECT CAST('2021-01-01 01:02:03.123456789' AS TIMESTAMP_LTZ(8)) AS ltz8; +-------------------------------+ | ltz8 | +-------------------------------+ | 2021-01-01 01:02:03.12345678 | +-------------------------------+ ``` With the flag off (the production default), a nanos literal continues to degrade to a microsecond timestamp, unchanged by this PR: ``` 0: jdbc:hive2://localhost:10000/default> SELECT timestamp_ntz'2021-01-01 01:02:03.000000001' AS ntz; +------------------------+ | ntz | +------------------------+ | 2021-01-01 01:02:03.0 | +------------------------+ ``` ### How was this patch tested? 1. New tests under `sql/hive-thriftserver`: - `SparkExecuteStatementOperationSuite`: asserts `toTTableSchema` maps `TimestampNTZNanosType(p)` / `TimestampLTZNanosType(p)` (p in 7-9) to `TTypeId.STRING_TYPE`. - `HiveThriftBinaryServerSuite`: an end-to-end JDBC test that enables the flag, queries NTZ/LTZ at precisions 7-9, and asserts both the column metadata (`VARCHAR` / `"string"`) and the rendered fractional digits. 2. `ThriftServerQueryTestSuite` now runs `timestamp-ntz-nanos.sql`, `timestamp-ltz-nanos.sql`, and `cast.sql` end-to-end over JDBC (previously the nanos files were ignored). ``` $ build/sbt -Phive -Phive-thriftserver "hive-thriftserver/testOnly *SparkExecuteStatementOperationSuite" $ build/sbt -Phive -Phive-thriftserver "hive-thriftserver/testOnly *HiveThriftBinaryServerSuite -- -z nanosecond" $ build/sbt -Phive -Phive-thriftserver "hive-thriftserver/testOnly *ThriftServerQueryTestSuite -- -z nanos" $ build/sbt -Phive -Phive-thriftserver "hive-thriftserver/testOnly *ThriftServerQueryTestSuite -- -z cast.sql" ``` 3. Regenerated `cast.sql` golden files via `SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/testOnly org.apache.spark.sql.SQLQueryTestSuite -- -z cast.sql"`; the output values are unchanged (only the result column type changed from `string` to the nanos type). 4. Manually verified end-to-end against a running Thrift server with `beeline` (the before/after transcripts shown above). ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Cursor (Claude Opus 4.8) Closes #56519 from MaxGekk/nanos-thriftserver. Authored-by: Maxim Gekk <max.gekk@gmail.com> Signed-off-by: Max Gekk <max.gekk@gmail.com> (cherry picked from commit a6e3fdd) Signed-off-by: Max Gekk <max.gekk@gmail.com>
1 parent 03c44cc commit 16dacbb

9 files changed

Lines changed: 68 additions & 25 deletions

File tree

sql/api/src/main/scala/org/apache/spark/sql/types/ops/TimestampNanosTypeApiOps.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ abstract class TimestampNanosTypeApiOps extends TypeApiOps with DataTypeErrorsBa
6767

6868
override def toSQLValue(v: Any): String = s"$sqlTypeName '${format(v)}'"
6969

70+
// ==================== Thrift Mapping ====================
71+
72+
// Over the Thrift / JDBC server the nanosecond timestamp value is sent as a string column
73+
// (RowSetUtils renders it via HiveResult.toHiveString at the column precision), so map the
74+
// column to STRING_TYPE for consistency, mirroring the reference TimeType ops.
75+
override def thriftTypeName: Option[String] = Some("STRING_TYPE")
76+
7077
// ==================== Row Encoding ====================
7178

7279
// Honor the spark.sql.timestampNanosTypes.enabled gate just like the legacy

sql/core/src/test/resources/sql-tests/analyzer-results/cast.sql.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -775,9 +775,9 @@ Project [concat(ts=, cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9)
775775

776776

777777
-- !query
778-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as string)
778+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9))
779779
-- !query analysis
780-
Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestamp_ntz(9)) as string) AS CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)) AS STRING)#x]
780+
Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestamp_ntz(9)) AS CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9))#x]
781781
+- OneRowRelation
782782

783783

@@ -796,9 +796,9 @@ Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestam
796796

797797

798798
-- !query
799-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as string)
799+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9))
800800
-- !query analysis
801-
Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp) as timestamp_ltz(9)) as string) AS CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)) AS STRING)#x]
801+
Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp) as timestamp_ltz(9)) AS CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9))#x]
802802
+- OneRowRelation
803803

804804

sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/cast.sql.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,9 +639,9 @@ Project [concat(ts=, cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9)
639639

640640

641641
-- !query
642-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as string)
642+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9))
643643
-- !query analysis
644-
Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestamp_ntz(9)) as string) AS CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)) AS STRING)#x]
644+
Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestamp_ntz(9)) AS CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9))#x]
645645
+- OneRowRelation
646646

647647

@@ -660,9 +660,9 @@ Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp_ntz) as timestam
660660

661661

662662
-- !query
663-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as string)
663+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9))
664664
-- !query analysis
665-
Project [cast(cast(cast(2020-01-01 00:00:00.123456 as timestamp) as timestamp_ltz(9)) as string) AS CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)) AS STRING)#x]
665+
Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp) as timestamp_ltz(9)) AS CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9))#x]
666666
+- OneRowRelation
667667

668668

sql/core/src/test/resources/sql-tests/inputs/cast.sql

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,15 @@ select concat('ts=', cast(cast('2020-01-01 00:00:00.123456789' as timestamp_ntz(
138138

139139
-- SPARK-57293: cast between nanosecond-precision timestamps and their microsecond counterparts.
140140
-- Both directions stay within one zone family (pure representation conversions, no timezone
141-
-- involvement). Widening (micros -> nanos(p)) is wrapped in cast(... as string) because a bare
142-
-- nanos result column is not yet serializable by JDBC/thrift; narrowing/round-trip yield micros.
141+
-- involvement); narrowing/round-trip yield micros.
143142
-- TIMESTAMP_NTZ <-> TIMESTAMP_NTZ(p): widening sets the sub-microsecond part to 0.
144-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as string);
143+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9));
145144
-- Narrowing TIMESTAMP_NTZ(p) -> TIMESTAMP_NTZ drops the sub-microsecond digits (floor).
146145
select cast(cast('2020-01-01 00:00:00.123456789' as timestamp_ntz(9)) as timestamp_ntz);
147146
-- Round-trip micros -> nanos(p) -> micros returns the original microseconds.
148147
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as timestamp_ntz);
149148
-- TIMESTAMP_LTZ <-> TIMESTAMP_LTZ(p): same conversions on the epoch-micros instant.
150-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as string);
149+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9));
151150
select cast(cast('2020-01-01 00:00:00.123456789' as timestamp_ltz(9)) as timestamp);
152151
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as timestamp);
153152

sql/core/src/test/resources/sql-tests/results/cast.sql.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,9 +1475,9 @@ ts=2020-01-01 00:00:00.123456789
14751475

14761476

14771477
-- !query
1478-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as string)
1478+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9))
14791479
-- !query schema
1480-
struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)) AS STRING):string>
1480+
struct<CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
14811481
-- !query output
14821482
2020-01-01 00:00:00.123456
14831483

@@ -1499,9 +1499,9 @@ struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_
14991499

15001500

15011501
-- !query
1502-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as string)
1502+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9))
15031503
-- !query schema
1504-
struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)) AS STRING):string>
1504+
struct<CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
15051505
-- !query output
15061506
2020-01-01 00:00:00.123456
15071507

sql/core/src/test/resources/sql-tests/results/nonansi/cast.sql.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -737,9 +737,9 @@ ts=2020-01-01 00:00:00.123456789
737737

738738

739739
-- !query
740-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9)) as string)
740+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp_ntz) as timestamp_ntz(9))
741741
-- !query schema
742-
struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)) AS STRING):string>
742+
struct<CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
743743
-- !query output
744744
2020-01-01 00:00:00.123456
745745

@@ -761,9 +761,9 @@ struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP_NTZ) AS TIMESTAMP_
761761

762762

763763
-- !query
764-
select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9)) as string)
764+
select cast(cast('2020-01-01 00:00:00.123456' as timestamp) as timestamp_ltz(9))
765765
-- !query schema
766-
struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)) AS STRING):string>
766+
struct<CAST(CAST(2020-01-01 00:00:00.123456 AS TIMESTAMP) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
767767
-- !query output
768768
2020-01-01 00:00:00.123456
769769

sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2Suites.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,31 @@ class HiveThriftBinaryServerSuite extends HiveThriftServer2Test {
244244
}
245245
}
246246

247+
test("SPARK-57463: nanosecond-precision timestamp types over JDBC") {
248+
withJdbcStatement() { statement =>
249+
statement.execute("SET spark.sql.timestampNanosTypes.enabled=true")
250+
// Fix the session zone so the LTZ wall-clock value is deterministic.
251+
statement.execute("SET spark.sql.session.timeZone=UTC")
252+
253+
// The cast truncates the 9-digit fraction to the column precision (SPARK-57256), matching
254+
// the cast-to-string / HiveResult rendering.
255+
val expectedFractions = Map(7 -> ".1234567", 8 -> ".12345678", 9 -> ".123456789")
256+
val base = "2019-07-22 18:14:00"
257+
Seq("TIMESTAMP_NTZ", "TIMESTAMP_LTZ").foreach { typeName =>
258+
for (p <- 7 to 9) {
259+
val resultSet = statement.executeQuery(
260+
s"SELECT CAST('$base.123456789' AS $typeName($p))")
261+
assert(resultSet.next())
262+
assert(resultSet.getString(1) === s"$base${expectedFractions(p)}")
263+
val metaData = resultSet.getMetaData
264+
// The nanosecond timestamp column is mapped to STRING_TYPE in the Thrift server.
265+
assert(metaData.getColumnTypeName(1) === "string")
266+
assert(metaData.getColumnType(1) === java.sql.Types.VARCHAR)
267+
}
268+
}
269+
}
270+
}
271+
247272
test("SPARK-4407 regression: Complex type support") {
248273
withJdbcStatement("test_map") { statement =>
249274
val queries = Seq(

sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperationSuite.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import org.mockito.invocation.InvocationOnMock
3232
import org.apache.spark.sql.classic.{DataFrame, SparkSession}
3333
import org.apache.spark.sql.hive.thriftserver.ui.HiveThriftServer2EventManager
3434
import org.apache.spark.sql.test.SharedSparkSession
35-
import org.apache.spark.sql.types.{GeographyType, GeometryType, IntegerType, NullType, StringType, StructField, StructType}
35+
import org.apache.spark.sql.types.{GeographyType, GeometryType, IntegerType, NullType, StringType, StructField, StructType, TimestampLTZNanosType, TimestampNTZNanosType}
3636

3737
class SparkExecuteStatementOperationSuite extends SharedSparkSession {
3838

@@ -76,6 +76,21 @@ class SparkExecuteStatementOperationSuite extends SharedSparkSession {
7676
assert(geogType === TTypeId.STRING_TYPE)
7777
}
7878

79+
test("SPARK-57463 nanosecond-precision timestamp types are mapped to STRING_TYPE " +
80+
"in ThriftServer") {
81+
for (p <- TimestampNTZNanosType.MIN_PRECISION to TimestampNTZNanosType.MAX_PRECISION) {
82+
val tableSchema = StructType(Seq(
83+
StructField("ntz", TimestampNTZNanosType(p)),
84+
StructField("ltz", TimestampLTZNanosType(p))))
85+
val columns = SparkExecuteStatementOperation.toTTableSchema(tableSchema)
86+
assert(columns.getColumnsSize == 2)
87+
val ntzType = columns.getColumns.get(0).getTypeDesc.getTypes.get(0).getPrimitiveEntry.getType
88+
assert(ntzType === TTypeId.STRING_TYPE)
89+
val ltzType = columns.getColumns.get(1).getTypeDesc.getTypes.get(0).getPrimitiveEntry.getType
90+
assert(ltzType === TTypeId.STRING_TYPE)
91+
}
92+
}
93+
7994
Seq(
8095
(OperationState.CANCELED, (_: SparkExecuteStatementOperation).cancel()),
8196
(OperationState.TIMEDOUT, (_: SparkExecuteStatementOperation).timeoutCancel()),

sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerQueryTestSuite.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,7 @@ class ThriftServerQueryTestSuite extends SQLQueryTestSuite with SharedThriftServ
118118
"pipe-operators.sql",
119119
// VARIANT type
120120
"variant/named-function-arguments.sql",
121-
"variant-field-extractions.sql",
122-
// SPARK-57257: nanosecond-precision timestamp types are not yet mapped by the Thrift Server
123-
"timestamp-ltz-nanos.sql",
124-
"timestamp-ntz-nanos.sql"
121+
"variant-field-extractions.sql"
125122
)
126123

127124
override def runQueries(

0 commit comments

Comments
 (0)