spark-test commented on code in PR #59990:
URL: https://github.com/apache/spark/pull/59990#discussion_r4436321971
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTable.scala:
##########
@@ -59,15 +60,25 @@ case class JDBCTable(
util.EnumSet.of(BATCH_READ, V1_BATCH_WRITE, TRUNCATE)
}
- override def newScanBuilder(options: CaseInsensitiveStringMap):
JDBCScanBuilder = {
- val mergedOptions = new JDBCOptions(
+ private def mergeOptions(options: CaseInsensitiveStringMap):
CaseInsensitiveMap[String] = {
+ // Preserve case-sensitive driver properties when merging query options.
+ val merged = CaseInsensitiveMap(
jdbcOptions.parameters.originalMap ++
options.asCaseSensitiveMap().asScala)
+ // Only normalize the timestamp options, preserving the table defaults
unless overridden.
+ merged +
+ (JDBCOptions.JDBC_TIMESTAMP_NTZ_WALL_CLOCK -> options.getOrDefault(
+ JDBCOptions.JDBC_TIMESTAMP_NTZ_WALL_CLOCK,
jdbcOptions.timestampNTZAsWallClock.toString)) +
+ (JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ -> options.getOrDefault(
Review Comment:
**Non-blocking (P2):** For a JDBC catalog table, the schema is resolved once
in `JDBCTableCatalog.loadTable` from the catalog options, and the docs say
per-query `zonelessTimestampAsNTZ` values do not apply there
(`docs/sql-data-sources-jdbc.md:389`, `docs/sql-migration-guide.md:50`). This
line still lets a per-query value replace the table's resolved one. In the
builder, the option's only reader is aggregate pushdown. `pushAggregation`
re-infers the pushed query's output types with it (`JDBCScanBuilder.scala:139`,
`JdbcUtils.scala:355-356`), and `buildScanWithPushedAggregate` silently casts
each column back to the schema's type in the session time zone.
Take a table that resolved `ts` as `TimestampNTZType`, and run `SELECT
MAX(ts) FROM ora.s.t WITH ('zonelessTimestampAsNTZ' = 'false')` with the
default `preferTimestampNTZ=false`. The pushed `MAX(ts)` is read as
`TimestampType` through `rs.getTimestamp` in the JVM default zone, then cast
back in the session zone. It comes back shifted by the offset between the two
zones, while the unpushed plan returns the exact wall clock. `MIN`, a `GROUP
BY` key on such a column, and the reverse case are affected the same way; the
reverse case is a per-query `true` on a catalog that sets `false`. Plain scans
and writes never read this option.
The base branch already behaved this way. But this PR now carries the
table's value into the builders to fix exactly this mismatch when the flag
flips, and it exempts these inputs. The new grid also asserts the per-query
precedence for this key (`JDBCTableCatalogSuite.scala:152-153`). Consider
always using the table's value for this key, regardless of the query options,
and keeping the per-query override only for `timestampNTZAsWallClock`, which
the docs allow per read or write:
```scala
(JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ ->
jdbcOptions.zonelessTimestampAsNTZ.toString)
```
The grid would then expect the table's value whatever the query supplies.
This extends an [existing
discussion](https://github.com/apache/spark/pull/59990#issuecomment-6558990658).
**Verification:**
- **Behavior:** For a per-query zonelessTimestampAsNTZ of either value and
in either casing, both the scan builder and the write builder report the
table's resolved zonelessTimestampAsNTZ, in both legacy-flag flip directions.
- **Regression:** Against Oracle, with the session time zone different from
the JVM default zone and the default spark.sql.timestampType, pushed aggregates
return the same values as the unpushed plan when the per-query
zonelessTimestampAsNTZ differs from the table's resolved value, in both
directions: a per-query false on a table resolved as TimestampNTZ, and a
per-query true on a catalog that sets zonelessTimestampAsNTZ to false. Cover a
MIN or MAX over a zoneless DATE or TIMESTAMP column and a GROUP BY key on one.
- **Compatibility:** A per-query timestampNTZAsWallClock still changes read
and write conversion for that operation, and case-distinct driver properties
still reach the connection properties unchanged.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTableCatalogSuite.scala:
##########
@@ -106,6 +108,73 @@ class JDBCTableCatalogSuite extends SharedSparkSession {
checkAnswer(sql("SHOW TABLES IN h2.bad_test"), Seq())
}
+ test("SPARK-60420: JDBC builders preserve timestamp defaults and honor query
overrides") {
+ val wallClockKey = JDBCOptions.JDBC_TIMESTAMP_NTZ_WALL_CLOCK
+ val zonelessKey = JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ
+ val legacyKey = SQLConf.LEGACY_ORACLE_TIMESTAMP_NTZ_MAPPING_ENABLED.key
+ val schema = new StructType().add("t", TimestampNTZType)
+ for {
+ legacy <- Seq(false, true)
+ catalogValue <- Seq(None, Some(false), Some(true))
+ catalogUpperCase <- Seq(false, true)
+ } {
+ val table = withSQLConf(legacyKey -> legacy.toString) {
+ val parameters = Map(
+ "url" -> "jdbc:oracle:thin:@//host:1521/db",
+ "dbtable" -> "t",
+ "driver" -> "org.h2.Driver") ++ catalogValue.toSeq.flatMap { value =>
+ Seq(wallClockKey -> value.toString, zonelessKey -> (!value).toString)
Review Comment:
**Nit (P3):** Here and on the query side (lines 137-141), one generated
value always produces both `timestampNTZAsWallClock` and
`zonelessTimestampAsNTZ`, so every case sets both keys or neither. Suppose a
later change to `mergeOptions` injected the load-time values only when no
timestamp key is supplied. Every case here would still pass. Yet a catalog that
sets only `zonelessTimestampAsNTZ`, the documented catalog usage, would
re-derive `timestampNTZAsWallClock` from the flipped flag in each builder,
which brings back the shifted NTZ read this PR fixes. The Oracle Docker tests
don't cover it either: the flag-flip V2 test sets no timestamp option, and the
per-query V2 test never flips the flag.
Consider generating the two keys independently on each side. Set only
`zonelessTimestampAsNTZ` or only `timestampNTZAsWallClock` on the catalog, and
only one of them per query. Then assert that the omitted key keeps the table's
load-time value in both builders.
**Verification:**
- **Regression:** A variant of the option merge that pins the load-time
values only when neither timestamp key is supplied fails the extended grid, in
both flag-flip directions and for both builders.
- **Behavior:** With only zonelessTimestampAsNTZ set on the catalog and the
legacy flag flipped after table resolution, both builders report the catalog's
zonelessTimestampAsNTZ and the load-time timestampNTZAsWallClock. With only
timestampNTZAsWallClock set per query, both builders report the query's
timestampNTZAsWallClock and the table's zonelessTimestampAsNTZ. With only
zonelessTimestampAsNTZ set per query, both builders report the table's
load-time timestampNTZAsWallClock.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]