content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to you 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.
   */
  package org.apache.calcite.avatica.remote;
  
  import org.apache.calcite.avatica.AvaticaUtils;
  import org.apache.calcite.avatica.ColumnMetaData;
  import org.apache.calcite.avatica.ColumnMetaData.ArrayType;
  import org.apache.calcite.avatica.ColumnMetaData.AvaticaType;
  import org.apache.calcite.avatica.ColumnMetaData.Rep;
  import org.apache.calcite.avatica.ColumnMetaData.ScalarType;
  import org.apache.calcite.avatica.SqlType;
  import org.apache.calcite.avatica.remote.Driver.Serialization;
  import org.apache.calcite.avatica.server.HttpServer;
  import org.apache.calcite.avatica.util.AbstractCursor.ArrayAccessor;
  import org.apache.calcite.avatica.util.ArrayImpl;
  import org.apache.calcite.avatica.util.Cursor.Accessor;
  import org.apache.calcite.avatica.util.ListIteratorCursor;
  import org.apache.calcite.avatica.util.Unsafe;
  
  import org.junit.AfterClass;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  import org.junit.runners.Parameterized;
  import org.junit.runners.Parameterized.Parameters;
  
  import java.sql.Array;
  import java.sql.Connection;
  import java.sql.Date;
  import java.sql.DriverManager;
  import java.sql.PreparedStatement;
  import java.sql.ResultSet;
  import java.sql.SQLException;
  import java.sql.Statement;
  import java.sql.Time;
  import java.sql.Timestamp;
  import java.sql.Types;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Calendar;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Random;
  
  import static org.junit.Assert.assertArrayEquals;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertNotNull;
  import static org.junit.Assert.assertTrue;
  import static org.junit.Assert.fail;
  
  import static java.nio.charset.StandardCharsets.UTF_8;
  
  /**
   * Test class for verifying functionality with arrays.
   */
  @RunWith(Parameterized.class)
  public class ArrayTypeTest {
    private static final AvaticaServersForTest SERVERS = new AvaticaServersForTest();
  
    private final HttpServer server;
    private final String url;
    private final int port;
    @SuppressWarnings("unused")
    private final Driver.Serialization serialization;
  
    @Parameters(name = "{0}")
    public static List<Object[]> parameters() throws Exception {
      SERVERS.startServers();
      return SERVERS.getJUnitParameters();
    }
  
    public ArrayTypeTest(Serialization serialization, HttpServer server) {
      this.server = server;
      this.port = this.server.getPort();
      this.serialization = serialization;
      this.url = SERVERS.getJdbcUrl(port, serialization);
    }
  
    @AfterClass public static void afterClass() throws Exception {
      SERVERS.stopServers();
    }
  
    @Test public void simpleArrayTest() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType varcharComponent = ColumnMetaData.scalar(Types.VARCHAR, "VARCHAR", Rep.STRING);
        List<Array> varcharArrays = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
          List<String> value = Collections.singletonList(Integer.toString(i));
          varcharArrays.add(createArray("VARCHAR", varcharComponent, value));
        }
        writeAndReadArrays(conn, "varchar_arrays", "VARCHAR(30)",
            varcharComponent, varcharArrays, PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void booleanArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.BOOLEAN, "BOOLEAN", Rep.BOOLEAN);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Boolean> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            switch (r.nextInt(3)) {
            case 0:
              elements.add(Boolean.FALSE);
              break;
            case 1:
              elements.add(Boolean.TRUE);
              break;
            case 2:
              elements.add(null);
              break;
            default:
              fail();
            }
          }
          arrays.add(createArray("BOOLEAN", component, elements));
        }
        // Verify we can read and write the data
        writeAndReadArrays(conn, "boolean_arrays", "BOOLEAN", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void shortArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.SMALLINT, "SMALLINT", Rep.SHORT);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Short> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            short value = (short) r.nextInt(Short.MAX_VALUE);
            // 50% of the time, negate the value
            if (0 == r.nextInt(2)) {
              value *= -1;
            }
            elements.add(value);
          }
          arrays.add(createArray("SMALLINT", component, elements));
        }
        // Verify read/write
        writeAndReadArrays(conn, "short_arrays", "SMALLINT", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void shortArraysWithNull() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.SMALLINT, "SMALLINT", Rep.SHORT);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Short> elements = new ArrayList<>();
          for (int j = 0; j < 4; j++) {
            short value = (short) r.nextInt(Short.MAX_VALUE);
            // 50% of the time, negate the value
            if (0 == r.nextInt(2)) {
              value *= -1;
            }
            elements.add(value);
          }
          elements.add(null);
          arrays.add(createArray("SMALLINT", component, elements));
        }
        // Verify read/write
        writeAndReadArrays(conn, "short_arrays", "SMALLINT", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void longArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.BIGINT, "BIGINT", Rep.LONG);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Long> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(r.nextLong());
          }
          arrays.add(createArray("BIGINT", component, elements));
        }
        // Verify read/write
        writeAndReadArrays(conn, "long_arrays", "BIGINT", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void stringArrays() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.VARCHAR, "VARCHAR", Rep.STRING);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<String> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(i + "_" + j);
          }
          arrays.add(createArray("VARCHAR", component, elements));
        }
        // Verify read/write
        writeAndReadArrays(conn, "string_arrays", "VARCHAR", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void bigintArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.BIGINT, "BIGINT", Rep.LONG);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 3; i++) {
          List<Long> elements = new ArrayList<>();
          for (int j = 0; j < 7; j++) {
            long element = r.nextLong();
            if (r.nextBoolean()) {
              element *= -1;
            }
            elements.add(element);
          }
          arrays.add(createArray("BIGINT", component, elements));
        }
        writeAndReadArrays(conn, "long_arrays", "BIGINT", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void doubleArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.DOUBLE, "DOUBLE", Rep.DOUBLE);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 3; i++) {
          List<Double> elements = new ArrayList<>();
          for (int j = 0; j < 7; j++) {
            double element = r.nextDouble();
            if (r.nextBoolean()) {
              element *= -1;
            }
            elements.add(element);
          }
          arrays.add(createArray("DOUBLE", component, elements));
        }
        writeAndReadArrays(conn, "double_arrays", "DOUBLE", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void realArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.REAL, "REAL", Rep.FLOAT);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 3; i++) {
          List<Float> elements = new ArrayList<>();
          for (int j = 0; j < 7; j++) {
            float element = r.nextFloat();
            if (r.nextBoolean()) {
              element *= -1;
            }
            elements.add(element);
          }
          arrays.add(createArray("REAL", component, elements));
        }
        writeAndReadArrays(conn, "real_arrays", "REAL", component, arrays,
            (expected, actual) -> {
              // 'Real' maps to 'Float' following the JDBC specs, but hsqldb maps 'Double', 'Real'
              // and 'Float' to Java 'double'
              double[] expectedArray = Arrays.stream((Object[]) expected.getArray())
                  .mapToDouble(x -> ((Float) x).doubleValue()).toArray();
              double[] actualArray = Arrays.stream((Object[]) actual.getArray())
                  .mapToDouble(x -> (double) x).toArray();
              assertArrayEquals(expectedArray, actualArray, Double.MIN_VALUE);
            });
      }
    }
  
    @Test public void floatArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.FLOAT, "FLOAT", Rep.FLOAT);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 3; i++) {
          List<Double> elements = new ArrayList<>();
          for (int j = 0; j < 7; j++) {
            double element = r.nextFloat();
            if (r.nextBoolean()) {
              element *= -1;
            }
            elements.add(element);
          }
          arrays.add(createArray("FLOAT", component, elements));
        }
        writeAndReadArrays(conn, "float_arrays", "FLOAT", component, arrays,
            PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void arraysOfByteArrays() throws Exception {
      final Random r = new Random();
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.TINYINT, "TINYINT", Rep.BYTE);
        // [ Array([b, b, b]), Array([b, b, b]), ... ]
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Byte> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            byte value = (byte) r.nextInt(Byte.MAX_VALUE);
            // 50% of the time, negate the value
            if (0 == r.nextInt(2)) {
              value *= -1;
            }
            elements.add(value);
          }
          arrays.add(createArray("TINYINT", component, elements));
        }
        // Verify read/write
        writeAndReadArrays(conn, "byte_arrays", "TINYINT", component, arrays,
            BYTE_ARRAY_VALIDATOR);
      }
    }
  
    @Test public void varbinaryArrays() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.VARBINARY, "VARBINARY", Rep.BYTE_STRING);
        // [ Array(binary, binary, binary), Array(binary, binary, binary), ...]
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<byte[]> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add((i + "_" + j).getBytes(UTF_8));
          }
          arrays.add(createArray("VARBINARY", component, elements));
        }
        writeAndReadArrays(conn, "binary_arrays", "VARBINARY", component, arrays,
            BYTE_ARRAY_ARRAY_VALIDATOR);
      }
    }
  
    @Test public void timeArrays() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        final long now = System.currentTimeMillis();
        ScalarType component = ColumnMetaData.scalar(Types.TIME, "TIME", Rep.JAVA_SQL_TIME);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Time> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(new Time(now + i + j));
          }
          arrays.add(createArray("TIME", component, elements));
        }
        writeAndReadArrays(conn, "time_arrays", "TIME", component, arrays,
            (expected, actual) -> {
              Object[] expectedTimes = (Object[]) expected.getArray();
              Object[] actualTimes = (Object[]) actual.getArray();
              assertEquals(expectedTimes.length, actualTimes.length);
              final Calendar cal = Unsafe.localCalendar();
              for (int i = 0;  i < expectedTimes.length; i++) {
                cal.setTime((Time) expectedTimes[i]);
                int expectedHour = cal.get(Calendar.HOUR_OF_DAY);
                int expectedMinute = cal.get(Calendar.MINUTE);
                int expectedSecond = cal.get(Calendar.SECOND);
                cal.setTime((Time) actualTimes[i]);
                assertEquals(expectedHour, cal.get(Calendar.HOUR_OF_DAY));
                assertEquals(expectedMinute, cal.get(Calendar.MINUTE));
                assertEquals(expectedSecond, cal.get(Calendar.SECOND));
              }
            });
        // Ensure an array with a null element can be written/read
        Array arrayWithNull = createArray("TIME", component, Arrays.asList((Time) null));
        writeAndReadArrays(conn, "time_array_with_null", "TIME", component,
            Collections.singletonList(arrayWithNull), (expected, actual) -> {
              Object[] expectedArray = (Object[]) expected.getArray();
              Object[] actualArray = (Object[]) actual.getArray();
              assertEquals(1, expectedArray.length);
              assertEquals(expectedArray.length, actualArray.length);
              assertEquals(expectedArray[0], actualArray[0]);
            });
      }
    }
  
    @Test public void dateArrays() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        final long now = System.currentTimeMillis();
        ScalarType component = ColumnMetaData.scalar(Types.DATE, "DATE", Rep.JAVA_SQL_DATE);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Date> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(new Date(now + i + j));
          }
          arrays.add(createArray("DATE", component, elements));
        }
        writeAndReadArrays(conn, "date_arrays", "DATE", component, arrays,
            (expected, actual) -> {
              Object[] expectedDates = (Object[]) expected.getArray();
              Object[] actualDates = (Object[]) actual.getArray();
              assertEquals(expectedDates.length, actualDates.length);
              final Calendar cal = Unsafe.localCalendar();
              for (int i = 0;  i < expectedDates.length; i++) {
                cal.setTime((Date) expectedDates[i]);
                int expectedDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
                int expectedMonth = cal.get(Calendar.MONTH);
                int expectedYear = cal.get(Calendar.YEAR);
                cal.setTime((Date) actualDates[i]);
                assertEquals(expectedDayOfMonth, cal.get(Calendar.DAY_OF_MONTH));
                assertEquals(expectedMonth, cal.get(Calendar.MONTH));
                assertEquals(expectedYear, cal.get(Calendar.YEAR));
              }
            });
        // Ensure an array with a null element can be written/read
        Array arrayWithNull = createArray("DATE", component, Arrays.asList((Time) null));
        writeAndReadArrays(conn, "date_array_with_null", "DATE", component,
            Collections.singletonList(arrayWithNull), (expected, actual) -> {
              Object[] expectedArray = (Object[]) expected.getArray();
              Object[] actualArray = (Object[]) actual.getArray();
              assertEquals(1, expectedArray.length);
              assertEquals(expectedArray.length, actualArray.length);
              assertEquals(expectedArray[0], actualArray[0]);
            });
      }
    }
  
    @Test public void timestampArrays() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        final long now = System.currentTimeMillis();
        ScalarType component = ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP",
            Rep.JAVA_SQL_TIMESTAMP);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<Timestamp> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(new Timestamp(now + i + j));
          }
          arrays.add(createArray("TIMESTAMP", component, elements));
        }
        writeAndReadArrays(conn, "timestamp_arrays", "TIMESTAMP", component, arrays,
            (expected, actual) -> {
              Object[] expectedTimestamps = (Object[]) expected.getArray();
              Object[] actualTimestamps = (Object[]) actual.getArray();
              assertEquals(expectedTimestamps.length, actualTimestamps.length);
              final Calendar cal = Unsafe.localCalendar();
              for (int i = 0;  i < expectedTimestamps.length; i++) {
                cal.setTime((Timestamp) expectedTimestamps[i]);
                int expectedDayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
                int expectedMonth = cal.get(Calendar.MONTH);
                int expectedYear = cal.get(Calendar.YEAR);
                int expectedHour = cal.get(Calendar.HOUR_OF_DAY);
                int expectedMinute = cal.get(Calendar.MINUTE);
                int expectedSecond = cal.get(Calendar.SECOND);
                int expectedMillisecond = cal.get(Calendar.MILLISECOND);
                cal.setTime((Timestamp) actualTimestamps[i]);
                assertEquals(expectedDayOfMonth, cal.get(Calendar.DAY_OF_MONTH));
                assertEquals(expectedMonth, cal.get(Calendar.MONTH));
                assertEquals(expectedYear, cal.get(Calendar.YEAR));
                assertEquals(expectedHour, cal.get(Calendar.HOUR_OF_DAY));
                assertEquals(expectedMinute, cal.get(Calendar.MINUTE));
                assertEquals(expectedSecond, cal.get(Calendar.SECOND));
                assertEquals(expectedMillisecond, cal.get(Calendar.MILLISECOND));
              }
            });
        // Ensure an array with a null element can be written/read
        Array arrayWithNull = createArray("TIMESTAMP", component, Arrays.asList((Timestamp) null));
        writeAndReadArrays(conn, "timestamp_array_with_null", "TIMESTAMP", component,
            Collections.singletonList(arrayWithNull), (expected, actual) -> {
              Object[] expectedArray = (Object[]) expected.getArray();
              Object[] actualArray = (Object[]) actual.getArray();
              assertEquals(1, expectedArray.length);
              assertEquals(expectedArray.length, actualArray.length);
              assertEquals(expectedArray[0], actualArray[0]);
            });
      }
    }
  
    @Test public void testCreateArrayOf() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        final String componentName = SqlType.INTEGER.name();
        Array a1 = conn.createArrayOf(componentName, new Object[] {1, 2, 3, 4, 5});
        Array a2 = conn.createArrayOf(componentName, new Object[] {2, 3, 4, 5, 6});
        Array a3 = conn.createArrayOf(componentName, new Object[] {3, 4, 5, 6, 7});
        AvaticaType arrayType = ColumnMetaData.array(
            ColumnMetaData.scalar(Types.INTEGER, componentName, Rep.INTEGER), "NUMBERS", Rep.ARRAY);
        writeAndReadArrays(conn, "CREATE_ARRAY_OF_INTEGERS", componentName, arrayType,
            Arrays.asList(a1, a2, a3), PRIMITIVE_LIST_VALIDATOR);
      }
    }
  
    @Test public void testBatchInsert() throws Exception {
      try (Connection conn = DriverManager.getConnection(url)) {
        ScalarType component = ColumnMetaData.scalar(Types.VARCHAR, "VARCHAR", Rep.STRING);
        List<Array> arrays = new ArrayList<>();
        // Construct the data
        for (int i = 0; i < 5; i++) {
          List<String> elements = new ArrayList<>();
          for (int j = 0; j < 5; j++) {
            elements.add(i + "_" + j);
          }
          arrays.add(createArray("VARCHAR", component, elements));
        }
  
        String tableName = "test_batch_insert";
        // Drop and create the table
        try (Statement stmt = conn.createStatement()) {
          assertFalse(stmt.execute(Unsafe.formatLocalString("DROP TABLE IF EXISTS %s", tableName)));
          String createTableSql = Unsafe.formatLocalString(
                  "CREATE TABLE %s (id integer, vals %s ARRAY)", tableName, "VARCHAR");
          assertFalse(stmt.execute(createTableSql));
        }
  
        // Insert records, each with an array
        final String dml = Unsafe.formatLocalString("INSERT INTO %s VALUES (?, ?)", tableName);
        try (PreparedStatement stmt = conn.prepareStatement(dml)) {
          int i = 0;
          for (Array inputArray : arrays)  {
            stmt.setInt(1, i);
            stmt.setArray(2, inputArray);
            stmt.addBatch();
            i++;
          }
          assertEquals(i, stmt.executeBatch().length);
        }
  
        // Read the records
        try (Statement stmt = conn.createStatement()) {
          ResultSet results = stmt.executeQuery(
                  Unsafe.formatLocalString("SELECT * FROM %s", tableName));
          assertNotNull("Expected a ResultSet", results);
          int i = 0;
          for (Array expectedArray : arrays) {
            assertTrue(results.next());
            assertEquals(i++, results.getInt(1));
            Array actualArray = results.getArray(2);
  
            PRIMITIVE_LIST_VALIDATOR.validate(expectedArray, actualArray);
          }
          assertFalse("Expected no more records", results.next());
        }
      }
    }
  
    /**
     * Creates a JDBC {@link Array} from a list of values.
     *
     * @param typeName the SQL type name of the elements in the array
     * @param componentType The Avatica type for the array elements
     * @param arrayValues The array elements
     * @return An Array instance for the given component and values
     */
    @SuppressWarnings("unchecked")
    private <T> Array createArray(String typeName, AvaticaType componentType, List<T> arrayValues) {
      // Make a "row" with one "column" (which is really a list)
      final List<Object> oneRow = Collections.singletonList((Object) arrayValues);
      // Make an iterator over this one "row"
      final Iterator<List<Object>> rowIterator = Collections.singletonList(oneRow).iterator();
  
      ArrayType array = ColumnMetaData.array(componentType, typeName, Rep.ARRAY);
      try (ListIteratorCursor cursor = new ListIteratorCursor(rowIterator)) {
        List<ColumnMetaData> types = Collections.singletonList(ColumnMetaData.dummy(array, true));
        Calendar calendar = Unsafe.localCalendar();
        List<Accessor> accessors = cursor.createAccessors(types, calendar, null);
        assertFalse("Expected at least one accessor, found " + accessors.size(),
            accessors.isEmpty());
        ArrayAccessor arrayAccessor = (ArrayAccessor) accessors.get(0);
  
        return new ArrayImpl((List<Object>) arrayValues, arrayAccessor);
      }
    }
  
    /**
     * Creates a table, writes the arrays to the table, and then verifies that the arrays can be
     * read from that table and are equivalent to the original arrays.
     *
     * @param conn The JDBC connection
     * @param tableName The name of the table to create and use
     * @param componentType The component type of the array
     * @param scalarType The Avatica type object for the component type of the array
     * @param inputArrays The data to write and read
     */
    private void writeAndReadArrays(Connection conn, String tableName, String componentType,
        AvaticaType scalarType, List<Array> inputArrays, Validator<Array> validator)
        throws Exception {
      // Drop and create the table
      try (Statement stmt = conn.createStatement()) {
        assertFalse(stmt.execute(Unsafe.formatLocalString("DROP TABLE IF EXISTS %s", tableName)));
        String createTableSql = Unsafe.formatLocalString(
            "CREATE TABLE %s (id integer, vals %s ARRAY)", tableName, componentType);
        assertFalse(stmt.execute(createTableSql));
      }
  
      // Insert records, each with an array
      final String dml = Unsafe.formatLocalString("INSERT INTO %s VALUES (?, ?)", tableName);
      try (PreparedStatement stmt = conn.prepareStatement(dml)) {
        int i = 0;
        for (Array inputArray : inputArrays)  {
          stmt.setInt(1, i);
          stmt.setArray(2, inputArray);
          assertEquals(1, stmt.executeUpdate());
          i++;
        }
      }
  
      // Read the records
      try (Statement stmt = conn.createStatement()) {
        ResultSet results = stmt.executeQuery(
            Unsafe.formatLocalString("SELECT * FROM %s", tableName));
        assertNotNull("Expected a ResultSet", results);
        int i = 0;
        for (Array expectedArray : inputArrays) {
          assertTrue(results.next());
          assertEquals(i++, results.getInt(1));
          Array actualArray = results.getArray(2);
  
          validator.validate(expectedArray, actualArray);
  
          // TODO Fix this. See {@link AvaticaResultSet#create(ColumnMetaData.AvaticaType,Iterable)}
          //ResultSet inputResults = expectedArray.getResultSet();
          //ResultSet actualResult = actualArray.getResultSet();
        }
        assertFalse("Expected no more records", results.next());
      }
    }
  
    /**
     * A simple interface to validate to objects in support of type test cases
     * @param <T> the type of element to be validated
     */
    private interface Validator<T> {
      void validate(T expected, T actual) throws Exception;
    }
  
    private static final PrimitiveArrayValidator PRIMITIVE_LIST_VALIDATOR =
        new PrimitiveArrayValidator();
    /**
     * Validator that coerces primitive arrays into lists and compare them.
     */
    private static class PrimitiveArrayValidator implements Validator<Array> {
      @Override public void validate(Array expected, Array actual) throws SQLException {
        assertEquals(AvaticaUtils.primitiveList(expected.getArray()),
            AvaticaUtils.primitiveList(actual.getArray()));
      }
    }
  
    private static final ByteArrayValidator BYTE_ARRAY_VALIDATOR = new ByteArrayValidator();
    /**
     * Validator that compares lists of bytes (the object).
     */
    private static class ByteArrayValidator implements Validator<Array> {
      @SuppressWarnings("unchecked")
      @Override public void validate(Array expected, Array actual) throws SQLException {
        // Need to compare the byte arrays.
        List<Byte> expectedArray =
            (List<Byte>) AvaticaUtils.primitiveList(expected.getArray());
        List<Byte> actualArray =
            (List<Byte>) AvaticaUtils.primitiveList(actual.getArray());
        assertEquals(expectedArray.size(), actualArray.size());
  
        for (int j = 0; j < expectedArray.size(); j++) {
          Byte expectedByte = expectedArray.get(j);
          Byte actualByte = actualArray.get(j);
          assertEquals(expectedByte, actualByte);
        }
      }
    }
  
    // Arrays of byte arrays (e.g. an Array<Varbinary>)
    private static final ByteArrayArrayValidator BYTE_ARRAY_ARRAY_VALIDATOR =
        new ByteArrayArrayValidator();
    /**
     * Validator that compares lists of byte arrays.
     */
    private static class ByteArrayArrayValidator implements Validator<Array> {
      @SuppressWarnings("unchecked")
      @Override public void validate(Array expected, Array actual) throws SQLException {
        // Need to compare the byte arrays.
        List<byte[]> expectedArray =
            (List<byte[]>) AvaticaUtils.primitiveList(expected.getArray());
        List<byte[]> actualArray =
            (List<byte[]>) AvaticaUtils.primitiveList(actual.getArray());
        assertEquals(expectedArray.size(), actualArray.size());
  
        for (int j = 0; j < expectedArray.size(); j++) {
          byte[] expectedBytes = expectedArray.get(j);
          byte[] actualBytes = actualArray.get(j);
          assertArrayEquals(expectedBytes, actualBytes);
        }
      }
    }
  }
  
  // End ArrayTypeTest.java
  