This is an automated email from the ASF dual-hosted git repository.
hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 6a446490718 [fix](regression) Materialize JDBC array results (#65084)
6a446490718 is described below
commit 6a4464907183c6af418cd92160a75b2d5244b9a2
Author: shuke <[email protected]>
AuthorDate: Fri Jul 3 11:01:38 2026 +0800
[fix](regression) Materialize JDBC array results (#65084)
### What
Materialize Arrow JDBC complex wrapper values while regression-test
reads rows from `ResultSet`.
### Why
Arrow Flight JDBC array objects keep references to Arrow vectors owned
by the live result set. The regression-test framework stores row objects
first, closes the `ResultSet`/`Statement`, and later calls
`OutputUtils.toCsvString()`. At that point the vector inside
`ArrowFlightJdbcArray` has already been cleared, so `Array.toString()` /
`Array.getArray()` can throw `IndexOutOfBoundsException`.
This change renders Arrow materialized complex objects to stable Doris
`.out`-style text while the result set is still alive. The renderer
preserves existing formatting for arrays, maps, structs, nulls, numeric
map keys, string map keys, and string values containing quotes.
### Tests
- `git diff --check`
- Groovy parse check for `JdbcUtils.groovy` with local Guava/slf4j
classpath
- Reflection check for array, struct-like map, numeric-key map,
string-key map, quoted-string array, and string-with-comma samples
Full regression verification will run in CI.
---
.../json_function/test_query_json_insert.out | 3 +
.../apache/doris/regression/util/JdbcUtils.groovy | 131 ++++++++++++++++++++-
.../json_function/test_query_json_insert.groovy | 1 +
3 files changed, 130 insertions(+), 5 deletions(-)
diff --git
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
index 03210f0f6a2..9c6126266f9 100644
---
a/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
+++
b/regression-test/data/query_p0/sql_functions/json_function/test_query_json_insert.out
@@ -47,6 +47,9 @@
-- !sql_json --
{"data":{"json":{"a":1.1}}}
+-- !sql_escape --
+["a\\b", "line\\nfeed"] {"name":"a\\b", "note":"line\\nfeed"}
+
-- !sql2 --
{"data":{"array":null,"struct":null,"json":null}}
{"data":{"array":["a","b"],"struct":{"name":"a","age":1},"json":{"a":"b"}}}
diff --git
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/JdbcUtils.groovy
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/JdbcUtils.groovy
index c79303c6f1e..b33759e341a 100644
---
a/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/JdbcUtils.groovy
+++
b/regression-test/framework/src/main/groovy/org/apache/doris/regression/util/JdbcUtils.groovy
@@ -134,7 +134,7 @@ class JdbcUtils {
row.add(resultSet.getObject(i))
}
} else {
- row.add(resultSet.getObject(i))
+ row.add(materializeObject(resultSet.getObject(i)))
}
}
rows.add(row)
@@ -153,7 +153,7 @@ class JdbcUtils {
try {
if (isPreparedStatement) {
// For prepared statements, use getObject to get
the value
- row.add(resultSet.getObject(i))
+ row.add(materializeObject(resultSet.getObject(i)))
} else {
int jdbcType = resultSet.metaData.getColumnType(i)
if (jdbcType == Types.TIME
@@ -177,7 +177,7 @@ class JdbcUtils {
byte[] bytes = resultSet.getBytes(i)
row.add(bytes == null ? null :
bytesToHex(bytes))
} else {
- row.add(resultSet.getObject(i))
+
row.add(materializeObject(resultSet.getObject(i)))
}
}
} catch (Throwable t) {
@@ -185,7 +185,7 @@ class JdbcUtils {
if(resultSet.getBytes(i) != null){
row.add(new String(resultSet.getBytes(i)))
} else {
- row.add(resultSet.getObject(i))
+
row.add(materializeObject(resultSet.getObject(i)))
}
} catch (Throwable t2) {
/**
@@ -195,7 +195,7 @@ class JdbcUtils {
at
org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321)
at
org.apache.doris.regression.util.JdbcUtils$_toStringList_closure5.doCall(JdbcUtils.groovy:163)
*/
- row.add(resultSet.getObject(i))
+ row.add(materializeObject(resultSet.getObject(i)))
}
}
}
@@ -205,6 +205,127 @@ class JdbcUtils {
}
}
+ private static Object materializeObject(Object value) {
+ if (value instanceof java.sql.Array) {
+ return renderComplexValue(((java.sql.Array) value).getArray())
+ }
+ if (value instanceof java.sql.Struct) {
+ return renderComplexValue(((java.sql.Struct)
value).getAttributes())
+ }
+ if (isArrowJsonStringContainer(value)) {
+ return renderComplexValue(value)
+ }
+ if (value instanceof byte[]) {
+ return bytesToHex((byte[]) value)
+ }
+ if (isGenericComplexContainer(value)) {
+ return renderComplexValue(value)
+ }
+ return value
+ }
+
+ private static boolean isGenericComplexContainer(Object value) {
+ return value != null && (value instanceof Map
+ || value.getClass().isArray()
+ || value instanceof Iterable)
+ }
+
+ private static boolean isArrowJsonStringContainer(Object value) {
+ if (value == null) {
+ return false
+ }
+ String className = value.getClass().getName()
+ return className.endsWith(".JsonStringHashMap") ||
className.endsWith(".JsonStringArrayList")
+ }
+
+ private static String renderComplexValue(Object value) {
+ if (value == null) {
+ return "null"
+ }
+ if (value instanceof java.sql.Array) {
+ return renderComplexValue(((java.sql.Array) value).getArray())
+ }
+ if (value instanceof java.sql.Struct) {
+ return renderComplexValue(((java.sql.Struct)
value).getAttributes())
+ }
+ if (value instanceof Map) {
+ StringBuilder sb = new StringBuilder()
+ sb.append("{")
+ boolean first = true
+ ((Map<?, ?>) value).each { Object key, Object mapValue ->
+ if (!first) {
+ sb.append(", ")
+ }
+
sb.append(renderComplexKey(key)).append(":").append(renderComplexValue(mapValue))
+ first = false
+ }
+ sb.append("}")
+ return sb.toString()
+ }
+ if (value instanceof byte[]) {
+ return bytesToHex((byte[]) value)
+ }
+ if (value.getClass().isArray()) {
+ StringBuilder sb = new StringBuilder()
+ sb.append("[")
+ int length = java.lang.reflect.Array.getLength(value)
+ for (int i = 0; i < length; i++) {
+ if (i > 0) {
+ sb.append(", ")
+ }
+
sb.append(renderComplexValue(java.lang.reflect.Array.get(value, i)))
+ }
+ sb.append("]")
+ return sb.toString()
+ }
+ if (value instanceof Iterable) {
+ StringBuilder sb = new StringBuilder()
+ sb.append("[")
+ boolean first = true
+ ((Iterable<?>) value).each { Object item ->
+ if (!first) {
+ sb.append(", ")
+ }
+ sb.append(renderComplexValue(item))
+ first = false
+ }
+ sb.append("]")
+ return sb.toString()
+ }
+ if (isStringLikeComplexValue(value)) {
+ return "\"" + escapeComplexString(value.toString()) + "\""
+ }
+ return value.toString()
+ }
+
+ private static String renderComplexKey(Object key) {
+ if (key == null) {
+ return "null"
+ }
+ if (isStringLikeComplexValue(key)) {
+ return "\"" + escapeComplexString(key.toString()) + "\""
+ }
+ return renderComplexValue(key)
+ }
+
+ private static boolean isStringLikeComplexValue(Object value) {
+ return value instanceof CharSequence
+ || isTextLikeObject(value)
+ || value instanceof java.time.temporal.TemporalAccessor
+ || value instanceof java.util.Date
+ }
+
+ private static boolean isTextLikeObject(Object value) {
+ return value != null && value.getClass().getName().endsWith(".Text")
+ }
+
+ private static String escapeComplexString(String value) {
+ if (value == null) {
+ return null
+ }
+ return value
+ }
+
// Detect if a JDBC column type is binary-like
private static boolean isBinaryJdbcType(int jdbcType) {
return jdbcType == Types.BINARY
diff --git
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
index 95c0cc3ab64..13893795fc1 100644
---
a/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
+++
b/regression-test/suites/query_p0/sql_functions/json_function/test_query_json_insert.groovy
@@ -59,6 +59,7 @@ suite("test_query_json_insert", "query,arrow_flight_sql") {
qt_sql_json """ SELECT json_insert('{"data": {}}', '\$.data.json',
cast('{\"a\":\"b\"}' as JSON)); """
qt_sql_json """ SELECT json_insert('{"data": {}}', '\$.data.json',
cast('{\"a\":1}' as JSON)); """
qt_sql_json """ SELECT json_insert('{"data": {}}', '\$.data.json',
cast('{\"a\":1.1}' as JSON)); """
+ qt_sql_escape """ SELECT array(concat('a', char(92), 'b'), concat('line',
char(92), 'nfeed')), named_struct('name', concat('a', char(92), 'b'), 'note',
concat('line', char(92), 'nfeed')); """
// test with table
tableName = "test_query_json_insert_complex"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]