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

Issue:
Documentation for connection properties are not formatted correctly.
#### External references such as API reference guides

When reviewing the [API documentation for JdbcDriver](https://googleapis.dev/java/google-cloud-spanner-jdbc/latest/index.html), I found that the content for `optimizerVersion` is not well-formatted. For the source, it seems that it missed a [closing code block element](https://github.com/googleapis/java-spanner-jdbc/blob/9b53df4ea33926d9fa0955a4445c2ea6790ac3dc/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java#L92).

#### Any additional information below


Following these steps guarantees the quickest resolution possible.

Thanks!


Pull request:
docs: fix javadoc formatting
One of the descriptions of the supported connection properties missed an ending `</code>` block, which caused the entire formatting to be incorrect.

Fixes #342


Patch:
diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml
index 2b6cdbc..76e6404 100644
--- a/.github/workflows/auto-release.yaml
+++ b/.github/workflows/auto-release.yaml
@@ -17,7 +17,7 @@ jobs:
           }
 
           // only approve PRs like "chore: release <release version>"
-          if ( !context.payload.pull_request.title.startsWith("chore: release") ) {
+          if ( !context.payload.pull_request.title.startsWith("chore(master): release") ) {
             return;
           }
 
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6830220..98a472e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -54,7 +54,18 @@ jobs:
       with:
         java-version: 8
     - run: java -version
-    - run: .kokoro/linkage-monitor.sh
+    - name: Install artifacts to local Maven repository
+      run: |
+        source .kokoro/common.sh
+        retry_with_backoff 3 10 \
+            mvn install -B -V \
+                -Dmaven.test.skip -DskipTests=true \
+                -Dclirr.skip=true \
+                -Denforcer.skip=true \
+                -Dmaven.javadoc.skip=true \
+                -Dgcloud.download.skip=true
+      shell: bash
+    - uses: GoogleCloudPlatform/cloud-opensource-java/linkage-monitor@v1-linkagemonitor
   lint:
     runs-on: ubuntu-latest
     steps:
diff --git a/.kokoro/linkage-monitor.sh b/.kokoro/linkage-monitor.sh
deleted file mode 100755
index 759ab4e..0000000
--- a/.kokoro/linkage-monitor.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/bash
-# Copyright 2019 Google LLC
-#
-# Licensed 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.
-
-set -eo pipefail
-# Display commands being run.
-set -x
-
-## Get the directory of the build script
-scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}"))
-## cd to the parent directory, i.e. the root of the git repo
-cd ${scriptDir}/..
-
-# include common functions
-source ${scriptDir}/common.sh
-
-# Print out Java version
-java -version
-echo ${JOB_TYPE}
-
-# attempt to install 3 times with exponential backoff (starting with 10 seconds)
-retry_with_backoff 3 10 \
-  mvn install -B -V \
-    -DskipTests=true \
-    -Dclirr.skip=true \
-    -Denforcer.skip=true \
-    -Dmaven.javadoc.skip=true \
-    -Dgcloud.download.skip=true
-
-# Kokoro job cloud-opensource-java/ubuntu/linkage-monitor-gcs creates this JAR
-JAR=linkage-monitor-latest-all-deps.jar
-curl -v -O "https://storage.googleapis.com/cloud-opensource-java-linkage-monitor/${JAR}"
-
-# Fails if there's new linkage errors compared with baseline
-java -jar ${JAR} com.google.cloud:libraries-bom
diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg
index fa2437d..448a04e 100644
--- a/.kokoro/release/publish_javadoc.cfg
+++ b/.kokoro/release/publish_javadoc.cfg
@@ -26,7 +26,4 @@ before_action {
       keyname: "docuploader_service_account"
     }
   }
