This is an automated email from the ASF dual-hosted git repository.

uros-b 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 0919cd405232 [SPARK-58825][SQL] Support TimestampNTZType as a JDBC 
partition column
0919cd405232 is described below

commit 0919cd4052329fc1cb8160c5965eefd15ebb1edd
Author: aleksandar-trajkovic-db <[email protected]>
AuthorDate: Fri Aug 21 11:17:08 2026 +0000

    [SPARK-58825][SQL] Support TimestampNTZType as a JDBC partition column
    
    ### What changes were proposed in this pull request?
    
    `JDBCRelation` restricts the `partitionColumn` option to numeric, 
`DateType`, and `TimestampType` columns (SPARK-22814). This extends that set to 
include `TimestampNTZType`:
    
    - `verifyAndGetNormalizedPartitionColumn` accepts `TimestampNTZType`.
    - Bound parsing uses `stringToTimestampWithoutTimeZone(_, allowTimeZone = 
false)`, so `lowerBound`/`upperBound` are parsed as zoneless wall-clock values.
    - Bound-to-string conversion formats NTZ micros in UTC, so no 
session-time-zone shift is applied to the generated partition predicates.
    
    Behavior for existing numeric/`DateType`/`TimestampType` partition columns 
is unchanged.
    
    ### Why are the changes needed?
    
    More JDBC sources map zoneless database types (timestamp-without-time-zone) 
to `TimestampNTZType`. Such a column is a valid, orderable partition column, 
but today it is rejected with `invalidPartitionColumnTypeError`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. A `TimestampNTZType` column can now be used as the JDBC 
`partitionColumn`; it was previously rejected with 
`invalidPartitionColumnTypeError`. There is no change for existing 
numeric/`DateType`/`TimestampType` partition columns. This is a new capability 
on `master`, not a change to released behavior.
    
    ### How was this patch tested?
    
    New unit tests in `JDBCSuite`:
    - `columnPartition supports TimestampNTZType partition column` — bound 
normalization (`lowerBound`/`upperBound` → partition predicates) for an NTZ 
partition column.
    - `columnPartition rejects zoned bounds for a TimestampNTZType partition 
column` — negative case, zoned bounds are rejected.
    - `support TimestampNTZType partition column end-to-end` — reads a genuine 
`TimestampNTZType` column partitioned across bounds and verifies the results.
    
    All pass locally (`build/sbt "sql/testOnly 
org.apache.spark.sql.jdbc.JDBCSuite"`).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    ---
    
    This is my original work and I license it to the project under the 
project's open source license.
    
    Closes #58082 from aleksandar-trajkovic-db/spark-jdbc-ntz-partition.
    
    Authored-by: aleksandar-trajkovic-db <[email protected]>
    Signed-off-by: Uros Bojanic <[email protected]>
---
 .../execution/datasources/jdbc/JDBCRelation.scala  |  14 ++-
 .../org/apache/spark/sql/jdbc/JDBCSuite.scala      | 107 +++++++++++++++++++++
 2 files changed, 117 insertions(+), 4 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCRelation.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCRelation.scala
index 972bb3e35ee6..1c529279744e 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCRelation.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCRelation.scala
@@ -27,14 +27,14 @@ import org.apache.spark.rdd.RDD
 import org.apache.spark.sql.{DataFrame, Row, SaveMode, SparkSession, 
SQLContext}
 import org.apache.spark.sql.catalyst.analysis._
 import org.apache.spark.sql.catalyst.util.{DateFormatter, DateTimeUtils, 
TimestampFormatter}
-import org.apache.spark.sql.catalyst.util.DateTimeUtils.{getZoneId, 
stringToDate, stringToTimestamp}
+import org.apache.spark.sql.catalyst.util.DateTimeUtils.{getZoneId, 
stringToDate, stringToTimestamp, stringToTimestampWithoutTimeZone}
 import org.apache.spark.sql.connector.expressions.filter.Predicate
 import org.apache.spark.sql.errors.QueryCompilationErrors
 import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.jdbc.JdbcDialects
 import org.apache.spark.sql.sources._
