This is an automated email from the ASF dual-hosted git repository.
pan3793 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 f97481501e56 [SPARK-57380][CONNECT] Support complex types (ARRAY, MAP,
STRUCT) in the Connect JDBC driver
f97481501e56 is described below
commit f97481501e5642817729d792ba6667936d135175
Author: Jiwon Park <[email protected]>
AuthorDate: Mon Jun 22 11:34:01 2026 +0800
[SPARK-57380][CONNECT] Support complex types (ARRAY, MAP, STRUCT) in the
Connect JDBC driver
### What changes were proposed in this pull request?
Support complex types (ARRAY, MAP, STRUCT) in the Spark Connect JDBC
driver, in both metadata (`SparkConnectResultSetMetaData`,
`SparkConnectDatabaseMetaData.getColumns`) and values
(`SparkConnectResultSet`). These previously threw
`SQLFeatureNotSupportedException`.
| | ARRAY | MAP | STRUCT |
|---|---|---|---|
| `getColumnType` | `Types.ARRAY` | `Types.JAVA_OBJECT` | `Types.STRUCT` |
| `getColumnClassName` / `getObject` | `java.sql.Array` | `java.util.Map` |
`java.sql.Struct` |
| `getPrecision` / `getScale` | 0 | 0 | 0 |
| `getColumnDisplaySize` / `getColumns` COLUMN_SIZE | `Int.MaxValue` |
`Int.MaxValue` | `Int.MaxValue` |
`getObject` returns standard JDBC types (nested types converted
recursively); JDBC has no Map type, so MAP returns `java.util.Map`. `getString`
and the complex-type `toString` render JSON (`Row.json` format). MAP uses
`JAVA_OBJECT` since there is no `Types.MAP`; precision/scale 0 follow the JDBC
javadoc for not-applicable types.
### Why are the changes needed?
JDBC tools read `ResultSetMetaData` before rendering rows and fetch complex
columns via `getObject`/`getArray`, so without this a complex-typed column
breaks both query rendering and `getColumns` schema introspection (DataGrip,
DBeaver). Returning standard `java.sql.Array` / `java.sql.Struct` lets tools
render structs as nested fields and arrays in dedicated viewers.
### Does this PR introduce _any_ user-facing change?
Yes, in the unreleased master only: the methods above now return values
instead of throwing.
### How was this patch tested?
New/updated tests in `SparkConnectJdbcDataTypeSuite` and
`SparkConnectDatabaseMetaDataSuite`.
build/sbt 'connect-client-jdbc/testOnly *SparkConnectJdbcDataTypeSuite
*SparkConnectDatabaseMetaDataSuite'
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)
Closes #56447 from j1wonpark/SPARK-57380.
Authored-by: Jiwon Park <[email protected]>
Signed-off-by: Cheng Pan <[email protected]>
---
.../connect/client/jdbc/SparkConnectArray.scala | 66 ++++++++
.../sql/connect/client/jdbc/SparkConnectMap.scala | 41 +++++
.../client/jdbc/SparkConnectResultSet.scala | 28 +++-
.../connect/client/jdbc/SparkConnectStruct.scala | 47 ++++++
.../connect/client/jdbc/util/JdbcTypeUtils.scala | 58 ++++++-
.../jdbc/SparkConnectDatabaseMetaDataSuite.scala | 8 +-
.../jdbc/SparkConnectJdbcDataTypeSuite.scala | 186 ++++++++++++++++++++-
7 files changed, 421 insertions(+), 13 deletions(-)
diff --git
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectArray.scala
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectArray.scala
new file mode 100644
index 000000000000..1e2600f01ca2
--- /dev/null
+++
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectArray.scala
@@ -0,0 +1,66 @@
+/*
+ * 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.connect.client.jdbc
+
+import java.sql.{Array => JdbcArray, ResultSet,
SQLFeatureNotSupportedException}
+import java.util
+
+import org.apache.spark.sql.connect.client.jdbc.util.JdbcTypeUtils
+import org.apache.spark.sql.types.{ArrayType, DataType}
+
+/**
+ * A [[java.sql.Array]] backed by an ARRAY value materialized by the Spark
Connect client.
+ * Elements are converted to standard JDBC objects on access (nested
ARRAY/MAP/STRUCT become
+ * [[java.sql.Array]] / [[java.util.Map]] / [[java.sql.Struct]]).
+ */
+private[jdbc] class SparkConnectArray(
+ elements: scala.collection.Seq[Any],
+ elementType: DataType) extends JdbcArray {
+
+ override def getBaseTypeName: String = elementType.sql
+
+ override def getBaseType: Int = JdbcTypeUtils.getColumnType(elementType)
+
+ override def getArray: AnyRef =
+ elements.map(JdbcTypeUtils.toJdbcObject(_, elementType)).toArray
+
+ override def getArray(map: util.Map[String, Class[_]]): AnyRef =
+ throw new SQLFeatureNotSupportedException
+
+ override def getArray(index: Long, count: Int): AnyRef =
+ throw new SQLFeatureNotSupportedException
+
+ override def getArray(index: Long, count: Int, map: util.Map[String,
Class[_]]): AnyRef =
+ throw new SQLFeatureNotSupportedException
+
+ override def getResultSet: ResultSet =
+ throw new SQLFeatureNotSupportedException
+
+ override def getResultSet(map: util.Map[String, Class[_]]): ResultSet =
+ throw new SQLFeatureNotSupportedException
+
+ override def getResultSet(index: Long, count: Int): ResultSet =
+ throw new SQLFeatureNotSupportedException
+
+ override def getResultSet(index: Long, count: Int, map: util.Map[String,
Class[_]]): ResultSet =
+ throw new SQLFeatureNotSupportedException
+
+ override def free(): Unit = ()
+
+ override def toString: String = JdbcTypeUtils.toJson(elements,
ArrayType(elementType))
+}
diff --git
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectMap.scala
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectMap.scala
new file mode 100644
index 000000000000..90854ea2300b
--- /dev/null
+++
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectMap.scala
@@ -0,0 +1,41 @@
+/*
+ * 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.connect.client.jdbc
+
+import org.apache.spark.sql.connect.client.jdbc.util.JdbcTypeUtils
+import org.apache.spark.sql.types.MapType
+
+/**
+ * A [[java.util.Map]] backed by a MAP value materialized by the Spark Connect
client. JDBC has
+ * no dedicated type for maps, so getObject returns a [[java.util.Map]];
entries are converted to
+ * standard JDBC objects (nested ARRAY/MAP/STRUCT become [[java.sql.Array]] /
[[java.util.Map]] /
+ * [[java.sql.Struct]]). toString renders JSON, consistent with
[[SparkConnectArray]] and
+ * [[SparkConnectStruct]].
+ */
+private[jdbc] class SparkConnectMap(
+ entries: scala.collection.Map[Any, Any],
+ mapType: MapType) extends java.util.LinkedHashMap[AnyRef, AnyRef] {
+
+ entries.foreach { case (k, v) =>
+ put(
+ JdbcTypeUtils.toJdbcObject(k, mapType.keyType),
+ JdbcTypeUtils.toJdbcObject(v, mapType.valueType))
+ }
+
+ override def toString: String = JdbcTypeUtils.toJson(entries, mapType)
+}
diff --git
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectResultSet.scala
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectResultSet.scala
index e90f80f783dc..6b02f655f04c 100644
---
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectResultSet.scala
+++
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectResultSet.scala
@@ -27,8 +27,8 @@ import java.util.Calendar
import org.apache.spark.sql.Row
import org.apache.spark.sql.connect.client.SparkResult
-import org.apache.spark.sql.connect.client.jdbc.util.JdbcErrorUtils
-import org.apache.spark.sql.types.{TimestampNTZType, TimestampType}
+import org.apache.spark.sql.connect.client.jdbc.util.{JdbcErrorUtils,
JdbcTypeUtils}
+import org.apache.spark.sql.types.{ArrayType, MapType, StructType,
TimestampNTZType, TimestampType}
class SparkConnectResultSet(
sparkResult: SparkResult[Row],
@@ -115,10 +115,15 @@ class SparkConnectResultSet(
override def getString(columnIndex: Int): String = {
getColumnValue(columnIndex, null: String) { idx =>
- currentRow.get(idx) match {
- case bytes: Array[Byte] =>
- new String(bytes, java.nio.charset.StandardCharsets.UTF_8)
- case other => String.valueOf(other)
+ sparkResult.schema(idx).dataType match {
+ case dt @ (_: ArrayType | _: MapType | _: StructType) =>
+ JdbcTypeUtils.toJson(currentRow.get(idx), dt)
+ case _ =>
+ currentRow.get(idx) match {
+ case bytes: Array[Byte] =>
+ new String(bytes, java.nio.charset.StandardCharsets.UTF_8)
+ case other => String.valueOf(other)
+ }
}
}
}
@@ -269,7 +274,7 @@ class SparkConnectResultSet(
override def getObject(columnIndex: Int): AnyRef = {
getColumnValue(columnIndex, null: AnyRef) { idx =>
- currentRow.get(idx).asInstanceOf[AnyRef]
+ JdbcTypeUtils.toJdbcObject(currentRow.get(idx),
sparkResult.schema(idx).dataType)
}
}
@@ -529,7 +534,14 @@ class SparkConnectResultSet(
throw new SQLFeatureNotSupportedException
override def getArray(columnIndex: Int): JdbcArray =
- throw new SQLFeatureNotSupportedException
+ getColumnValue(columnIndex, null: JdbcArray) { idx =>
+ JdbcTypeUtils.toJdbcObject(currentRow.get(idx),
sparkResult.schema(idx).dataType) match {
+ case array: JdbcArray => array
+ case _ =>
+ throw new SQLException(
+ s"The column ${idx + 1} is not an ARRAY type.")
+ }
+ }
override def getObject(columnLabel: String, map: util.Map[String,
Class[_]]): AnyRef =
throw new SQLFeatureNotSupportedException
diff --git
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStruct.scala
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStruct.scala
new file mode 100644
index 000000000000..76411051dbd0
--- /dev/null
+++
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStruct.scala
@@ -0,0 +1,47 @@
+/*
+ * 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.connect.client.jdbc
+
+import java.sql.{SQLFeatureNotSupportedException, Struct}
+import java.util
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.connect.client.jdbc.util.JdbcTypeUtils
+import org.apache.spark.sql.types.StructType
+
+/**
+ * A [[java.sql.Struct]] backed by a STRUCT value materialized by the Spark
Connect client.
+ * Attributes are converted to standard JDBC objects on access (nested
ARRAY/MAP/STRUCT become
+ * [[java.sql.Array]] / [[java.util.Map]] / [[java.sql.Struct]]).
+ */
+private[jdbc] class SparkConnectStruct(
+ row: Row,
+ structType: StructType) extends Struct {
+
+ override def getSQLTypeName: String = structType.sql
+
+ override def getAttributes: Array[AnyRef] =
+ structType.fields.zipWithIndex.map { case (field, i) =>
+ JdbcTypeUtils.toJdbcObject(row.get(i), field.dataType)
+ }
+
+ override def getAttributes(map: util.Map[String, Class[_]]): Array[AnyRef] =
+ throw new SQLFeatureNotSupportedException
+
+ override def toString: String = JdbcTypeUtils.toJson(row, structType)
+}
diff --git
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcTypeUtils.scala
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcTypeUtils.scala
index 458f94c51f89..48a42cd9ec9c 100644
---
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcTypeUtils.scala
+++
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcTypeUtils.scala
@@ -21,11 +21,19 @@ import java.lang.{Boolean => JBoolean, Byte => JByte,
Double => JDouble, Float =
import java.math.{BigDecimal => JBigDecimal}
import java.sql.{Array => _, _}
+import org.json4s.JObject
+import org.json4s.jackson.JsonMethods.{compact, render}
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.catalyst.expressions.GenericRowWithSchema
+import org.apache.spark.sql.connect.client.jdbc.{SparkConnectArray,
SparkConnectMap, SparkConnectStruct}
import org.apache.spark.sql.types._
private[jdbc] object JdbcTypeUtils {
- def getColumnType(field: StructField): Int = field.dataType match {
+ def getColumnType(field: StructField): Int = getColumnType(field.dataType)
+
+ def getColumnType(dataType: DataType): Int = dataType match {
case NullType => Types.NULL
case BooleanType => Types.BOOLEAN
case ByteType => Types.TINYINT
@@ -41,6 +49,9 @@ private[jdbc] object JdbcTypeUtils {
case TimestampNTZType => Types.TIMESTAMP
case BinaryType => Types.VARBINARY
case _: TimeType => Types.TIME
+ case _: ArrayType => Types.ARRAY
+ case _: MapType => Types.JAVA_OBJECT
+ case _: StructType => Types.STRUCT
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
}
@@ -61,6 +72,9 @@ private[jdbc] object JdbcTypeUtils {
case TimestampNTZType => classOf[Timestamp].getName
case BinaryType => classOf[Array[Byte]].getName
case _: TimeType => classOf[Time].getName
+ case _: ArrayType => classOf[java.sql.Array].getName
+ case _: MapType => classOf[java.util.Map[_, _]].getName
+ case _: StructType => classOf[java.sql.Struct].getName
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
}
@@ -69,7 +83,7 @@ private[jdbc] object JdbcTypeUtils {
case ByteType | ShortType | IntegerType | LongType | FloatType |
DoubleType |
_: DecimalType => true
case NullType | BooleanType | StringType | DateType | BinaryType | _:
TimeType |
- TimestampType | TimestampNTZType => false
+ TimestampType | TimestampNTZType | _: ArrayType | _: MapType | _:
StructType => false
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
}
@@ -94,6 +108,9 @@ private[jdbc] object JdbcTypeUtils {
// Users can call getObject(index, classOf[LocalTime]) to access full
microsecond
// precision when the source type is TIME(4) or higher.
case TimeType(precision) => precision
+ // ResultSetMetaData.getPrecision: "0 is returned for data types where the
+ // column size is not applicable."
+ case _: ArrayType | _: MapType | _: StructType => 0
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
}
@@ -104,7 +121,7 @@ private[jdbc] object JdbcTypeUtils {
case TimestampType => 6
case TimestampNTZType => 6
case NullType | BooleanType | ByteType | ShortType | IntegerType |
LongType | StringType |
- DateType | BinaryType | _: TimeType => 0
+ DateType | BinaryType | _: TimeType | _: ArrayType | _: MapType | _:
StructType => 0
case DecimalType.Fixed(_, s) => s
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
@@ -131,6 +148,7 @@ private[jdbc] object JdbcTypeUtils {
case DecimalType.Fixed(p, s) if s == 0 => p + 1
// precision + negative sign + decimal point, like DECIMAL(5,2) = -123.45
case DecimalType.Fixed(p, _) => p + 2
+ case _: ArrayType | _: MapType | _: StructType => Int.MaxValue
case other =>
throw new SQLFeatureNotSupportedException(s"DataType $other is not
supported yet.")
}
@@ -149,4 +167,38 @@ private[jdbc] object JdbcTypeUtils {
case _: NumericType => 10
case _ => null
}
+
+ /**
+ * Converts a value materialized by the Spark Connect client (Scala Seq /
Map / Row for
+ * complex types) into the corresponding standard JDBC object, recursively:
+ * ARRAY -> java.sql.Array, STRUCT -> java.sql.Struct, MAP -> java.util.Map.
Scalar values
+ * are returned as is.
+ */
+ def toJdbcObject(value: Any, dataType: DataType): AnyRef = {
+ if (value == null) {
+ null
+ } else {
+ dataType match {
+ case at: ArrayType =>
+ new SparkConnectArray(value.asInstanceOf[scala.collection.Seq[Any]],
at.elementType)
+ case st: StructType =>
+ new SparkConnectStruct(value.asInstanceOf[Row], st)
+ case mt: MapType =>
+ new SparkConnectMap(value.asInstanceOf[scala.collection.Map[Any,
Any]], mt)
+ case _ => value.asInstanceOf[AnyRef]
+ }
+ }
+ }
+
+ /**
+ * Renders a complex type value as JSON text following the Row.json format,
used as a
+ * stable string representation for getString and the complex type toString
fallbacks.
+ * Row.jsonValue requires a Row with a schema, so wrap the value into a
single-field row
+ * and extract the field back from the rendered JSON object.
+ */
+ def toJson(value: Any, dataType: DataType): String = {
+ val wrapped = new GenericRowWithSchema(
+ Array(value), StructType(Array(StructField("c", dataType))))
+ compact(render(wrapped.jsonValue.asInstanceOf[JObject].obj.head._2))
+ }
}
diff --git
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
index 747d9dc5d0ff..49c0dccc780c 100644
---
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
+++
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
@@ -667,7 +667,10 @@ class SparkConnectDatabaseMetaDataSuite extends
ConnectFunSuite with RemoteSpark
| col_timestamp TIMESTAMP,
| col_timestamp_ntz TIMESTAMP_NTZ,
| col_binary BINARY,
- | col_time TIME)""".stripMargin)
+ | col_time TIME,
+ | col_array ARRAY<INT>,
+ | col_map MAP<STRING, INT>,
+ | col_struct STRUCT<a: INT, b: STRING>)""".stripMargin)
spark.sql(
"""CREATE VIEW IF NOT EXISTS spark_catalog.db1.t1_v AS
@@ -720,6 +723,9 @@ class SparkConnectDatabaseMetaDataSuite extends
ConnectFunSuite with RemoteSpark
("spark_catalog", "db1", "t2", 13, "col_timestamp_ntz"),
("spark_catalog", "db1", "t2", 14, "col_binary"),
("spark_catalog", "db1", "t2", 15, "col_time"),
+ ("spark_catalog", "db1", "t2", 16, "col_array"),
+ ("spark_catalog", "db1", "t2", 17, "col_map"),
+ ("spark_catalog", "db1", "t2", 18, "col_struct"),
("spark_catalog", "db_", "t_", 1, "id"),
("spark_catalog", "db_", "t_", 2, "i_"),
("spark_catalog", "db_2", "t_2", 1, "id"),
diff --git
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectJdbcDataTypeSuite.scala
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectJdbcDataTypeSuite.scala
index 99511c56740b..4ad8ab91d4b2 100644
---
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectJdbcDataTypeSuite.scala
+++
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectJdbcDataTypeSuite.scala
@@ -17,12 +17,13 @@
package org.apache.spark.sql.connect.client.jdbc
-import java.sql.{ResultSet, SQLException, Types}
+import java.sql.{Array => JdbcArray, ResultSet, SQLException, Struct, Types}
import scala.util.Using
import org.apache.spark.sql.connect.client.jdbc.test.JdbcHelper
import org.apache.spark.sql.connect.test.{ConnectFunSuite, RemoteSparkSession}
+import org.apache.spark.util.SparkClassUtils
class SparkConnectJdbcDataTypeSuite extends ConnectFunSuite with
RemoteSparkSession
with JdbcHelper {
@@ -789,4 +790,187 @@ class SparkConnectJdbcDataTypeSuite extends
ConnectFunSuite with RemoteSparkSess
}
}
}
+
+ test("get array type") {
+ withExecuteQuery("SELECT array(1, 2, 3)") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[JdbcArray])
+ val array = value.asInstanceOf[JdbcArray]
+ assert(array.getBaseType === Types.INTEGER)
+ assert(array.getArray.asInstanceOf[Array[_]].length === 3)
+ assert(array.toString === "[1,2,3]")
+ assert(rs.getArray(1).getArray.asInstanceOf[Array[_]].length === 3)
+ assert(rs.getString(1) === "[1,2,3]")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnName(1) === "array(1, 2, 3)")
+ assert(metaData.getColumnLabel(1) === "array(1, 2, 3)")
+ assert(metaData.getColumnType(1) === Types.ARRAY)
+ assert(metaData.getColumnTypeName(1) === "ARRAY<INT>")
+ assert(metaData.getColumnClassName(1) === "java.sql.Array")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get map type") {
+ withExecuteQuery("SELECT map('a', 1)") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[java.util.Map[_, _]])
+ val map = value.asInstanceOf[java.util.Map[String, AnyRef]]
+ assert(map.get("a") === 1)
+ assert(value.toString === """{"a":1}""")
+ assert(rs.getString(1) === """{"a":1}""")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnName(1) === "map(a, 1)")
+ assert(metaData.getColumnLabel(1) === "map(a, 1)")
+ assert(metaData.getColumnType(1) === Types.JAVA_OBJECT)
+ assert(metaData.getColumnTypeName(1) === "MAP<STRING, INT>")
+ assert(metaData.getColumnClassName(1) === "java.util.Map")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get struct type") {
+ withExecuteQuery("SELECT named_struct('a', 1, 'b', 'x')") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[Struct])
+ val struct = value.asInstanceOf[Struct]
+ val attrs = struct.getAttributes
+ assert(attrs(0) === 1)
+ assert(attrs(1) === "x")
+ assert(value.toString === """{"a":1,"b":"x"}""")
+ assert(rs.getString(1) === """{"a":1,"b":"x"}""")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnName(1) === "named_struct(a, 1, b, x)")
+ assert(metaData.getColumnLabel(1) === "named_struct(a, 1, b, x)")
+ assert(metaData.getColumnType(1) === Types.STRUCT)
+ assert(metaData.getColumnTypeName(1) === "STRUCT<a: INT NOT NULL, b:
STRING NOT NULL>")
+ assert(metaData.getColumnClassName(1) === "java.sql.Struct")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get nested array of struct type") {
+ withExecuteQuery("SELECT array(named_struct('a', 1), named_struct('a',
2))") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[JdbcArray])
+ val elems = value.asInstanceOf[JdbcArray].getArray.asInstanceOf[Array[_]]
+ assert(elems.length === 2)
+ assert(elems(0).isInstanceOf[Struct])
+ assert(elems(0).asInstanceOf[Struct].getAttributes()(0) === 1)
+ assert(elems(1).asInstanceOf[Struct].getAttributes()(0) === 2)
+ assert(value.toString === """[{"a":1},{"a":2}]""")
+ assert(rs.getString(1) === """[{"a":1},{"a":2}]""")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnType(1) === Types.ARRAY)
+ assert(metaData.getColumnTypeName(1) === "ARRAY<STRUCT<a: INT NOT
NULL>>")
+ assert(metaData.getColumnClassName(1) === "java.sql.Array")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get nested map of array type") {
+ withExecuteQuery("SELECT map('x', array(1, 2))") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[java.util.Map[_, _]])
+ val map = value.asInstanceOf[java.util.Map[String, AnyRef]]
+ assert(map.get("x").isInstanceOf[JdbcArray])
+
assert(map.get("x").asInstanceOf[JdbcArray].getArray.asInstanceOf[Array[_]].length
=== 2)
+ assert(value.toString === """{"x":[1,2]}""")
+ assert(rs.getString(1) === """{"x":[1,2]}""")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnType(1) === Types.JAVA_OBJECT)
+ assert(metaData.getColumnTypeName(1) === "MAP<STRING, ARRAY<INT>>")
+ assert(metaData.getColumnClassName(1) === "java.util.Map")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get nested struct of map type") {
+ withExecuteQuery("SELECT named_struct('m', map('k', 1))") { rs =>
+ assert(rs.next())
+ val value = rs.getObject(1)
+ assert(value.isInstanceOf[Struct])
+ val attr = value.asInstanceOf[Struct].getAttributes()(0)
+ assert(attr.isInstanceOf[java.util.Map[_, _]])
+ assert(attr.asInstanceOf[java.util.Map[String, AnyRef]].get("k") === 1)
+ assert(value.toString === """{"m":{"k":1}}""")
+ assert(rs.getString(1) === """{"m":{"k":1}}""")
+ assert(!rs.wasNull)
+ assert(!rs.next())
+
+ val metaData = rs.getMetaData
+ assert(metaData.getColumnCount === 1)
+ assert(metaData.getColumnType(1) === Types.STRUCT)
+ assert(metaData.getColumnTypeName(1) === "STRUCT<m: MAP<STRING, INT> NOT
NULL>")
+ assert(metaData.getColumnClassName(1) === "java.sql.Struct")
+
assert(SparkClassUtils.classForName(metaData.getColumnClassName(1)).isInstance(value))
+ assert(metaData.isSigned(1) === false)
+ assert(metaData.getPrecision(1) === 0)
+ assert(metaData.getScale(1) === 0)
+ assert(metaData.getColumnDisplaySize(1) === Int.MaxValue)
+ }
+ }
+
+ test("get complex types with null") {
+ Seq(
+ "cast(null as array<int>)",
+ "cast(null as map<string, int>)",
+ "cast(null as struct<a: int>)").foreach { expr =>
+ withExecuteQuery(s"SELECT $expr") { rs =>
+ assert(rs.next())
+ assert(rs.getObject(1) === null)
+ assert(rs.wasNull)
+ assert(rs.getString(1) === null)
+ assert(rs.wasNull)
+ assert(rs.getArray(1) === null)
+ assert(rs.wasNull)
+ assert(!rs.next())
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]