-}
-
-# Downloads docfx doclet resource. This will be in ${KOKORO_GFILE_DIR}/<doclet name>
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/docfx"
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/.kokoro/release/publish_javadoc11.cfg b/.kokoro/release/publish_javadoc11.cfg
new file mode 100644
index 0000000..28074bc
--- /dev/null
+++ b/.kokoro/release/publish_javadoc11.cfg
@@ -0,0 +1,30 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+  key: "STAGING_BUCKET_V2"
+  value: "docs-staging-v2"
+  # Production will be at: docs-staging-v2
+}
+
+# Configure the docker image for kokoro-trampoline
+env_vars: {
+  key: "TRAMPOLINE_IMAGE"
+  value: "gcr.io/cloud-devrel-kokoro-resources/java11"
+}
+
+env_vars: {
+  key: "TRAMPOLINE_BUILD_FILE"
+  value: "github/java-spanner-jdbc/.kokoro/release/publish_javadoc11.sh"
+}
+
+before_action {
+  fetch_keystore {
+    keystore_resource {
+      keystore_config_id: 73713
+      keyname: "docuploader_service_account"
+    }
+  }
+}
+
+# Downloads docfx doclet resource. This will be in ${KOKORO_GFILE_DIR}/<doclet name>
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/docfx"
diff --git a/.kokoro/release/publish_javadoc11.sh b/.kokoro/release/publish_javadoc11.sh
new file mode 100755
index 0000000..dc26611
--- /dev/null
+++ b/.kokoro/release/publish_javadoc11.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Copyright 2021 Google Inc.
+#
+# Licensed 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.
+
+set -eo pipefail
+
+if [[ -z "${CREDENTIALS}" ]]; then
+  CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account
+fi
+
+if [[ -z "${STAGING_BUCKET_V2}" ]]; then
+  echo "Need to set STAGING_BUCKET_V2 environment variable"
+  exit 1
+fi
+
+# work from the git root directory
+pushd $(dirname "$0")/../../
+
+# install docuploader package
+python3 -m pip install gcp-docuploader
+
+# compile all packages
+mvn clean install -B -q -DskipTests=true
+
+export NAME=google-cloud-spanner-jdbc
+export VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
+
+# V3 generates docfx yml from javadoc
+# generate yml
+mvn clean site -B -q -P docFX
+
+pushd target/docfx-yml
+
+# create metadata
+python3 -m docuploader create-metadata \
+ --name ${NAME} \
+ --version ${VERSION} \
+ --language java
+
+# upload yml
+python3 -m docuploader upload . \
+ --credentials ${CREDENTIALS} \
+ --staging-bucket ${STAGING_BUCKET_V2} \
+ --destination-prefix docfx-
diff --git a/LICENSE b/LICENSE
index d645695..261eeb9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,3 @@
-
                                  Apache License
                            Version 2.0, January 2004
                         http://www.apache.org/licenses/
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
index 67d8e0c..2a9d9e7 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
@@ -16,11 +16,21 @@
 
 package com.google.cloud.spanner.jdbc;
 
+import com.google.cloud.ByteArray;
+import com.google.cloud.spanner.ResultSets;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.ValueBinder;
+import com.google.common.collect.ImmutableList;
 import com.google.rpc.Code;
+import java.math.BigDecimal;
 import java.sql.Array;
+import java.sql.Date;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -137,28 +147,89 @@ class JdbcArray implements Array {
     return null;
   }
 
-  private static final String RESULTSET_NOT_SUPPORTED =
-      "Getting a ResultSet from an array is not supported";
+  private static final String RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED =
+      "Getting a ResultSet with a custom type mapping from an array is not supported";
 
   @Override
   public ResultSet getResultSet() throws SQLException {
-    throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
+    return getResultSet(1L, Integer.MAX_VALUE);
   }
 
   @Override
   public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
-    throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
+    throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
   }
 
   @Override
-  public ResultSet getResultSet(long index, int count) throws SQLException {
-    throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
+  public ResultSet getResultSet(long startIndex, int count) throws SQLException {
+    JdbcPreconditions.checkArgument(
+        startIndex + count - 1L <= Integer.MAX_VALUE,
+        String.format("End index cannot exceed %d", Integer.MAX_VALUE));
+    JdbcPreconditions.checkArgument(startIndex >= 1L, "Start index must be >= 1");
+    JdbcPreconditions.checkArgument(count >= 0, "Count must be >= 0");
+    checkFree();
+    ImmutableList.Builder<Struct> rows = ImmutableList.builder();
+    int added = 0;
+    if (data != null) {
+      // Note that array index in JDBC is base-one.
+      for (int index = (int) startIndex;
+          added < count && index <= ((Object[]) data).length;
+          index++) {
+        Object value = ((Object[]) data)[index - 1];
+        ValueBinder<Struct.Builder> binder =
+            Struct.newBuilder().set("INDEX").to(index).set("VALUE");
+        Struct.Builder builder = null;
+        switch (type.getCode()) {
+          case BOOL:
+            builder = binder.to((Boolean) value);
+            break;
+          case BYTES:
+            builder = binder.to(ByteArray.copyFrom((byte[]) value));
+            break;
+          case DATE:
+            builder = binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
+            break;
+          case FLOAT64:
+            builder = binder.to((Double) value);
+            break;
+          case INT64:
+            builder = binder.to((Long) value);
+            break;
+          case NUMERIC:
+            builder = binder.to((BigDecimal) value);
+            break;
+          case STRING:
+            builder = binder.to((String) value);
+            break;
+          case TIMESTAMP:
+            builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
+            break;
+          case ARRAY:
+          case STRUCT:
+          default:
+            throw new SQLFeatureNotSupportedException(
+                String.format(
+                    "Array of type %s cannot be converted to a ResultSet", type.getCode().name()));
+        }
+        rows.add(builder.build());
+        added++;
+        if (added == count) {
+          break;
+        }
+      }
+    }
+    return JdbcResultSet.of(
+        ResultSets.forRows(
+            Type.struct(
+                StructField.of("INDEX", Type.int64()),
+                StructField.of("VALUE", type.getSpannerType())),
+            rows.build()));
   }
 
   @Override
   public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map)
       throws SQLException {
-    throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
+    throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
   }
 
   @Override
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
index d2a2fea..f7214ea 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
@@ -67,7 +67,7 @@ import java.util.regex.Pattern;
  * The property-value strings should be url-encoded.
  *
  * <p>The project-id part of the URI may be filled with the placeholder DEFAULT_PROJECT_ID. This
