diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java index e9e80323299f..55a21b304932 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java @@ -29,6 +29,7 @@ import java.sql.Timestamp; import java.util.Base64; import java.util.Map; +import org.apache.arrow.vector.util.Text; /** * An abstract implementation of {@link java.sql.Struct} used as a base class for {@link @@ -86,30 +87,24 @@ public String toString() { Object value = attributes[i]; sb.append("\"").append(fieldName.replace("\"", "\\\"")).append("\":"); - - if (value == null) { - sb.append("null"); - } else if (value instanceof String || value instanceof org.apache.arrow.vector.util.Text) { - String stringValue = value.toString().replace("\"", "\\\""); - sb.append("\"").append(stringValue).append("\""); - } else if (value instanceof Timestamp - && field.getType().getStandardType() == StandardSQLTypeName.TIMESTAMP) { - sb.append("\"") - .append(BigQueryTemporalUtility.formatTimestampToUtcString((Timestamp) value)) - .append("\""); - } else if (value instanceof Time - && field.getType().getStandardType() == StandardSQLTypeName.TIME) { - sb.append("\"") - .append( - BigQueryTypeRegistry.convert( - getRawAttribute(i), StandardSQLTypeName.TIME, String.class, null)) - .append("\""); - } else if (value instanceof Timestamp || value instanceof Date || value instanceof Time) { - sb.append("\"").append(value.toString()).append("\""); - } else if (value instanceof byte[]) { - sb.append("\"").append(Base64.getEncoder().encodeToString((byte[]) value)).append("\""); + if (value instanceof BigQueryBaseArray) { + BigQueryBaseArray arrayVal = (BigQueryBaseArray) value; + Object[] elements = (Object[]) arrayVal.getArray(); + if (elements == null) { + sb.append("null"); + } else { + sb.append("["); + for (int j = 0; j < elements.length; j++) { + if (j > 0) { + sb.append(","); + } + appendJsonValue( + sb, field, elements[j], elements[j] == null ? null : arrayVal.getRawElement(j)); + } + sb.append("]"); + } } else { - sb.append(value.toString()); + appendJsonValue(sb, field, value, value == null ? null : getRawAttribute(i)); } } sb.append("}"); @@ -119,4 +114,33 @@ public String toString() { return "{ \"error\": \"Error converting struct to string: " + e.getMessage() + "\" }"; } } + + private static void appendJsonValue(StringBuilder sb, Field field, Object value, Object rawValue) + throws SQLException { + if (value == null) { + sb.append("null"); + } else if (field.getType().getStandardType() == StandardSQLTypeName.JSON) { + sb.append(value); + } else if (value instanceof String || value instanceof Text) { + String stringValue = value.toString().replace("\"", "\\\""); + sb.append("\"").append(stringValue).append("\""); + } else if (value instanceof Timestamp + && field.getType().getStandardType() == StandardSQLTypeName.TIMESTAMP) { + sb.append("\"") + .append(BigQueryTemporalUtility.formatTimestampToUtcString((Timestamp) value)) + .append("\""); + } else if (value instanceof Time + && field.getType().getStandardType() == StandardSQLTypeName.TIME) { + sb.append("\"") + .append( + BigQueryTypeRegistry.convert(rawValue, StandardSQLTypeName.TIME, String.class, null)) + .append("\""); + } else if (value instanceof Timestamp || value instanceof Date || value instanceof Time) { + sb.append("\"").append(value).append("\""); + } else if (value instanceof byte[]) { + sb.append("\"").append(Base64.getEncoder().encodeToString((byte[]) value)).append("\""); + } else { + sb.append(value); + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStructTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStructTest.java index 1e431175d19b..46592e20397f 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStructTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowStructTest.java @@ -181,6 +181,20 @@ GEOGRAPHY, new Text("POINT(-122 47)"), new Text("POINT(-122 48)")), .isEqualTo(new String[] {"POINT(-122 47)", "POINT(-122 48)"}); assertThat(((Array) attributes[11]).getArray()) .isEqualTo(new byte[][] {"one".getBytes(), "two".getBytes()}); + assertThat(struct.toString()) + .isEqualTo( + "{\"INT64_arr_column\":[10,20]," + + "\"BOOL_arr_column\":[true,false]," + + "\"FLOAT64_arr_column\":[11.2,33.4]," + + "\"NUMERIC_arr_column\":[11.2657,33.4657]," + + "\"BIGNUMERIC_arr_column\":[11.2657,33.4657]," + + "\"STRING_arr_column\":[\"one\",\"two\"]," + + "\"TIMESTAMP_arr_column\":[\"2023-03-30 11:14:19.820227\",\"2023-03-31 11:14:19.820227\"]," + + "\"DATE_arr_column\":[\"2023-03-30\",\"2023-03-31\"]," + + "\"TIME_arr_column\":[\"11:14:19.820227\",\"11:14:20.820227\"]," + + "\"DATETIME_arr_column\":[\"2023-03-30 11:14:19.820227\",\"2023-03-30 11:15:19.820227\"]," + + "\"GEOGRAPHY_arr_column\":[\"POINT(-122 47)\",\"POINT(-122 48)\"]," + + "\"BYTES_arr_column\":[\"b25l\",\"dHdv\"]}"); } @Test diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java index a57aeacdcc19..6c1b56c58c28 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonStructTest.java @@ -193,6 +193,20 @@ public void structOfArrays() throws SQLException { .isEqualTo(new String[] {"POINT(-122 47)", "POINT(-122 48)"}); assertThat(((Array) attributes[11]).getArray()) .isEqualTo(new byte[][] {"one".getBytes(), "two".getBytes()}); + assertThat(struct.toString()) + .isEqualTo( + "{\"INT64_arr_column\":[10,20]," + + "\"BOOL_arr_column\":[true,false]," + + "\"FLOAT64_arr_column\":[11.2,33.4]," + + "\"NUMERIC_arr_column\":[11.2657,33.4657]," + + "\"BIGNUMERIC_arr_column\":[11.2657,33.4657]," + + "\"STRING_arr_column\":[\"one\",\"two\"]," + + "\"TIMESTAMP_arr_column\":[\"2023-03-30 11:14:19.82\",\"2023-03-31 11:14:19.82\"]," + + "\"DATE_arr_column\":[\"2023-03-30\",\"2023-03-31\"]," + + "\"TIME_arr_column\":[\"11:14:19.82\",\"11:14:20.82\"]," + + "\"DATETIME_arr_column\":[\"2023-03-30 11:14:19.82\",\"2023-03-30 11:15:19.82\"]," + + "\"GEOGRAPHY_arr_column\":[\"POINT(-122 47)\",\"POINT(-122 48)\"]," + + "\"BYTES_arr_column\":[\"b25l\",\"dHdv\"]}"); } @Test diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java index e361ca06f6f1..31853252d06d 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java @@ -71,6 +71,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class ITBigQueryJDBCTest extends ITBase { static final String PROJECT_ID = ServiceOptions.getDefaultProjectId(); @@ -3095,4 +3097,33 @@ public void testSessionAbortedOnConnectionClose() throws SQLException { } } } + + @ParameterizedTest + @ValueSource(strings = {"", FORCE_READ_API_PROPERTIES}) + public void testStructWithNestedArraysValidJson(String extraProps) throws SQLException { + String sql = + "SELECT STRUCT(" + + "1 AS id, " + + "['hello', 'world'] AS str_arr, " + + "[DATE '2026-01-01', DATE '2026-01-02'] AS date_arr, " + + "[TIME '12:34:56.789012'] AS time_arr, " + + "[TIMESTAMP '2026-01-01 00:00:00 UTC'] AS ts_arr, " + + "[b'abc', b'def'] AS bytes_arr" + + ") AS s_nested_arrays"; + + String expectedJson = + "{\"id\":1," + + "\"str_arr\":[\"hello\",\"world\"]," + + "\"date_arr\":[\"2026-01-01\",\"2026-01-02\"]," + + "\"time_arr\":[\"12:34:56.789012\"]," + + "\"ts_arr\":[\"2026-01-01 00:00:00.0\"]," + + "\"bytes_arr\":[\"YWJj\",\"ZGVm\"]}"; + + try (Connection conn = DriverManager.getConnection(connection_uri + extraProps); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + assertTrue(rs.next()); + assertEquals(expectedJson, rs.getString("s_nested_arrays")); + } + } }