Repository: googleapis/java-spanner-jdbc (java)

Issue:
NullPointerException when querying structs
Queries that select an array of structs cause a NullPointerException in the JDBC driver. For example the following query cannot be executed using the JDBC driver:

```sql
WITH points AS
  (SELECT [1, 5] as point
   UNION ALL SELECT [2, 8] as point
   UNION ALL SELECT [3, 7] as point
   UNION ALL SELECT [4, 1] as point
   UNION ALL SELECT [5, 7] as point)
SELECT ARRAY(
  SELECT STRUCT(point)
  FROM points)
  AS coordinates;
```

Pull request description:
fix: NPE was thrown when getting an array of structs from a ResultSet
A `NullPointerException` was thrown if a `ResultSet` contained an array of `Struct`s and the `getArray()` method was called on the column.

Fixes #444


Patch:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index 1dc5f0e..6e6c3bf 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
@@ -17,6 +17,7 @@
 package com.google.cloud.spanner.jdbc;
 
 import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Struct;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
 import java.math.BigDecimal;
@@ -253,6 +254,32 @@ enum JdbcDataType {
     public Type getSpannerType() {
       return Type.timestamp();
     }
+  },
+  STRUCT {
+    @Override
+    public int getSqlType() {
+      return Types.STRUCT;
+    }
+
+    @Override
+    public Class<Struct> getJavaClass() {
+      return Struct.class;
+    }
+
+    @Override
+    public Code getCode() {
+      return Code.STRUCT;
+    }
+
+    @Override
+    public List<Struct> getArrayElements(ResultSet rs, int columnIndex) {
+      return rs.getStructList(columnIndex);
+    }
+
+    @Override
+    public Type getSpannerType() {
+      return Type.struct();
+    }
   };
 
   public abstract int getSqlType();


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
index 24e3e57..f8cacd9 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
@@ -18,15 +18,19 @@ package com.google.cloud.spanner.jdbc;
 
 import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.toSqlDate;
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Struct;
 import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
 import java.math.BigDecimal;
 import java.sql.Date;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
 import java.sql.Timestamp;
 import java.sql.Types;
 import org.junit.Test;
@@ -174,16 +178,17 @@ public class JdbcArrayTest {
   }
 
   @Test
-  public void testCreateArrayOfStruct() {
-    try {
-      JdbcArray.createArray("STRUCT", new Object[] {});
-      fail("missing expected exception");
-    } catch (SQLException e) {
-      assertThat((Exception) e).isInstanceOf(JdbcSqlException.class);
-      JdbcSqlException jse = (JdbcSqlException) e;
-      assertThat(jse.getErrorCode())
-          .isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
-    }
+  public void testCreateArrayOfStruct() throws SQLException {
+    JdbcArray array =
+        JdbcArray.createArray(
+            "STRUCT",
+            new Struct[] {Struct.newBuilder().set("f1").to("v1").set("f2").to(1L).build(), null});
+    assertEquals(Types.STRUCT, array.getBaseType());
+    assertThat((Struct[]) array.getArray())
+        .asList()
+        .containsExactly(Struct.newBuilder().set("f1").to("v1").set("f2").to(1L).build(), null)
+        .inOrder();
+    assertThrows(SQLFeatureNotSupportedException.class, () -> array.getResultSet());
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSimpleStatementsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSimpleStatementsTest.java
index 15f25b9..fcb3b0e 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSimpleStatementsTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSimpleStatementsTest.java
@@ -17,14 +17,22 @@
 package com.google.cloud.spanner.jdbc.it;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.cloud.spanner.IntegrationTest;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest;
+import java.sql.Array;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
 import java.sql.Statement;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
@@ -149,4 +157,38 @@ public class ITJdbcSimpleStatementsTest extends ITAbstractJdbcTest {
               "Calling addBatch() is not allowed when a DML or DDL batch has been started on the connection.");
     }
   }
+
+  @Test
+  public void testSelectArrayOfStructs() throws SQLException {
+    String sql =
+        "WITH points AS\n"
+            + "  (SELECT [1, 5] as point\n"
+            + "   UNION ALL SELECT [2, 8] as point\n"
+            + "   UNION ALL SELECT [3, 7] as point\n"
+            + "   UNION ALL SELECT [4, 1] as point\n"
+            + "   UNION ALL SELECT [5, 7] as point)\n"
+            + "SELECT ARRAY(\n"
+            + "  SELECT STRUCT(point)\n"
+            + "  FROM points)\n"
+            + "  AS coordinates";
+    try (Connection connection = createConnection()) {
+      try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
+        assertTrue(resultSet.next());
+        assertEquals(resultSet.getMetaData().getColumnCount(), 1);
+        Array array = resultSet.getArray(1);
+        assertThat((Struct[]) array.getArray())
+            .asList()
+            .containsExactly(
+                Struct.newBuilder().set("point").to(Value.int64Array(new long[] {1L, 5L})).build(),
+                Struct.newBuilder().set("point").to(Value.int64Array(new long[] {2L, 8L})).build(),
+                Struct.newBuilder().set("point").to(Value.int64Array(new long[] {3L, 7L})).build(),
+                Struct.newBuilder().set("point").to(Value.int64Array(new long[] {4L, 1L})).build(),
+                Struct.newBuilder().set("point").to(Value.int64Array(new long[] {5L, 7L})).build());
+        // Getting a result set from an array of structs is not supported, as structs are not
+        // supported as a valid column type in a result set.
+        assertThrows(SQLFeatureNotSupportedException.class, () -> array.getResultSet());
+        assertFalse(resultSet.next());
+      }
+    }
+  }
 }