- * placeholder will be replaced by the default project id of the environment that is requesting a
+ * placeholder is replaced by the default project id of the environment that is requesting a
  * connection.
  *
  * <p>The supported properties are:
@@ -75,7 +75,7 @@ import java.util.regex.Pattern;
  * <ul>
  *   <li>credentials (String): URL for the credentials file to use for the connection. If you do not
  *       specify any credentials at all, the default credentials of the environment as returned by
- *       {@link GoogleCredentials#getApplicationDefault()} will be used.
+ *       {@link GoogleCredentials#getApplicationDefault()} is used.
  *   <li>oauthtoken (String): A valid OAuth2 token to use for the JDBC connection. The token must
  *       have been obtained with one or both of the scopes
  *       'https://www.googleapis.com/auth/spanner.admin' and/or
@@ -89,8 +89,15 @@ import java.util.regex.Pattern;
  *       com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection#setRetryAbortsInternally(boolean)}
  *       for more information.
  *   <li>numChannels (int): Sets the number of gRPC channels to use. Defaults to 4.
- *   <li>usePlainText (boolean): Sets whether the JDBC connection should establish an unencrypted connection to the server. This option can only be used when connecting to a local emulator that does not require an encrypted connection, and that does not require authentication.
- *   <li>optimizerVersion (string): The query optimizer version to use for the connection. The value must be either a valid version number or <code>LATEST</code>. If no value is specified, the query optimizer version specified in the environment variable <code>SPANNER_OPTIMIZER_VERSION<code> will be used. If no query optimizer version is specified in the connection URL or in the environment variable, the default query optimizer version of Cloud Spanner will be used.
+ *   <li>usePlainText (boolean): Sets whether the JDBC connection should establish an unencrypted
+ *       connection to the server. This option can only be used when connecting to a local emulator
+ *       that does not require an encrypted connection, and that does not require authentication.
+ *   <li>optimizerVersion (string): The query optimizer version to use for the connection. The value
+ *       must be either a valid version number or <code>LATEST</code>. If no value is specified, the
+ *       query optimizer version specified in the environment variable <code>
+ *       SPANNER_OPTIMIZER_VERSION</code> is used. If no query optimizer version is specified in the
+ *       connection URL or in the environment variable, the default query optimizer version of Cloud
+ *       Spanner is used.
  * </ul>
  */
 public class JdbcDriver implements Driver {
diff --git a/synth.metadata b/synth.metadata
index e4e8ea4..6c9056a 100644
--- a/synth.metadata
+++ b/synth.metadata
@@ -4,14 +4,14 @@
       "git": {
         "name": ".",
         "remote": "https://github.com/googleapis/java-spanner-jdbc.git",
-        "sha": "7e5a13374ec6f343b2b2ea2320e8c12f895f3e87"
+        "sha": "81a844413be775520dcf15ba74938a6c1cf36442"
       }
     },
     {
       "git": {
         "name": "synthtool",
         "remote": "https://github.com/googleapis/synthtool.git",
-        "sha": "3816b080296d4d52975079fd26c110dd26ba25af"
+        "sha": "692715c0f23a7bb3bfbbaa300f7620ddfa8c47e5"
       }
     }
   ],
@@ -40,7 +40,6 @@
     ".kokoro/continuous/java8.cfg",
     ".kokoro/continuous/readme.cfg",
     ".kokoro/dependencies.sh",
-    ".kokoro/linkage-monitor.sh",
     ".kokoro/nightly/common.cfg",
     ".kokoro/nightly/integration.cfg",
     ".kokoro/nightly/java11.cfg",
@@ -72,6 +71,8 @@
     ".kokoro/release/promote.sh",
     ".kokoro/release/publish_javadoc.cfg",
     ".kokoro/release/publish_javadoc.sh",
+    ".kokoro/release/publish_javadoc11.cfg",
+    ".kokoro/release/publish_javadoc11.sh",
     ".kokoro/release/snapshot.cfg",
     ".kokoro/release/snapshot.sh",
     ".kokoro/release/stage.cfg",


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 01184c2..24e3e57 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
@@ -16,10 +16,16 @@
 
 package com.google.cloud.spanner.jdbc;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.toSqlDate;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
 
+import com.google.cloud.spanner.ErrorCode;
+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.Timestamp;
 import java.sql.Types;
@@ -35,42 +41,208 @@ public class JdbcArrayTest {
     // Note that JDBC array indices start at 1.
     JdbcArray array;
     array = JdbcArray.createArray("BOOL", new Boolean[] {true, false, true});
-    assertEquals(array.getBaseType(), Types.BOOLEAN);
-    assertEquals(((Boolean[]) array.getArray(1, 1))[0], Boolean.TRUE);
+    assertThat(array.getBaseType()).isEqualTo(Types.BOOLEAN);
+    assertThat(((Boolean[]) array.getArray(1, 1))[0]).isEqualTo(Boolean.TRUE);
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBoolean(2)).isEqualTo(true);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBoolean(2)).isEqualTo(false);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBoolean(2)).isEqualTo(true);
+      assertThat(rs.next()).isFalse();
+    }
 
     array = JdbcArray.createArray("BYTES", new byte[][] {new byte[] {1, 2}, new byte[] {3, 4}});
