urosstan-db commented on code in PR #59671:
URL: https://github.com/apache/spark/pull/59671#discussion_r4387183526
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTable.scala:
##########
@@ -60,13 +60,13 @@ case class JDBCTable(
override def newScanBuilder(options: CaseInsensitiveStringMap):
JDBCScanBuilder = {
val mergedOptions = new JDBCOptions(
- jdbcOptions.parameters.originalMap ++
options.asCaseSensitiveMap().asScala)
Review Comment:
Why do we need this change?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTableCatalog.scala:
##########
@@ -151,11 +153,28 @@ class JDBCTableCatalog extends TableCatalog
}
}
+ // The per-read value decides the resolved schema, so it must reach
loadTable.
+ override def tableStateOptionKeys(): util.Set[String] =
+ util.Set.of(JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ)
+
+ // Carries the state options down the default dispatch to loadTable(ident),
so subclass
+ // overrides of loadTable(ident) and loadTable(ident, writePrivileges) stay
in the path.
+ private val currentStateOptions =
Review Comment:
This means we need to use same instance of catalog for both loadTable
invocations?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala:
##########
@@ -352,13 +352,17 @@ object JdbcUtils extends Logging with SQLConfHelper {
metadata.putBoolean("isSigned", isSigned)
metadata.putBoolean("isTimestampNTZ", options.preferTimestampNTZ)
metadata.putBoolean("preferTimestampNanos", options.preferTimestampNanos)
+ metadata.putBoolean(
+ JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ,
options.zonelessTimestampAsNTZ)
metadata.putLong("scale", fieldScale)
metadata.putString("jdbcClientType", typeName)
dialect.updateExtraColumnMeta(conn, rsmd, i + 1, metadata)
val columnType =
dialect.getCatalystType(dataType, typeName, fieldSize,
metadata).getOrElse(
getCatalystType(dataType, typeName, fieldSize, fieldScale, isSigned,
options))
+ // Only an input to the dialect's type mapping, so keep it out of the
schema.
+ metadata.remove(JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ)
Review Comment:
It is fine to keep it, since other options are kept as well?
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTableCatalog.scala:
##########
@@ -151,11 +153,28 @@ class JDBCTableCatalog extends TableCatalog
}
}
+ // The per-read value decides the resolved schema, so it must reach
loadTable.
+ override def tableStateOptionKeys(): util.Set[String] =
+ util.Set.of(JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ)
Review Comment:
**B1 (blocker): forward the timestamp preference during table loading.**
With the default Oracle legacy flag (`false`), no catalog-level timestamp
preference, and `spark.sql.timestampType=TIMESTAMP_LTZ`, this combination
resolves `TS` as `TimestampType` instead of the documented `TimestampNTZType`
fallback:
```sql
SELECT TS FROM oracle_catalog.schema_name.T WITH (
'zonelessTimestampAsNTZ' = 'false',
'preferTimestampNTZ' = 'true')
```
`CatalogV2Util` filters the load options to these declared keys, so
`preferTimestampNTZ=true` is lost during schema discovery. `newScanBuilder`
receives it later but keeps the already-resolved schema. The older legacy-mode
limitation predates this PR; the new opt-out exposes it with the legacy flag
disabled.
Could we include the preference in the table-state keys and add a V2 schema
assertion for this combination?
```suggestion
override def tableStateOptionKeys(): util.Set[String] =
util.Set.of(
JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ,
JDBCOptions.JDBC_PREFER_TIMESTAMP_NTZ)
```
##########
connector/docker-integration-tests/src/test/scala/org/apache/spark/sql/jdbc/OracleIntegrationSuite.scala:
##########
@@ -629,6 +636,19 @@ class OracleIntegrationSuite extends
SharedJDBCIntegrationSuite
val legacyRow = legacyDf.select("D", "T").collect().head
assert(legacyRow.getTimestamp(0) === Timestamp.valueOf("1991-11-09
00:00:00"))
assert(legacyRow.getTimestamp(1) === Timestamp.valueOf("1996-01-01
01:23:45"))
+
+ withDefaultTimeZone(LA) {
+ val optInDf = readDatetime(
+ Map("zonelessTimestampAsNTZ" -> "true", "timestampNTZAsWallClock" ->
"true"))
+ assert(optInDf.schema("T").dataType === TimestampNTZType)
Review Comment:
**S1: cover the independent read-conversion opt-out.**
The new read checks enable NTZ mapping and wall-clock conversion together.
The mapping opt-out produces `TimestampType`, so it bypasses the NTZ getter;
the explicit false conversion tests exercise writes.
Could we add one read with `timestampNTZAsWallClock=false` while retaining
NTZ mapping? Insert this before the enclosing test enables the legacy flag,
reusing the `datetime` fixture:
```scala
withDefaultTimeZone(LA) {
val baseRead = readDatetime(Map("timestampNTZAsWallClock" -> "false"))
assert(baseRead.schema("T").dataType === TimestampNTZType)
checkAnswer(baseRead.select("T"), Row(LocalDateTime.of(1996, 1, 1, 9, 23,
45)))
}
```
The fixture stores `1996-01-01 01:23:45`; LA is UTC-08:00 on that date, so
the legacy conversion yields `09:23:45`. This checks the independent read
contract without duplicating B1.
##########
docs/sql-data-sources-jdbc.md:
##########
@@ -382,6 +382,22 @@ logging into the data sources.
</td>
<td>read</td>
</tr>
+ <tr>
+ <td><code>zonelessTimestampAsNTZ</code></td>
+ <td><code>true</code> for Oracle, <code>false</code> for other
dialects</td>
+ <td>
+ When the option is set to <code>true</code>, timestamp types without a
time zone are inferred as Spark's TimestampNTZ type regardless of
<code>preferTimestampNTZ</code>. Otherwise, they follow
<code>preferTimestampNTZ</code>. This option is currently only supported by
Oracle. Its default follows the dialect's legacy TimestampNTZ mapping
configuration: it is <code>false</code> when that configuration is enabled.
Depending on the driver, values read as TimestampNTZ may not keep their
wall-clock time unless <code>timestampNTZAsWallClock</code> is also
<code>true</code>. On writes through a JDBC catalog, it also decides the types
of the target table's columns, and so how the written values are cast. For
tables created with <code>CREATE TABLE ... USING jdbc</code>, the schema is
fixed when the table is created, so set this option in the table's
<code>OPTIONS</code>.
+ </td>
+ <td>read/write</td>
+ </tr>
+ <tr>
+ <td><code>timestampNTZAsWallClock</code></td>
+ <td><code>true</code> for Oracle, <code>false</code> for other
dialects</td>
+ <td>
+ When the option is set to <code>true</code>, TimestampNTZ values are
read and written as <code>java.time.LocalDateTime</code>, preserving the
wall-clock value without a time zone conversion. This requires a JDBC driver
that supports <code>java.time.LocalDateTime</code>. Otherwise, they are
converted through <code>java.sql.Timestamp</code> in the JVM default time zone.
Its default follows the dialect's legacy TimestampNTZ mapping configuration: it
is <code>false</code> when that configuration is enabled (only Oracle has one,
so the default is <code>true</code> for Oracle and <code>false</code> for other
dialects).
Review Comment:
**N1 (optional): name the legacy configuration directly.**
The final sentence repeats the adjacent default column. Naming the
configuration preserves the useful exception more directly:
```suggestion
When the option is set to <code>true</code>, TimestampNTZ values are
read and written as <code>java.time.LocalDateTime</code>, preserving the
wall-clock value without a time zone conversion. This requires a JDBC driver
that supports <code>java.time.LocalDateTime</code>. Otherwise, they are
converted through <code>java.sql.Timestamp</code> in the JVM default time zone.
For Oracle, enabling
<code>spark.sql.legacy.oracle.timestampNTZMapping.enabled</code> changes the
default to <code>false</code>.
```
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/jdbc/JDBCTableCatalog.scala:
##########
@@ -151,11 +153,28 @@ class JDBCTableCatalog extends TableCatalog
}
}
+ // The per-read value decides the resolved schema, so it must reach
loadTable.
+ override def tableStateOptionKeys(): util.Set[String] =
+ util.Set.of(JDBCOptions.JDBC_ZONELESS_TIMESTAMP_AS_NTZ)
+
+ // Carries the state options down the default dispatch to loadTable(ident),
so subclass
+ // overrides of loadTable(ident) and loadTable(ident, writePrivileges) stay
in the path.
+ private val currentStateOptions =
+ new DynamicVariable[collection.Map[String, String]](Map.empty)
Review Comment:
**Optional: group the scoped state with the other catalog fields.**
Could we move this declaration and its comment beside the fields at the top
of the class? That keeps the `tableStateOptionKeys` and `loadTable` overrides
together:
```scala
private var dialect: JdbcDialect = _
private var functions: Map[String, UnboundFunction] = _
private val currentStateOptions =
new DynamicVariable[collection.Map[String, String]](Map.empty)
```
--
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]