This is an automated email from the ASF dual-hosted git repository.
cloud-fan 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 589e0078af2f [SPARK-57855][SQL] Refactor JDBC value getters into a
sealed type for readability
589e0078af2f is described below
commit 589e0078af2fcd6bd99c795e43483e2877c21e18
Author: Abdelrahman Gamal <[email protected]>
AuthorDate: Thu Jul 2 20:53:34 2026 +0800
[SPARK-57855][SQL] Refactor JDBC value getters into a sealed type for
readability
### What changes were proposed in this pull request?
`JdbcUtils.makeGetter` builds a per-column reader (`JDBCValueGetter`) that
reads a column from a `ResultSet`, converts it to Spark's internal
representation, and stores it into an `InternalRow`. `JDBCValueGetter` was a
private type alias for `(ResultSet, InternalRow, Int) => Unit`, so `makeGetter`
returned an anonymous closure per column.
This PR makes the getter an explicit type:
- Adds `JDBCValueGetter.scala` with a `private[jdbc]` sealed trait
`JDBCValueGetter` (single `apply(rs, row, pos)` method) and one named case per
getter in its companion object — `case object` for stateless getters, `final
case class` for parameterized ones — plus the getter-only helpers
`nullSafeConvert` / `arrayConverter`.
- `JdbcUtils.makeGetter` becomes a straightforward `DataType` -> getter
selection.
Class hierarchy (sealed):
```
JDBCValueGetter
├─ BooleanGetter, DoubleGetter, FloatGetter, IntGetter, LongGetter,
ShortGetter,
│ ByteGetter, StringGetter, RowIdGetter, BytesGetter, BinaryLongGetter,
│ BinaryBitGetter, TimeGetter, LogicalTimeGetter, PostgresBitArrayGetter,
NullGetter (case object)
└─ DateGetter, DecimalGetter, TimestampGetter, LogicalTimeNTZGetter,
TimestampNTZGetter,
YearMonthIntervalGetter, DayTimeIntervalGetter, ArrayGetter
(final case class)
```
Each getter's body is transcribed unchanged from the corresponding `match`
arm, and the selection order and guards in `makeGetter` are unchanged.
### Why are the changes needed?
Because `JDBCValueGetter` was only a function alias, the getter chosen for
a column was not expressed in the type system: `makeGetter` read as a large
`match` returning indistinguishable closures, and an individual getter could
not be named or referred to. Turning it into a sealed trait with one named case
per getter makes the set of getters explicit and self-documenting, and reduces
`makeGetter` to a simple selection over the column's `DataType`, separating
"which getter to use" from [...]
### Does this PR introduce _any_ user-facing change?
No. This is an internal, behavior-preserving refactor of private
(`private[jdbc]`) symbols; the per-column read/convert/store logic is
unchanged, and there is no public API or binary-compatibility (MiMa) impact.
### How was this patch tested?
Existing JDBC test suites (`JDBCSuite`, `JDBCV2Suite`, `JDBCWriteSuite`,
`JdbcUtilsSuite`, etc.). This is a behavior-preserving refactor with no new
behavior, so no new tests were added — the getter logic is a direct
transcription of the previous `match` arms.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code
Closes #56936 from Gamal72/jdbc-value-getter-sealed-type.
Authored-by: Abdelrahman Gamal <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
---
.../datasources/jdbc/JDBCValueGetter.scala | 305 +++++++++++++++++++++
.../sql/execution/datasources/jdbc/JdbcUtils.scala | 263 ++----------------
2 files changed, 331 insertions(+), 237 deletions(-)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCValueGetter.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCValueGetter.scala
new file mode 100644
index 000000000000..bd8d04760da6
--- /dev/null
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCValueGetter.scala
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.execution.datasources.jdbc
+
+import java.math.{BigDecimal => JBigDecimal}
+import java.nio.charset.StandardCharsets
+import java.sql.{Date, ResultSet, Time, Timestamp}
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.util.DateTimeConstants.MICROS_PER_MILLIS
+import org.apache.spark.sql.catalyst.util.DateTimeUtils._
+import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.errors.QueryExecutionErrors
+import org.apache.spark.sql.jdbc.JdbcDialect
+import org.apache.spark.sql.types._
+import org.apache.spark.unsafe.types.UTF8String
+
+/**
+ * A `JDBCValueGetter` is responsible for getting a value from a `ResultSet`
into a field of an
+ * `InternalRow`. `pos` is the index for the value to be set in the row, and
is also used for the
+ * value in the `ResultSet`. `JdbcUtils.makeGetter` selects one getter per
column.
+ */
+private[jdbc] sealed trait JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit
+}
+
+private[jdbc] object JDBCValueGetter {
+ case object BooleanGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setBoolean(pos, rs.getBoolean(pos + 1))
+ }
+
+ final case class DateGetter(dialect: JdbcDialect) extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ // DateTimeUtils.fromJavaDate does not handle null value, so we need to
check it.
+ val dateVal = rs.getDate(pos + 1)
+ if (dateVal != null) {
+ row.setInt(pos, fromJavaDate(dialect.convertJavaDateToDate(dateVal)))
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ case object TimeGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val localTime = rs.getObject(pos + 1, classOf[java.time.LocalTime])
+ if (localTime != null) {
+ row.setLong(pos, localTime.toNanoOfDay)
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ // When connecting with Oracle DB through JDBC, the precision and scale of
BigDecimal
+ // object returned by ResultSet.getBigDecimal is not correctly matched to
the table
+ // schema reported by ResultSetMetaData.getPrecision and
ResultSetMetaData.getScale.
+ // If inserting values like 19999 into a column with NUMBER(12, 2) type, you
get through
+ // a BigDecimal object with scale as 0. But the dataframe schema has correct
type as
+ // DecimalType(12, 2). Thus, after saving the dataframe into parquet file
and then
+ // retrieve it, you will get wrong result 199.99.
+ // So it is needed to set precision and scale for Decimal based on JDBC
metadata.
+ final case class DecimalGetter(precision: Int, scale: Int) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val decimal =
+ nullSafeConvert[JBigDecimal](rs.getBigDecimal(pos + 1), d =>
Decimal(d, precision, scale))
+ row.update(pos, decimal)
+ }
+ }
+
+ case object DoubleGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setDouble(pos, rs.getDouble(pos + 1))
+ }
+
+ case object FloatGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setFloat(pos, rs.getFloat(pos + 1))
+ }
+
+ case object IntGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setInt(pos, rs.getInt(pos + 1))
+ }
+
+ case object BinaryLongGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val l = nullSafeConvert[Array[Byte]](rs.getBytes(pos + 1), bytes => {
+ var ans = 0L
+ var j = 0
+ while (j < bytes.length) {
+ ans = 256 * ans + (255 & bytes(j))
+ j = j + 1
+ }
+ ans
+ })
+ row.update(pos, l)
+ }
+ }
+
+ case object LongGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setLong(pos, rs.getLong(pos + 1))
+ }
+
+ case object ShortGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setShort(pos, rs.getShort(pos + 1))
+ }
+
+ case object ByteGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.setByte(pos, rs.getByte(pos + 1))
+ }
+
+ case object RowIdGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val rawRowId = rs.getRowId(pos + 1)
+ if (rawRowId == null) {
+ row.update(pos, null)
+ } else {
+ row.update(pos, UTF8String.fromString(rawRowId.toString))
+ }
+ }
+ }
+
+ case object StringGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ // TODO(davies): use getBytes for better performance, if the encoding is
UTF-8
+ row.update(pos, UTF8String.fromString(rs.getString(pos + 1)))
+ }
+
+ // SPARK-34357 - sql TIME type represents as zero epoch timestamp.
+ // It is mapped as Spark TimestampType but fixed at 1970-01-01 for day,
+ // time portion is time of day, with no reference to a particular calendar,
+ // time zone or date, with a precision till microseconds.
+ // It stores the number of milliseconds after midnight, 00:00:00.000000
+ case object LogicalTimeGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.update(pos, nullSafeConvert[Time](
+ rs.getTime(pos + 1), t => Math.multiplyExact(t.getTime,
MICROS_PER_MILLIS)))
+ }
+
+ final case class TimestampGetter(dialect: JdbcDialect) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val t = rs.getTimestamp(pos + 1)
+ if (t != null) {
+ row.setLong(pos,
fromJavaTimestamp(dialect.convertJavaTimestampToTimestamp(t)))
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ final case class LogicalTimeNTZGetter(dialect: JdbcDialect) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val micros = nullSafeConvert[Time](rs.getTime(pos + 1), t => {
+ val time = dialect.convertJavaTimestampToTimestampNTZ(new
Timestamp(t.getTime))
+ localDateTimeToMicros(time)
+ })
+ row.update(pos, micros)
+ }
+ }
+
+ final case class TimestampNTZGetter(dialect: JdbcDialect) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val t = rs.getTimestamp(pos + 1)
+ if (t != null) {
+ row.setLong(pos,
localDateTimeToMicros(dialect.convertJavaTimestampToTimestampNTZ(t)))
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ case object BinaryBitGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val bytes = rs.getBytes(pos + 1)
+ if (bytes != null) {
+ val binary =
bytes.flatMap(Integer.toBinaryString(_).getBytes(StandardCharsets.US_ASCII))
+ row.update(pos, binary)
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ case object BytesGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.update(pos, rs.getBytes(pos + 1))
+ }
+
+ final case class YearMonthIntervalGetter(dialect: JdbcDialect) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.update(pos,
+ nullSafeConvert(rs.getString(pos + 1),
dialect.getYearMonthIntervalAsMonths))
+ }
+
+ final case class DayTimeIntervalGetter(dialect: JdbcDialect) extends
JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ row.update(pos,
+ nullSafeConvert(rs.getString(pos + 1),
dialect.getDayTimeIntervalAsMicros))
+ }
+
+ // SPARK-47628: Handle PostgreSQL bit(n>1) array type ahead. As in the
pgjdbc driver,
+ // bit(n>1)[] is not distinguishable from bit(1)[], and they are all
recognized as boolean[].
+ // This is wrong for bit(n>1)[], so we need to handle it first as byte array.
+ case object PostgresBitArrayGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit = {
+ val fieldString = rs.getString(pos + 1)
+ if (fieldString != null) {
+ val strArray = fieldString.substring(1, fieldString.length -
1).split(",")
+ // Charset is picked from the pgjdbc driver for consistency.
+ val bytesArray = strArray.map(_.getBytes(StandardCharsets.US_ASCII))
+ row.update(pos, new GenericArrayData(bytesArray))
+ } else {
+ row.update(pos, null)
+ }
+ }
+ }
+
+ final case class ArrayGetter(arrayType: ArrayType, dialect: JdbcDialect,
metadata: Metadata)
+ extends JDBCValueGetter {
+ private def elementConversion(et: DataType): AnyRef => Any = et match {
+ case TimestampType => arrayConverter[Timestamp] {
+ (t: Timestamp) =>
fromJavaTimestamp(dialect.convertJavaTimestampToTimestamp(t))
+ }
+
+ case TimestampNTZType =>
+ arrayConverter[Timestamp] {
+ (t: Timestamp) =>
localDateTimeToMicros(dialect.convertJavaTimestampToTimestampNTZ(t))
+ }
+
+ case StringType =>
+ arrayConverter[Object]((obj: Object) =>
UTF8String.fromString(obj.toString))
+
+ case DateType => arrayConverter[Date] {
+ (d: Date) => fromJavaDate(dialect.convertJavaDateToDate(d))
+ }
+
+ case dt: DecimalType =>
+ arrayConverter[java.math.BigDecimal](d => Decimal(d, dt.precision,
dt.scale))
+
+ case LongType if metadata.contains("binarylong") =>
+ throw
QueryExecutionErrors.unsupportedArrayElementTypeBasedOnBinaryError(arrayType)
+
+ case ArrayType(et0, _) =>
+ arrayConverter[Array[Any]] {
+ arr => new GenericArrayData(elementConversion(et0)(arr))
+ }
+
+ case IntegerType => arrayConverter[Int]((i: Int) => i)
+ case FloatType => arrayConverter[Float]((f: Float) => f)
+ case DoubleType => arrayConverter[Double]((d: Double) => d)
+ case ShortType => arrayConverter[Short]((s: Short) => s)
+ case BooleanType => arrayConverter[Boolean]((b: Boolean) => b)
+ case LongType => arrayConverter[Long]((l: Long) => l)
+
+ case _ => (array: Object) => array.asInstanceOf[Array[Any]]
+ }
+
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
+ try {
+ val array = nullSafeConvert[java.sql.Array](
+ input = rs.getArray(pos + 1),
+ arr => new
GenericArrayData(elementConversion(arrayType.elementType)(arr.getArray())))
+ row.update(pos, array)
+ } catch {
+ case _: java.lang.ClassCastException =>
+ throw QueryExecutionErrors.wrongDatatypeInSomeRows(pos, arrayType)
+ }
+ }
+
+ case object NullGetter extends JDBCValueGetter {
+ def apply(rs: ResultSet, row: InternalRow, pos: Int): Unit =
row.update(pos, null)
+ }
+
+ private def nullSafeConvert[T](input: T, f: T => Any): Any = {
+ if (input == null) {
+ null
+ } else {
+ f(input)
+ }
+ }
+
+ private def arrayConverter[T](elementConvert: T => Any): Any => Any =
(array: Any) => {
+ array.asInstanceOf[Array[T]].map(e => nullSafeConvert(e, elementConvert))
+ }
+}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala
index cebc45002d44..ca44e8b710b1 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JdbcUtils.scala
@@ -17,9 +17,7 @@
package org.apache.spark.sql.execution.datasources.jdbc
-import java.math.{BigDecimal => JBigDecimal}
-import java.nio.charset.StandardCharsets
-import java.sql.{Connection, Date, JDBCType, PreparedStatement, ResultSet,
ResultSetMetaData, SQLException, Time, Timestamp}
+import java.sql.{Connection, JDBCType, PreparedStatement, ResultSet,
ResultSetMetaData, SQLException}
import java.time.{Instant, LocalDate}
import java.util
@@ -39,8 +37,7 @@ import
org.apache.spark.sql.catalyst.analysis.{DecimalPrecisionTypeCoercion, Res
import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder
import org.apache.spark.sql.catalyst.expressions.SpecificInternalRow
import org.apache.spark.sql.catalyst.parser.CatalystSqlParser
-import org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap,
CharVarcharUtils, GenericArrayData}
-import org.apache.spark.sql.catalyst.util.DateTimeConstants.MICROS_PER_MILLIS
+import org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap,
CharVarcharUtils}
import org.apache.spark.sql.catalyst.util.DateTimeUtils._
import org.apache.spark.sql.connector.catalog.{Identifier, TableChange}
import org.apache.spark.sql.connector.catalog.index.{SupportsIndex, TableIndex}
@@ -50,7 +47,6 @@ import org.apache.spark.sql.execution.metric.{SQLMetric,
SQLMetrics}
import org.apache.spark.sql.jdbc.{JdbcDialect, JdbcDialects, JdbcType,
NoopDialect}
import org.apache.spark.sql.types._
import org.apache.spark.sql.util.SchemaUtils
-import org.apache.spark.unsafe.types.UTF8String
import org.apache.spark.util.{NextIterator, TaskInterruptListener}
import org.apache.spark.util.ArrayImplicits._
@@ -418,11 +414,6 @@ object JdbcUtils extends Logging with SQLConfHelper {
)
}
- // A `JDBCValueGetter` is responsible for getting a value from `ResultSet`
into a field
- // for `MutableRow`. The last argument `Int` means the index for the value
to be set in
- // the row and also used for the value in `ResultSet`.
- private type JDBCValueGetter = (ResultSet, InternalRow, Int) => Unit
-
/**
* Creates `JDBCValueGetter`s according to [[StructType]], which can set
* each value from `ResultSet` to each field of [[InternalRow]] correctly.
@@ -438,238 +429,36 @@ object JdbcUtils extends Logging with SQLConfHelper {
dt: DataType,
dialect: JdbcDialect,
metadata: Metadata): JDBCValueGetter = dt match {
- case BooleanType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setBoolean(pos, rs.getBoolean(pos + 1))
-
- case DateType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- // DateTimeUtils.fromJavaDate does not handle null value, so we need
to check it.
- val dateVal = rs.getDate(pos + 1)
- if (dateVal != null) {
- row.setInt(pos, fromJavaDate(dialect.convertJavaDateToDate(dateVal)))
- } else {
- row.update(pos, null)
- }
-
- case _: TimeType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val localTime = rs.getObject(pos + 1, classOf[java.time.LocalTime])
- if (localTime != null) {
- row.setLong(pos, localTime.toNanoOfDay)
- } else {
- row.update(pos, null)
- }
-
- // When connecting with Oracle DB through JDBC, the precision and scale of
BigDecimal
- // object returned by ResultSet.getBigDecimal is not correctly matched to
the table
- // schema reported by ResultSetMetaData.getPrecision and
ResultSetMetaData.getScale.
- // If inserting values like 19999 into a column with NUMBER(12, 2) type,
you get through
- // a BigDecimal object with scale as 0. But the dataframe schema has
correct type as
- // DecimalType(12, 2). Thus, after saving the dataframe into parquet file
and then
- // retrieve it, you will get wrong result 199.99.
- // So it is needed to set precision and scale for Decimal based on JDBC
metadata.
- case DecimalType.Fixed(p, s) =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val decimal =
- nullSafeConvert[JBigDecimal](rs.getBigDecimal(pos + 1), d =>
Decimal(d, p, s))
- row.update(pos, decimal)
-
- case DoubleType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setDouble(pos, rs.getDouble(pos + 1))
-
- case FloatType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setFloat(pos, rs.getFloat(pos + 1))
-
- case IntegerType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setInt(pos, rs.getInt(pos + 1))
-
- case LongType if metadata.contains("binarylong") =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val l = nullSafeConvert[Array[Byte]](rs.getBytes(pos + 1), bytes => {
- var ans = 0L
- var j = 0
- while (j < bytes.length) {
- ans = 256 * ans + (255 & bytes(j))
- j = j + 1
- }
- ans
- })
- row.update(pos, l)
-
- case LongType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setLong(pos, rs.getLong(pos + 1))
-
- case ShortType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setShort(pos, rs.getShort(pos + 1))
-
- case ByteType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.setByte(pos, rs.getByte(pos + 1))
-
- case StringType if metadata.contains("rowid") =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val rawRowId = rs.getRowId(pos + 1)
- if (rawRowId == null) {
- row.update(pos, null)
- } else {
- row.update(pos, UTF8String.fromString(rawRowId.toString))
- }
-
- case StringType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- // TODO(davies): use getBytes for better performance, if the encoding
is UTF-8
- row.update(pos, UTF8String.fromString(rs.getString(pos + 1)))
-
- // SPARK-34357 - sql TIME type represents as zero epoch timestamp.
- // It is mapped as Spark TimestampType but fixed at 1970-01-01 for day,
- // time portion is time of day, with no reference to a particular calendar,
- // time zone or date, with a precision till microseconds.
- // It stores the number of milliseconds after midnight, 00:00:00.000000
+ case BooleanType => JDBCValueGetter.BooleanGetter
+ case DateType => JDBCValueGetter.DateGetter(dialect)
+ case _: TimeType => JDBCValueGetter.TimeGetter
+ case DecimalType.Fixed(p, s) => JDBCValueGetter.DecimalGetter(p, s)
+ case DoubleType => JDBCValueGetter.DoubleGetter
+ case FloatType => JDBCValueGetter.FloatGetter
+ case IntegerType => JDBCValueGetter.IntGetter
+ case LongType if metadata.contains("binarylong") =>
JDBCValueGetter.BinaryLongGetter
+ case LongType => JDBCValueGetter.LongGetter
+ case ShortType => JDBCValueGetter.ShortGetter
+ case ByteType => JDBCValueGetter.ByteGetter
+ case StringType if metadata.contains("rowid") =>
JDBCValueGetter.RowIdGetter
+ case StringType => JDBCValueGetter.StringGetter
case TimestampType if metadata.contains("logical_time_type") =>
- (rs: ResultSet, row: InternalRow, pos: Int) => {
- row.update(pos, nullSafeConvert[Time](
- rs.getTime(pos + 1), t => Math.multiplyExact(t.getTime,
MICROS_PER_MILLIS)))
- }
-
- case TimestampType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val t = rs.getTimestamp(pos + 1)
- if (t != null) {
- row.setLong(pos,
fromJavaTimestamp(dialect.convertJavaTimestampToTimestamp(t)))
- } else {
- row.update(pos, null)
- }
-
+ JDBCValueGetter.LogicalTimeGetter
+ case TimestampType => JDBCValueGetter.TimestampGetter(dialect)
case TimestampNTZType if metadata.contains("logical_time_type") =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val micros = nullSafeConvert[Time](rs.getTime(pos + 1), t => {
- val time = dialect.convertJavaTimestampToTimestampNTZ(new
Timestamp(t.getTime))
- localDateTimeToMicros(time)
- })
- row.update(pos, micros)
-
- case TimestampNTZType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val t = rs.getTimestamp(pos + 1)
- if (t != null) {
- row.setLong(pos,
localDateTimeToMicros(dialect.convertJavaTimestampToTimestampNTZ(t)))
- } else {
- row.update(pos, null)
- }
-
- case BinaryType if metadata.contains("binarylong") =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val bytes = rs.getBytes(pos + 1)
- if (bytes != null) {
- val binary =
bytes.flatMap(Integer.toBinaryString(_).getBytes(StandardCharsets.US_ASCII))
- row.update(pos, binary)
- } else {
- row.update(pos, null)
- }
-
- case BinaryType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.update(pos, rs.getBytes(pos + 1))
-
- case _: YearMonthIntervalType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.update(pos,
- nullSafeConvert(rs.getString(pos + 1),
dialect.getYearMonthIntervalAsMonths))
-
- case _: DayTimeIntervalType =>
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- row.update(pos,
- nullSafeConvert(rs.getString(pos + 1),
dialect.getDayTimeIntervalAsMicros))
-
+ JDBCValueGetter.LogicalTimeNTZGetter(dialect)
+ case TimestampNTZType => JDBCValueGetter.TimestampNTZGetter(dialect)
+ case BinaryType if metadata.contains("binarylong") =>
JDBCValueGetter.BinaryBitGetter
+ case BinaryType => JDBCValueGetter.BytesGetter
+ case _: YearMonthIntervalType =>
JDBCValueGetter.YearMonthIntervalGetter(dialect)
+ case _: DayTimeIntervalType =>
JDBCValueGetter.DayTimeIntervalGetter(dialect)
case _: ArrayType if metadata.contains("pg_bit_array_type") =>
- // SPARK-47628: Handle PostgreSQL bit(n>1) array type ahead. As in the
pgjdbc driver,
- // bit(n>1)[] is not distinguishable from bit(1)[], and they are all
recognized as boolen[].
- // This is wrong for bit(n>1)[], so we need to handle it first as byte
array.
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- val fieldString = rs.getString(pos + 1)
- if (fieldString != null) {
- val strArray = fieldString.substring(1, fieldString.length -
1).split(",")
- // Charset is picked from the pgjdbc driver for consistency.
- val bytesArray = strArray.map(_.getBytes(StandardCharsets.US_ASCII))
- row.update(pos, new GenericArrayData(bytesArray))
- } else {
- row.update(pos, null)
- }
-
- case ArrayType(et, _) =>
- def elementConversion(et: DataType): AnyRef => Any = et match {
- case TimestampType => arrayConverter[Timestamp] {
- (t: Timestamp) =>
fromJavaTimestamp(dialect.convertJavaTimestampToTimestamp(t))
- }
-
- case TimestampNTZType =>
- arrayConverter[Timestamp] {
- (t: Timestamp) =>
localDateTimeToMicros(dialect.convertJavaTimestampToTimestampNTZ(t))
- }
-
- case StringType =>
- arrayConverter[Object]((obj: Object) =>
UTF8String.fromString(obj.toString))
-
- case DateType => arrayConverter[Date] {
- (d: Date) => fromJavaDate(dialect.convertJavaDateToDate(d))
- }
-
- case dt: DecimalType =>
- arrayConverter[java.math.BigDecimal](d => Decimal(d, dt.precision,
dt.scale))
-
- case LongType if metadata.contains("binarylong") =>
- throw
QueryExecutionErrors.unsupportedArrayElementTypeBasedOnBinaryError(dt)
-
- case ArrayType(et0, _) =>
- arrayConverter[Array[Any]] {
- arr => new GenericArrayData(elementConversion(et0)(arr))
- }
-
- case IntegerType => arrayConverter[Int]((i: Int) => i)
- case FloatType => arrayConverter[Float]((f: Float) => f)
- case DoubleType => arrayConverter[Double]((d: Double) => d)
- case ShortType => arrayConverter[Short]((s: Short) => s)
- case BooleanType => arrayConverter[Boolean]((b: Boolean) => b)
- case LongType => arrayConverter[Long]((l: Long) => l)
-
- case _ => (array: Object) => array.asInstanceOf[Array[Any]]
- }
-
- (rs: ResultSet, row: InternalRow, pos: Int) =>
- try {
- val array = nullSafeConvert[java.sql.Array](
- input = rs.getArray(pos + 1),
- array => new
GenericArrayData(elementConversion(et)(array.getArray())))
- row.update(pos, array)
- } catch {
- case e: java.lang.ClassCastException =>
- throw QueryExecutionErrors.wrongDatatypeInSomeRows(pos, dt)
- }
-
- case NullType =>
- (_: ResultSet, row: InternalRow, pos: Int) => row.update(pos, null)
-
+ JDBCValueGetter.PostgresBitArrayGetter
+ case at: ArrayType => JDBCValueGetter.ArrayGetter(at, dialect, metadata)
+ case NullType => JDBCValueGetter.NullGetter
case _ => throw
QueryExecutionErrors.unsupportedJdbcTypeError(dt.catalogString)
}
- private def nullSafeConvert[T](input: T, f: T => Any): Any = {
- if (input == null) {
- null
- } else {
- f(input)
- }
- }
-
- private def arrayConverter[T](elementConvert: T => Any): Any => Any =
(array: Any) => {
- array.asInstanceOf[Array[T]].map(e => nullSafeConvert(e, elementConvert))
- }
-
// A `JDBCValueSetter` is responsible for setting a value from `Row` into a
field for
// `PreparedStatement`. The last argument `Int` means the index for the
value to be set
// in the SQL statement and also used for the value in `Row`.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]