-    assertEquals(array.getBaseType(), Types.BINARY);
-    assertEquals(((byte[][]) array.getArray(1, 1))[0][1], (byte) 2);
+    assertThat(array.getBaseType()).isEqualTo(Types.BINARY);
+    assertThat(((byte[][]) array.getArray(1, 1))[0][1]).isEqualTo((byte) 2);
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBytes(2)).isEqualTo(new byte[] {1, 2});
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBytes(2)).isEqualTo(new byte[] {3, 4});
+      assertThat(rs.next()).isFalse();
+    }
 
     array =
-        JdbcArray.createArray("DATE", new Date[] {new Date(1L), new Date(100L), new Date(1000L)});
-    assertEquals(array.getBaseType(), Types.DATE);
-    assertEquals(((Date[]) array.getArray(1, 1))[0], new Date(1L));
+        JdbcArray.createArray(
+            "DATE",
+            new Date[] {
+              toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)),
+              toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)),
+              toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31))
+            });
+    assertThat(array.getBaseType()).isEqualTo(Types.DATE);
+    assertThat(((Date[]) array.getArray(1, 1))[0])
+        .isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDate(2))
+          .isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDate(2))
+          .isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)));
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDate(2))
+          .isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31)));
+      assertThat(rs.next()).isFalse();
+    }
 
     array = JdbcArray.createArray("FLOAT64", new Double[] {1.1D, 2.2D, Math.PI});
-    assertEquals(array.getBaseType(), Types.DOUBLE);
-    assertEquals(((Double[]) array.getArray(1, 3))[2], Double.valueOf(Math.PI));
+    assertThat(array.getBaseType()).isEqualTo(Types.DOUBLE);
+    assertThat(((Double[]) array.getArray(1, 3))[2]).isEqualTo(Double.valueOf(Math.PI));
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDouble(2)).isEqualTo(1.1D);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDouble(2)).isEqualTo(2.2D);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getDouble(2)).isEqualTo(Math.PI);
+      assertThat(rs.next()).isFalse();
+    }
 
     array = JdbcArray.createArray("INT64", new Long[] {1L, 2L, 3L});