-import org.apache.spark.sql.types.{DataType, DateType, NumericType, 
StructType, TimestampType}
+import org.apache.spark.sql.types.{DataType, DateType, NumericType, 
StructType, TimestampNTZType, TimestampType}
 import org.apache.spark.unsafe.types.UTF8String
 
 /**
@@ -188,7 +188,7 @@ private[sql] object JDBCRelation extends Logging {
         columnName, schema.simpleString(maxNumToStringFields))
     }
     column.dataType match {
-      case _: NumericType | DateType | TimestampType =>
+      case _: NumericType | DateType | TimestampType | TimestampNTZType =>
       case _ =>
         throw QueryCompilationErrors.invalidPartitionColumnTypeError(column)
     }
@@ -209,6 +209,7 @@ private[sql] object JDBCRelation extends Logging {
       case _: NumericType => value.toLong
       case DateType => parse(stringToDate).toLong
       case TimestampType => parse(stringToTimestamp(_, getZoneId(timeZoneId)))
+      case TimestampNTZType => parse(stringToTimestampWithoutTimeZone(_, 
allowTimeZone = false))
     }
   }
 
@@ -224,12 +225,17 @@ private[sql] object JDBCRelation extends Logging {
           val timestampFormatter = TimestampFormatter.getFractionFormatter(
             DateTimeUtils.getZoneId(timeZoneId))
           timestampFormatter.format(value)
+        case TimestampNTZType =>
+          // NTZ micros are zoneless wall-clock values; format in UTC so no 
zone shift is applied.
+          val timestampFormatter = TimestampFormatter.getFractionFormatter(
+            DateTimeUtils.getZoneId("UTC"))
+          timestampFormatter.format(value)
       }
       s"'$dateTimeStr'"
     }
     columnType match {
       case _: NumericType => value.toString
-      case DateType | TimestampType => dateTimeToString()
+      case DateType | TimestampType | TimestampNTZType => dateTimeToString()
     }
   }
 
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
index 16f3f697ff17..6642b527e40e 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
@@ -479,6 +479,67 @@ class JDBCSuite extends SharedSparkSession {
     assert(lastPredicate == """"PartitionColumn" >= '2020-08-02'""")
   }
 
+  test("columnPartition supports TimestampNTZType partition column") {
+    val schema = StructType(Seq(
+      StructField("PartitionColumn", TimestampNTZType)
+    ))
+
+    // (lowerBound, upperBound, numPartitions, expected where clauses in 
partition order).
+    val cases = Seq(
+      ("2018-07-06 10:00:00", "2018-07-06 16:00:00", "3", Seq(
+        """"PartitionColumn" < '2018-07-06 12:00:00' or "PartitionColumn" is 
null""",
+        """"PartitionColumn" >= '2018-07-06 12:00:00' AND """ +
+          """"PartitionColumn" < '2018-07-06 14:00:00'""",
+        """"PartitionColumn" >= '2018-07-06 14:00:00'""")),
+      // Fractional-second bounds parse, and the zoneless midpoint keeps 
sub-second precision.
+      ("2018-07-06 10:00:00.100", "2018-07-06 10:00:00.300", "2", Seq(
+        """"PartitionColumn" < '2018-07-06 10:00:00.2' or "PartitionColumn" is 
null""",
+        """"PartitionColumn" >= '2018-07-06 10:00:00.2'"""))
+    )
+
+    // NTZ bounds are zoneless, so the generated predicates must be identical 
regardless of the
+    // session time zone (unlike TimestampType, which shifts by the zone).
+    Seq("UTC", "America/Los_Angeles", "Asia/Kolkata").foreach { tz =>
+      cases.foreach { case (lowerBound, upperBound, numPartitions, expected) =>
+        val partitions = JDBCRelation.columnPartition(
+          schema,
+          analysis.caseInsensitiveResolution,
+          tz,
+          new JDBCOptions(url, "table", Map(
+            "lowerBound" -> lowerBound,
+            "upperBound" -> upperBound,
+            "numPartitions" -> numPartitions,
+            "partitionColumn" -> "PartitionColumn")))
+
+        val clauses = partitions.map(_.asInstanceOf[JDBCPartition].whereClause)
+        assert(clauses === expected.toArray,
+          s"NTZ partition clauses should be time-zone independent, but 
differed for tz=$tz " +
+            s"(bounds $lowerBound..$upperBound)")
+      }
+    }
+  }
+
+  test("columnPartition rejects zoned bounds for a TimestampNTZType partition 
column") {
+    val schema = StructType(Seq(
+      StructField("PartitionColumn", TimestampNTZType)
+    ))
+    // allowTimeZone = false: a bound carrying a zone offset is rejected 
rather than silently
+    // shifted, so NTZ bounds stay zoneless.
+    val e = intercept[IllegalArgumentException] {
+      JDBCRelation.columnPartition(
+        schema,
+        analysis.caseInsensitiveResolution,
+        "America/Los_Angeles",
+        new JDBCOptions(url, "table", Map(
+          "lowerBound" -> "2018-07-06 10:00:00+05:00",
+          "upperBound" -> "2018-07-06 16:00:00+05:00",
+          "numPartitions" -> "2",
+          "partitionColumn" -> "PartitionColumn")))
+    }
+    assert(e.getMessage.contains("Cannot parse the bound value"))
+    assert(e.getMessage.contains("2018-07-06 10:00:00+05:00"))
+  }
+
   test("overflow of partition bound difference does not give negative stride") 
{
     val df = sql("SELECT * FROM partsoverflow")
     checkNumPartitions(df, expectedNumPartitions = 3)
@@ -2487,6 +2548,52 @@ class JDBCSuite extends SharedSparkSession {
     checkAnswer(df2, expectedResult)
   }
 
+  test("support TimestampNTZType partition column end-to-end") {
+    val tableName = "timestamp_ntz_partition_table"
+    // Write a genuine TimestampNTZType column through Spark so it round-trips 
as a zoneless
+    // wall-clock value (a raw JDBC TIMESTAMP column would pick up a 
JVM-time-zone shift on read).
+    val df = Seq(
+      "2018-07-06T05:50:00",
+      "2018-07-06T08:10:08",
+      "2018-07-08T13:32:01",
+      "2018-07-12T09:51:15"
+    ).map(LocalDateTime.parse).toDF("t")
+    df.write.format("jdbc")
+      .mode("overwrite")
+      .option("url", urlWithUserAndPass)
+      .option("dbtable", tableName)
+      .save()
+
+    // Bounds are zoneless, so both the generated predicates and the results 
must be identical
+    // regardless of the JVM default time zone.
+    DateTimeTestUtils.outstandingZoneIds.foreach { zoneId =>
+      DateTimeTestUtils.withDefaultTimeZone(zoneId) {
+        val readDf = spark.read.format("jdbc")
+          .option("url", urlWithUserAndPass)
+          .option("dbtable", tableName)
+          .option("preferTimestampNTZ", true)
+          .option("partitionColumn", "t")
+          .option("lowerBound", "2018-07-04 03:30:00")
+          .option("upperBound", "2018-07-27 14:11:05")
+          .option("numPartitions", 2)
+          .load()
+
+        assert(readDf.schema("t").dataType === TimestampNTZType)
+
+        readDf.logicalPlan match {
+          case LogicalRelationWithTable(JDBCRelation(_, parts, _, _), _) =>
+            val whereClauses = 
parts.map(_.asInstanceOf[JDBCPartition].whereClause).toSet
+            assert(whereClauses === Set(
+              """"t" < '2018-07-15 20:50:32.5' or "t" is null""",
+              """"t" >= '2018-07-15 20:50:32.5'"""),
+              s"NTZ partition predicates should be time-zone independent, but 
differed for " +
+                s"zone=$zoneId")
+        }
+        checkAnswer(readDf, df)
+      }
+    }
+  }
+
   test("throws an exception for unsupported partition column types") {
     val errMsg = intercept[AnalysisException] {
       spark.read.format("jdbc")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to