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]

Reply via email to