-    assertEquals(array.getBaseType(), Types.BIGINT);
-    assertEquals(((Long[]) array.getArray(1, 1))[0], Long.valueOf(1L));
+    assertThat(array.getBaseType()).isEqualTo(Types.BIGINT);
+    assertThat(((Long[]) array.getArray(1, 1))[0]).isEqualTo(Long.valueOf(1L));
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getLong(2)).isEqualTo(1L);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getLong(2)).isEqualTo(2L);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getLong(2)).isEqualTo(3L);
+      assertThat(rs.next()).isFalse();
+    }
 
     array =
         JdbcArray.createArray("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN});
-    assertEquals(array.getBaseType(), Types.NUMERIC);
-    assertEquals(((BigDecimal[]) array.getArray(1, 1))[0], BigDecimal.ONE);
-    assertEquals(((BigDecimal[]) array.getArray(2, 1))[0], null);
-    assertEquals(((BigDecimal[]) array.getArray(3, 1))[0], BigDecimal.TEN);
+    assertThat(array.getBaseType()).isEqualTo(Types.NUMERIC);
+    assertThat(((BigDecimal[]) array.getArray(1, 1))[0]).isEqualTo(BigDecimal.ONE);
+    assertThat(((BigDecimal[]) array.getArray(2, 1))[0]).isNull();
+    assertThat(((BigDecimal[]) array.getArray(3, 1))[0]).isEqualTo(BigDecimal.TEN);
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.ONE);
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBigDecimal(2)).isNull();
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.TEN);
+      assertThat(rs.next()).isFalse();
+    }
 
     array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
-    assertEquals(array.getBaseType(), Types.NVARCHAR);
-    assertEquals(((String[]) array.getArray(1, 1))[0], "foo");
+    assertThat(array.getBaseType()).isEqualTo(Types.NVARCHAR);
+    assertThat(((String[]) array.getArray(1, 1))[0]).isEqualTo("foo");
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("foo");
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("bar");
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("baz");
+      assertThat(rs.next()).isFalse();
+    }
 
     array =
         JdbcArray.createArray(
             "TIMESTAMP",
             new Timestamp[] {new Timestamp(1L), new Timestamp(100L), new Timestamp(1000L)});
-    assertEquals(array.getBaseType(), Types.TIMESTAMP);
-    assertEquals(((Timestamp[]) array.getArray(1, 1))[0], new Timestamp(1L));
+    assertThat(array.getBaseType()).isEqualTo(Types.TIMESTAMP);
+    assertThat(((Timestamp[]) array.getArray(1, 1))[0]).isEqualTo(new Timestamp(1L));
+    try (ResultSet rs = array.getResultSet()) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1L));
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(100L));
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1000L));
+      assertThat(rs.next()).isFalse();
+    }
+  }
+
+  @Test
+  public void testCreateArrayOfArray() {
+    try {
+      JdbcArray.createArray("ARRAY<STRING>", new String[][] {{}});
+      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());
+    }
+  }
+
+  @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());
+    }
+  }
+
+  @Test
+  public void testGetResultSetMetadata() throws SQLException {
+    JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
+    try (ResultSet rs = array.getResultSet()) {
+      ResultSetMetaData metadata = rs.getMetaData();
+      assertThat(metadata.getColumnCount()).isEqualTo(2);
+      assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT);
+      assertThat(metadata.getColumnType(2)).isEqualTo(Types.NVARCHAR);
+      assertThat(metadata.getColumnName(1)).isEqualTo("INDEX");
+      assertThat(metadata.getColumnName(2)).isEqualTo("VALUE");
+    }
+  }
+
+  @Test
+  public void testGetResultSetWithIndex() throws SQLException {
+    JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
+    try (ResultSet rs = array.getResultSet(2L, 1)) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getLong("INDEX")).isEqualTo(2L);
+      assertThat(rs.getString("VALUE")).isEqualTo("bar");
+      assertThat(rs.next()).isFalse();
+    }
+
+    try (ResultSet rs = array.getResultSet(1L, 5)) {
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("foo");
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("bar");
+      assertThat(rs.next()).isTrue();
+      assertThat(rs.getString(2)).isEqualTo("baz");
+      assertThat(rs.next()).isFalse();
+    }
+
+    try (ResultSet rs = array.getResultSet(1L, 0)) {
+      assertThat(rs.next()).isFalse();
+    }
+  }
+
+  @Test
+  public void testGetResultSetWithInvalidIndex() throws SQLException {
+    JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
+    try (ResultSet rs = array.getResultSet(0L, 1)) {
+      fail("missing expected exception");
+    } catch (JdbcSqlExceptionImpl e) {
+      assertThat(e.getErrorCode())
+          .isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
+    }
+  }
+
+  @Test
+  public void testGetResultSetWithInvalidCount() throws SQLException {
+    JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
+    try (ResultSet rs = array.getResultSet(1L, -1)) {
+      fail("missing expected exception");
+    } catch (JdbcSqlExceptionImpl e) {
+      assertThat(e.getErrorCode())
+          .isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
+    }
   }
 }