http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java index 77739ab..ba6a669 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java @@ -177,9 +177,9 @@ public abstract class AbstractCursor implements Cursor { (ColumnMetaData.ArrayType) columnMetaData.type; final SlotGetter componentGetter = new SlotGetter(); final Accessor componentAccessor = - createAccessor(ColumnMetaData.dummy(arrayType.component, true), + createAccessor(ColumnMetaData.dummy(arrayType.getComponent(), true), componentGetter, localCalendar, factory); - return new ArrayAccessor(getter, arrayType.component, componentAccessor, + return new ArrayAccessor(getter, arrayType.getComponent(), componentAccessor, componentGetter, factory); case Types.STRUCT: switch (columnMetaData.type.rep) { @@ -201,14 +201,14 @@ public abstract class AbstractCursor implements Cursor { } case Types.JAVA_OBJECT: case Types.OTHER: // e.g. map - if (columnMetaData.type.name.startsWith("INTERVAL_")) { - int end = columnMetaData.type.name.indexOf("("); + if (columnMetaData.type.getName().startsWith("INTERVAL_")) { + int end = columnMetaData.type.getName().indexOf("("); if (end < 0) { - end = columnMetaData.type.name.length(); + end = columnMetaData.type.getName().length(); } TimeUnitRange range = TimeUnitRange.valueOf( - columnMetaData.type.name.substring("INTERVAL_".length(), end)); + columnMetaData.type.getName().substring("INTERVAL_".length(), end)); if (range.monthly()) { return new IntervalYearMonthAccessor(getter, range); } else { @@ -480,8 +480,13 @@ public abstract class AbstractCursor implements Cursor { } public byte getByte() throws SQLException { - Byte o = (Byte) getObject(); - return o == null ? 0 : o; + Object obj = getObject(); + if (null == obj) { + return 0; + } else if (obj instanceof Integer) { + return ((Integer) obj).byteValue(); + } + return (Byte) obj; } public long getLong() throws SQLException { @@ -499,8 +504,13 @@ public abstract class AbstractCursor implements Cursor { } public short getShort() throws SQLException { - Short o = (Short) getObject(); - return o == null ? 0 : o; + Object obj = getObject(); + if (null == obj) { + return 0; + } else if (obj instanceof Integer) { + return ((Integer) obj).shortValue(); + } + return (Short) obj; } public long getLong() throws SQLException { @@ -603,8 +613,13 @@ public abstract class AbstractCursor implements Cursor { } public double getDouble() throws SQLException { - Double o = (Double) getObject(); - return o == null ? 0d : o; + Object obj = getObject(); + if (null == obj) { + return 0d; + } else if (obj instanceof BigDecimal) { + return ((BigDecimal) obj).doubleValue(); + } + return (Double) obj; } } @@ -725,7 +740,11 @@ public abstract class AbstractCursor implements Cursor { } public String getString() throws SQLException { - return (String) getObject(); + final Object obj = getObject(); + if (obj instanceof String) { + return (String) obj; + } + return null == obj ? null : obj.toString(); } @Override public byte[] getBytes() throws SQLException { @@ -792,8 +811,10 @@ public abstract class AbstractCursor implements Cursor { if (obj instanceof ByteString) { return ((ByteString) obj).getBytes(); } else if (obj instanceof String) { - return ((String) obj).getBytes(StandardCharsets.UTF_8); + // Need to unwind the base64 for JSON + return ByteString.parseBase64((String) obj); } else if (obj instanceof byte[]) { + // Protobuf would have a byte array return (byte[]) obj; } else { throw new RuntimeException("Cannot handle " + obj.getClass() + " as bytes"); @@ -1235,7 +1256,7 @@ public abstract class AbstractCursor implements Cursor { * Accessor that assumes that the underlying value is an ARRAY; * corresponds to {@link java.sql.Types#ARRAY}. */ - static class ArrayAccessor extends AccessorImpl { + public static class ArrayAccessor extends AccessorImpl { final ColumnMetaData.AvaticaType componentType; final Accessor componentAccessor; final SlotGetter componentSlotGetter; @@ -1253,20 +1274,80 @@ public abstract class AbstractCursor implements Cursor { @Override public Object getObject() throws SQLException { final Object object = super.getObject(); - if (object == null || object instanceof List) { + if (object == null || object instanceof ArrayImpl) { return object; + } else if (object instanceof List) { + List<?> list = (List<?>) object; + // Run the array values through the component accessor + List<Object> convertedValues = new ArrayList<>(list.size()); + for (Object val : list) { + if (null == val) { + convertedValues.add(null); + } else { + // Set the current value in the SlotGetter so we can use the Accessor to coerce it. + componentSlotGetter.slot = val; + convertedValues.add(convertValue()); + } + } + return convertedValues; } - // The object can be java array in case of user-provided class for row - // storage. + // The object can be java array in case of user-provided class for row storage. return AvaticaUtils.primitiveList(object); } - @Override public Array getArray() throws SQLException { - final List list = (List) getObject(); - if (list == null) { + private Object convertValue() throws SQLException { + switch (componentType.id) { + case Types.BOOLEAN: + case Types.BIT: + return componentAccessor.getBoolean(); + case Types.TINYINT: + return componentAccessor.getByte(); + case Types.SMALLINT: + return componentAccessor.getShort(); + case Types.INTEGER: + return componentAccessor.getInt(); + case Types.BIGINT: + return componentAccessor.getLong(); + case Types.FLOAT: + return componentAccessor.getFloat(); + case Types.DOUBLE: + return componentAccessor.getDouble(); + case Types.ARRAY: + return componentAccessor.getArray(); + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.NCHAR: + case Types.LONGNVARCHAR: + return componentAccessor.getString(); + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return componentAccessor.getBytes(); + case Types.DECIMAL: + return componentAccessor.getBigDecimal(); + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + case Types.STRUCT: + case Types.JAVA_OBJECT: + return componentAccessor.getObject(); + default: + throw new IllegalStateException("Unhandled ARRAY component type: " + componentType.rep + + ", id: " + componentType.id); + } + } + + @SuppressWarnings("unchecked") @Override public Array getArray() throws SQLException { + final Object o = getObject(); + if (o == null) { return null; } - return new ArrayImpl(list, this); + if (o instanceof ArrayImpl) { + return (ArrayImpl) o; + } + // If it's not an Array already, assume it is a List. + return new ArrayImpl((List<Object>) o, this); } @Override public String getString() throws SQLException { @@ -1291,10 +1372,22 @@ public abstract class AbstractCursor implements Cursor { return getStruct(); } + @SuppressWarnings("unchecked") + @Override public <T> T getObject(Class<T> clz) throws SQLException { + // getStruct() is not exposed on Accessor, only AccessorImpl. getObject(Class) is exposed, + // so we can make it do the right thing (call getStruct()). + if (clz.equals(Struct.class)) { + return (T) getStruct(); + } + return super.getObject(clz); + } + @Override public Struct getStruct() throws SQLException { final Object o = super.getObject(); if (o == null) { return null; + } else if (o instanceof StructImpl) { + return (StructImpl) o; } else if (o instanceof List) { return new StructImpl((List) o); } else {
http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/java/org/apache/calcite/avatica/util/ArrayFactoryImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/ArrayFactoryImpl.java b/core/src/main/java/org/apache/calcite/avatica/util/ArrayFactoryImpl.java new file mode 100644 index 0000000..c90e999 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/util/ArrayFactoryImpl.java @@ -0,0 +1,142 @@ +/* + * 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.util; + +import org.apache.calcite.avatica.AvaticaParameter; +import org.apache.calcite.avatica.AvaticaResultSet; +import org.apache.calcite.avatica.AvaticaResultSetMetaData; +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.Meta; +import org.apache.calcite.avatica.QueryState; +import org.apache.calcite.avatica.util.AbstractCursor.ArrayAccessor; +import org.apache.calcite.avatica.util.Cursor.Accessor; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.TimeZone; + +/** + * Implementation of {@link ArrayImpl.Factory}. + */ +public class ArrayFactoryImpl implements ArrayImpl.Factory { + private TimeZone timeZone; + + public ArrayFactoryImpl(TimeZone timeZone) { + this.timeZone = Objects.requireNonNull(timeZone); + } + + @Override public ResultSet create(AvaticaType elementType, Iterable<Object> elements) { + // The ColumnMetaData for offset "1" in the ResultSet for an Array. + ScalarType arrayOffsetType = ColumnMetaData.scalar(Types.INTEGER, "INTEGER", Rep.PRIMITIVE_INT); + // Two columns (types) in the ResultSet we will create + List<ColumnMetaData> types = Arrays.asList(ColumnMetaData.dummy(arrayOffsetType, false), + ColumnMetaData.dummy(elementType, true)); + List<List<Object>> rows = createResultSetRowsForArrayData(elements); + // `(List<Object>) rows` is a compile error. + @SuppressWarnings({ "unchecked", "rawtypes" }) + List<Object> untypedRows = (List<Object>) ((List) rows); + try (ListIteratorCursor cursor = new ListIteratorCursor(rows.iterator())) { + final String sql = "MOCKED"; + QueryState state = new QueryState(sql); + Meta.Signature signature = new Meta.Signature(types, sql, + Collections.<AvaticaParameter>emptyList(), Collections.<String, Object>emptyMap(), + Meta.CursorFactory.LIST, Meta.StatementType.SELECT); + AvaticaResultSetMetaData resultSetMetaData = new AvaticaResultSetMetaData(null, sql, + signature); + Meta.Frame frame = new Meta.Frame(0, true, untypedRows); + AvaticaResultSet resultSet = new AvaticaResultSet(null, state, signature, resultSetMetaData, + timeZone, frame); + resultSet.execute2(cursor, types); + return resultSet; + } + } + + @Override public Array createArray(AvaticaType elementType, Iterable<Object> elements) { + final ArrayType array = ColumnMetaData.array(elementType, elementType.name, Rep.ARRAY); + final List<ColumnMetaData> types = Collections.singletonList(ColumnMetaData.dummy(array, true)); + // Avoid creating a new List if we already have a List + List<Object> elementList; + if (elements instanceof List) { + elementList = (List<Object>) elements; + } else { + elementList = new ArrayList<>(); + for (Object element : elements) { + elementList.add(element); + } + } + try (ListIteratorCursor cursor = new ListIteratorCursor(createRowForArrayData(elementList))) { + List<Accessor> accessor = cursor.createAccessors(types, Unsafe.localCalendar(), this); + assert 1 == accessor.size(); + return new ArrayImpl(elementList, (ArrayAccessor) accessor.get(0)); + } + } + + /** + * Creates the row-level view over the values that will make up an Array. The Iterator has a row + * per Array element, each row containing two columns. The second column is the array element and + * the first column is the offset into the array of that array element (one-based, not zero-based) + * + * The ordering of the rows is not guaranteed to be in the same order as the array elements. + * + * A list of {@code elements}: + * <pre>[1, 2, 3]</pre> + * might be converted into + * <pre>Iterator{ [1, 1], [2, 2], [3, 3] }</pre> + * + * @param elements The elements of an array. + */ + private List<List<Object>> createResultSetRowsForArrayData(Iterable<Object> elements) { + List<List<Object>> rows = new ArrayList<>(); + int i = 0; + for (Object element : elements) { + rows.add(Arrays.asList(i + 1, element)); + i++; + } + return rows; + } + + /** + * Creates an row-level view over the values that will make up an Array. The Iterator has one + * entry which has a list that also has one entry. + * + * A provided list of {@code elements} + * <pre>[1, 2, 3]</pre> + * would be converted into + * <pre>Iterator{ [ [1,2,3] ] }</pre> + * + * @param elements The elements of an array + */ + private Iterator<List<Object>> createRowForArrayData(List<Object> elements) { + // Make a "row" with one "column" (which is really a list) + final List<Object> row = Collections.singletonList((Object) elements); + // Make an iterator over this one "row" + return Collections.singletonList(row).iterator(); + } +} + +// End ArrayFactoryImpl.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java b/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java index b2d5ae9..e57fde8 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java @@ -27,18 +27,19 @@ import java.util.Iterator; import java.util.List; import java.util.Map; + /** Implementation of JDBC {@link Array}. */ public class ArrayImpl implements Array { - private final List list; + private final List<Object> list; private final AbstractCursor.ArrayAccessor accessor; - public ArrayImpl(List list, AbstractCursor.ArrayAccessor accessor) { + public ArrayImpl(List<Object> list, AbstractCursor.ArrayAccessor accessor) { this.list = list; this.accessor = accessor; } public String getBaseTypeName() throws SQLException { - return accessor.componentType.name; + return accessor.componentType.getName(); } public int getBaseType() throws SQLException { @@ -46,11 +47,11 @@ public class ArrayImpl implements Array { } public Object getArray() throws SQLException { - return getArray(list); + return getArray(list, accessor); } @Override public String toString() { - final Iterator iterator = list.iterator(); + final Iterator<?> iterator = list.iterator(); if (!iterator.hasNext()) { return "[]"; } @@ -93,9 +94,10 @@ public class ArrayImpl implements Array { * @throws NullPointerException if any element is null */ @SuppressWarnings("unchecked") - protected Object getArray(List list) throws SQLException { + protected Object getArray(List<?> list, AbstractCursor.ArrayAccessor arrayAccessor) + throws SQLException { int i = 0; - switch (accessor.componentType.rep) { + switch (arrayAccessor.componentType.rep) { case PRIMITIVE_DOUBLE: final double[] doubles = new double[list.size()]; for (double v : (List<Double>) list) { @@ -148,56 +150,96 @@ public class ArrayImpl implements Array { // fall through } final Object[] objects = list.toArray(); - switch (accessor.componentType.id) { + switch (arrayAccessor.componentType.id) { case Types.ARRAY: final AbstractCursor.ArrayAccessor componentAccessor = - (AbstractCursor.ArrayAccessor) accessor.componentAccessor; + (AbstractCursor.ArrayAccessor) arrayAccessor.componentAccessor; for (i = 0; i < objects.length; i++) { - objects[i] = new ArrayImpl((List) objects[i], componentAccessor); + // Convert the element into a Object[] or primitive array, recurse! + objects[i] = getArrayData(objects[i], componentAccessor); } } return objects; } - public Object getArray(Map<String, Class<?>> map) throws SQLException { + Object getArrayData(Object o, AbstractCursor.ArrayAccessor componentAccessor) + throws SQLException { + if (o instanceof List) { + return getArray((List<?>) o, componentAccessor); + } else if (o instanceof ArrayImpl) { + return (ArrayImpl) o; + } + throw new RuntimeException("Unhandled"); + } + + @Override public Object getArray(Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); // TODO } - public Object getArray(long index, int count) throws SQLException { - return getArray(list.subList((int) index, count)); + @Override public Object getArray(long index, int count) throws SQLException { + if (index > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Arrays cannot be longer than " + Integer.MAX_VALUE); + } + // Convert from one-index to zero-index + int startIndex = ((int) index) - 1; + if (startIndex < 0 || startIndex > list.size()) { + throw new IllegalArgumentException("Invalid index: " + index + ". Size = " + list.size()); + } + int endIndex = startIndex + count; + if (endIndex > list.size()) { + throw new IllegalArgumentException("Invalid count provided. Size = " + list.size() + + ", count = " + count); + } + // End index is non-inclusive + return getArray(list.subList(startIndex, endIndex), accessor); } - public Object getArray(long index, int count, Map<String, Class<?>> map) + @Override public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); // TODO } - public ResultSet getResultSet() throws SQLException { + @Override public ResultSet getResultSet() throws SQLException { return accessor.factory.create(accessor.componentType, list); } - public ResultSet getResultSet(Map<String, Class<?>> map) + @Override public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); // TODO } - public ResultSet getResultSet(long index, int count) throws SQLException { + @Override public ResultSet getResultSet(long index, int count) throws SQLException { throw new UnsupportedOperationException(); // TODO } - public ResultSet getResultSet(long index, int count, + @Override public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map) throws SQLException { throw new UnsupportedOperationException(); // TODO } - public void free() throws SQLException { + @Override public void free() throws SQLException { // nothing to do } - /** Factory that can create a result set based on a list of values. */ + /** Factory that can create a ResultSet or Array based on a stream of values. */ public interface Factory { - ResultSet create(ColumnMetaData.AvaticaType elementType, - Iterable<Object> iterable); + + /** + * Creates a {@link ResultSet} from the given list of values per {@link Array#getResultSet()}. + * + * @param elementType The type of the elements + * @param iterable The elements + */ + ResultSet create(ColumnMetaData.AvaticaType elementType, Iterable<Object> iterable); + + /** + * Creates an {@link Array} from the given list of values, converting any primitive values + * into the corresponding objects. + * + * @param elementType The type of the elements + * @param elements The elements + */ + Array createArray(ColumnMetaData.AvaticaType elementType, Iterable<Object> elements); } } http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/java/org/apache/calcite/avatica/util/PositionedCursor.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/PositionedCursor.java b/core/src/main/java/org/apache/calcite/avatica/util/PositionedCursor.java index f60f47d..070319d 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/PositionedCursor.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/PositionedCursor.java @@ -17,6 +17,7 @@ package org.apache.calcite.avatica.util; import java.lang.reflect.Field; +import java.sql.SQLException; import java.util.List; import java.util.Map; @@ -48,7 +49,19 @@ public abstract class PositionedCursor<T> extends AbstractCursor { } public Object getObject() { - Object o = ((Object[]) current())[field]; + Object collection = current(); + Object o; + if (collection instanceof List) { + o = ((List) collection).get(field); + } else if (collection instanceof StructImpl) { + try { + o = ((StructImpl) collection).getAttributes()[field]; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } else { + o = ((Object[]) collection)[field]; + } wasNull[0] = o == null; return o; } http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java b/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java index 1d0238c..906651d 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java @@ -50,6 +50,17 @@ public class Unsafe { public static Calendar localCalendar() { return Calendar.getInstance(Locale.ROOT); } + + /** + * Returns a {@link java.lang.String}, created from the given format and args, + * with the root locale. Analog to {@link String#format(String, Object...)}. + * + * @param format The format string + * @param args Arguments to be substituted into the format string. + */ + public static String formatLocalString(String format, Object... args) { + return String.format(Locale.ROOT, format, args); + } } // End Unsafe.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/main/protobuf/common.proto ---------------------------------------------------------------------- diff --git a/core/src/main/protobuf/common.proto b/core/src/main/protobuf/common.proto index affe5d5..63dbcc9 100644 --- a/core/src/main/protobuf/common.proto +++ b/core/src/main/protobuf/common.proto @@ -199,6 +199,9 @@ message TypedValue { bytes bytes_value = 5; // binary/varbinary double double_value = 6; // big numbers bool null = 7; // a null object + + repeated TypedValue array_value = 8; // The Array + Rep component_type = 9; // If an Array, the representation for the array values } // The severity of some unexpected outcome to an operation. http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java index bf3047f..8605aaf 100644 --- a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java @@ -16,7 +16,6 @@ */ package org.apache.calcite.avatica; -import org.apache.calcite.avatica.ColumnMetaData.AvaticaType; import org.apache.calcite.avatica.remote.TypedValue; import org.apache.calcite.avatica.util.DateTimeUtils; @@ -85,22 +84,11 @@ public class AvaticaResultSetConversionsTest { throw new UnsupportedOperationException(); } - @SuppressWarnings("deprecation") @Override public ExecuteResult prepareAndExecute(StatementHandle h, String sql, long maxRowCount, PrepareCallback callback) throws NoSuchStatementException { throw new UnsupportedOperationException(); } - private static ColumnMetaData columnMetaData(String name, int ordinal, AvaticaType type, - int columnNullable) { - return new ColumnMetaData( - ordinal, false, true, false, false, - columnNullable, - true, -1, name, name, null, - 0, 0, null, null, type, true, false, false, - type.columnClassName()); - } - @Override public ExecuteResult prepareAndExecute(StatementHandle h, String sql, long maxRowCount, int maxRowsInFirstFrame, PrepareCallback callback) throws NoSuchStatementException { @@ -191,7 +179,6 @@ public class AvaticaResultSetConversionsTest { throw new UnsupportedOperationException(); } - @SuppressWarnings("deprecation") @Override public ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues, long maxRowCount) throws NoSuchStatementException { throw new UnsupportedOperationException(); http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/FrameTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/FrameTest.java b/core/src/test/java/org/apache/calcite/avatica/FrameTest.java index e17bf92..4f34a3c 100644 --- a/core/src/test/java/org/apache/calcite/avatica/FrameTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/FrameTest.java @@ -206,6 +206,34 @@ public class FrameTest { List<Common.TypedValue> arrayValues = protoColumns.get(1).getArrayValueList(); assertEquals(arrayValues, deprecatedValues); } + + @Test public void testNestedArraySerialization() { + List<Object> rows = new ArrayList<>(); + // [ "pk", [[1,2], [3,4]] ] + rows.add(Arrays.asList("pk", Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4)))); + Frame frame = new Frame(0, true, rows); + // Parse back the list in serialized form + Common.Frame protoFrame = frame.toProto(); + Common.Row protoRow = protoFrame.getRows(0); + Common.ColumnValue protoColumn = protoRow.getValue(1); + assertTrue(protoColumn.getHasArrayValue()); + int value = 1; + for (Common.TypedValue arrayElement : protoColumn.getArrayValueList()) { + assertEquals(Common.Rep.ARRAY, arrayElement.getType()); + for (Common.TypedValue nestedArrayElement : arrayElement.getArrayValueList()) { + assertEquals(Common.Rep.INTEGER, nestedArrayElement.getType()); + assertEquals(value++, nestedArrayElement.getNumberValue()); + } + } + + Frame newFrame = Frame.fromProto(protoFrame); + @SuppressWarnings("unchecked") + List<Object> newRow = (List<Object>) newFrame.rows.iterator().next(); + @SuppressWarnings("unchecked") + List<Object> expectedRow = (List<Object>) rows.get(0); + assertEquals(expectedRow.get(0), newRow.get(0)); + assertEquals(expectedRow.get(1), newRow.get(1)); + } } // End FrameTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/RepTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/RepTest.java b/core/src/test/java/org/apache/calcite/avatica/RepTest.java new file mode 100644 index 0000000..ce0ba9b --- /dev/null +++ b/core/src/test/java/org/apache/calcite/avatica/RepTest.java @@ -0,0 +1,57 @@ +/* + * 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; + +import org.apache.calcite.avatica.ColumnMetaData.Rep; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Test class for {@link Rep}. + */ +public class RepTest { + + @Test public void testNonPrimitiveRepForType() { + assertEquals(Rep.BOOLEAN, Rep.nonPrimitiveRepOf(SqlType.BIT)); + assertEquals(Rep.BOOLEAN, Rep.nonPrimitiveRepOf(SqlType.BOOLEAN)); + assertEquals(Rep.BYTE, Rep.nonPrimitiveRepOf(SqlType.TINYINT)); + assertEquals(Rep.SHORT, Rep.nonPrimitiveRepOf(SqlType.SMALLINT)); + assertEquals(Rep.INTEGER, Rep.nonPrimitiveRepOf(SqlType.INTEGER)); + assertEquals(Rep.LONG, Rep.nonPrimitiveRepOf(SqlType.BIGINT)); + assertEquals(Rep.DOUBLE, Rep.nonPrimitiveRepOf(SqlType.FLOAT)); + assertEquals(Rep.DOUBLE, Rep.nonPrimitiveRepOf(SqlType.DOUBLE)); + assertEquals(Rep.STRING, Rep.nonPrimitiveRepOf(SqlType.CHAR)); + } + + @Test public void testSerialRep() { + assertEquals(Rep.BOOLEAN, Rep.serialRepOf(SqlType.BIT)); + assertEquals(Rep.BOOLEAN, Rep.serialRepOf(SqlType.BOOLEAN)); + assertEquals(Rep.BYTE, Rep.serialRepOf(SqlType.TINYINT)); + assertEquals(Rep.SHORT, Rep.serialRepOf(SqlType.SMALLINT)); + assertEquals(Rep.INTEGER, Rep.serialRepOf(SqlType.INTEGER)); + assertEquals(Rep.LONG, Rep.serialRepOf(SqlType.BIGINT)); + assertEquals(Rep.DOUBLE, Rep.serialRepOf(SqlType.FLOAT)); + assertEquals(Rep.DOUBLE, Rep.serialRepOf(SqlType.DOUBLE)); + assertEquals(Rep.INTEGER, Rep.serialRepOf(SqlType.DATE)); + assertEquals(Rep.INTEGER, Rep.serialRepOf(SqlType.TIME)); + assertEquals(Rep.LONG, Rep.serialRepOf(SqlType.TIMESTAMP)); + } +} + +// End RepTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java b/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java index 7606a87..50d492e 100644 --- a/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java @@ -16,16 +16,25 @@ */ package org.apache.calcite.avatica.remote; +import org.apache.calcite.avatica.ColumnMetaData; import org.apache.calcite.avatica.ColumnMetaData.Rep; +import org.apache.calcite.avatica.ColumnMetaData.ScalarType; import org.apache.calcite.avatica.proto.Common; +import org.apache.calcite.avatica.util.ArrayFactoryImpl; +import org.apache.calcite.avatica.util.ArrayImpl; import org.apache.calcite.avatica.util.Base64; import org.apache.calcite.avatica.util.ByteString; import org.apache.calcite.avatica.util.DateTimeUtils; +import org.apache.calcite.avatica.util.Unsafe; import org.junit.Test; import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Types; +import java.util.Arrays; import java.util.Calendar; +import java.util.List; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; @@ -33,6 +42,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static java.nio.charset.StandardCharsets.UTF_8; @@ -203,6 +213,25 @@ public class TypedValueTest { assertEquals(Rep.BYTE_STRING, tv.type); assertEquals(base64Str, tv.value); } + + @Test public void testArrays() { + List<Object> serialObj = Arrays.<Object>asList(1, 2, 3, 4); + ArrayImpl.Factory factory = new ArrayFactoryImpl(Unsafe.localCalendar().getTimeZone()); + ScalarType scalarType = ColumnMetaData.scalar(Types.INTEGER, "INTEGER", Rep.INTEGER); + Array a1 = factory.createArray(scalarType, serialObj); + TypedValue tv1 = TypedValue.ofJdbc(Rep.ARRAY, a1, Unsafe.localCalendar()); + Object jdbcObj = tv1.toJdbc(Unsafe.localCalendar()); + assertTrue("The JDBC object is an " + jdbcObj.getClass(), jdbcObj instanceof Array); + Object localObj = tv1.toLocal(); + assertTrue("The local object is an " + localObj.getClass(), localObj instanceof List); + Common.TypedValue protoTv1 = tv1.toProto(); + assertEquals(serialObj.size(), protoTv1.getArrayValueCount()); + TypedValue tv1Copy = TypedValue.fromProto(protoTv1); + Object jdbcObjCopy = tv1Copy.toJdbc(Unsafe.localCalendar()); + assertTrue("The JDBC object is an " + jdbcObjCopy.getClass(), jdbcObjCopy instanceof Array); + Object localObjCopy = tv1Copy.toLocal(); + assertTrue("The local object is an " + localObjCopy.getClass(), localObjCopy instanceof List); + } } // End TypedValueTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/util/ArrayImplTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/util/ArrayImplTest.java b/core/src/test/java/org/apache/calcite/avatica/util/ArrayImplTest.java new file mode 100644 index 0000000..2ebd13b --- /dev/null +++ b/core/src/test/java/org/apache/calcite/avatica/util/ArrayImplTest.java @@ -0,0 +1,193 @@ +/* + * 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.util; + +import org.apache.calcite.avatica.ColumnMetaData; +import org.apache.calcite.avatica.ColumnMetaData.ArrayType; +import org.apache.calcite.avatica.ColumnMetaData.Rep; +import org.apache.calcite.avatica.ColumnMetaData.ScalarType; +import org.apache.calcite.avatica.ColumnMetaData.StructType; +import org.apache.calcite.avatica.MetaImpl; +import org.apache.calcite.avatica.util.Cursor.Accessor; + +import org.junit.Test; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.Struct; +import java.sql.Types; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test class for ArrayImpl. + */ +public class ArrayImplTest { + + @Test public void resultSetFromArray() throws Exception { + // Define the struct type we're creating + ScalarType intType = ColumnMetaData.scalar(Types.INTEGER, "INTEGER", Rep.INTEGER); + ArrayType arrayType = ColumnMetaData.array(intType, "INTEGER", Rep.INTEGER); + ColumnMetaData arrayMetaData = MetaImpl.columnMetaData("MY_ARRAY", 1, arrayType, false); + ArrayImpl.Factory factory = new ArrayFactoryImpl(Unsafe.localCalendar().getTimeZone()); + // Create some arrays from the structs + Array array1 = factory.createArray(intType, Arrays.<Object>asList(1, 2)); + Array array2 = factory.createArray(intType, Arrays.<Object>asList(3)); + Array array3 = factory.createArray(intType, Arrays.<Object>asList(4, 5, 6)); + List<List<Object>> rows = Arrays.asList(Collections.<Object>singletonList(array1), + Collections.<Object>singletonList(array2), Collections.<Object>singletonList(array3)); + // Create two rows, each with one (array) column + try (Cursor cursor = new ListIteratorCursor(rows.iterator())) { + List<Accessor> accessors = cursor.createAccessors(Collections.singletonList(arrayMetaData), + Unsafe.localCalendar(), factory); + assertEquals(1, accessors.size()); + Accessor accessor = accessors.get(0); + + assertTrue(cursor.next()); + Array actualArray = accessor.getArray(); + // An Array's result set has one row per array element. + // Each row has two columns. Column 1 is the array offset (1-based), Column 2 is the value. + ResultSet actualArrayResultSet = actualArray.getResultSet(); + assertEquals(2, actualArrayResultSet.getMetaData().getColumnCount()); + assertTrue(actualArrayResultSet.next()); + // Order is Avatica implementation specific + assertEquals(1, actualArrayResultSet.getInt(1)); + assertEquals(1, actualArrayResultSet.getInt(2)); + assertTrue(actualArrayResultSet.next()); + assertEquals(2, actualArrayResultSet.getInt(1)); + assertEquals(2, actualArrayResultSet.getInt(2)); + assertFalse(actualArrayResultSet.next()); + + assertTrue(cursor.next()); + actualArray = accessor.getArray(); + actualArrayResultSet = actualArray.getResultSet(); + assertEquals(2, actualArrayResultSet.getMetaData().getColumnCount()); + assertTrue(actualArrayResultSet.next()); + assertEquals(1, actualArrayResultSet.getInt(1)); + assertEquals(3, actualArrayResultSet.getInt(2)); + assertFalse(actualArrayResultSet.next()); + + assertTrue(cursor.next()); + actualArray = accessor.getArray(); + actualArrayResultSet = actualArray.getResultSet(); + assertEquals(2, actualArrayResultSet.getMetaData().getColumnCount()); + assertTrue(actualArrayResultSet.next()); + assertEquals(1, actualArrayResultSet.getInt(1)); + assertEquals(4, actualArrayResultSet.getInt(2)); + assertTrue(actualArrayResultSet.next()); + assertEquals(2, actualArrayResultSet.getInt(1)); + assertEquals(5, actualArrayResultSet.getInt(2)); + assertTrue(actualArrayResultSet.next()); + assertEquals(3, actualArrayResultSet.getInt(1)); + assertEquals(6, actualArrayResultSet.getInt(2)); + assertFalse(actualArrayResultSet.next()); + + assertFalse(cursor.next()); + } + } + + @Test public void arraysOfStructs() throws Exception { + // Define the struct type we're creating + ColumnMetaData intMetaData = MetaImpl.columnMetaData("MY_INT", 1, int.class, false); + ColumnMetaData stringMetaData = MetaImpl.columnMetaData("MY_STRING", 2, String.class, true); + StructType structType = ColumnMetaData.struct(Arrays.asList(intMetaData, stringMetaData)); + // Create some structs + Struct struct1 = new StructImpl(Arrays.<Object>asList(1, "one")); + Struct struct2 = new StructImpl(Arrays.<Object>asList(2, "two")); + Struct struct3 = new StructImpl(Arrays.<Object>asList(3)); + Struct struct4 = new StructImpl(Arrays.<Object>asList(4, "four")); + ArrayType arrayType = ColumnMetaData.array(structType, "OBJECT", Rep.STRUCT); + ColumnMetaData arrayMetaData = MetaImpl.columnMetaData("MY_ARRAY", 1, arrayType, false); + ArrayImpl.Factory factory = new ArrayFactoryImpl(Unsafe.localCalendar().getTimeZone()); + // Create some arrays from the structs + Array array1 = factory.createArray(structType, Arrays.<Object>asList(struct1, struct2)); + Array array2 = factory.createArray(structType, Arrays.<Object>asList(struct3, struct4)); + List<List<Object>> rows = Arrays.asList(Collections.<Object>singletonList(array1), + Collections.<Object>singletonList(array2)); + // Create two rows, each with one (array) column + try (Cursor cursor = new ListIteratorCursor(rows.iterator())) { + List<Accessor> accessors = cursor.createAccessors(Collections.singletonList(arrayMetaData), + Unsafe.localCalendar(), factory); + assertEquals(1, accessors.size()); + Accessor accessor = accessors.get(0); + + assertTrue(cursor.next()); + Array actualArray = accessor.getArray(); + // Avoiding explicit use of the getResultSet() method for now.. + Object[] arrayData = (Object[]) actualArray.getArray(); + assertEquals(2, arrayData.length); + Struct actualStruct = (Struct) arrayData[0]; + Object[] o = actualStruct.getAttributes(); + assertEquals(2, o.length); + assertEquals(1, o[0]); + assertEquals("one", o[1]); + + actualStruct = (Struct) arrayData[1]; + o = actualStruct.getAttributes(); + assertEquals(2, o.length); + assertEquals(2, o[0]); + assertEquals("two", o[1]); + + assertTrue(cursor.next()); + actualArray = accessor.getArray(); + arrayData = (Object[]) actualArray.getArray(); + assertEquals(2, arrayData.length); + actualStruct = (Struct) arrayData[0]; + o = actualStruct.getAttributes(); + assertEquals(1, o.length); + assertEquals(3, o[0]); + + actualStruct = (Struct) arrayData[1]; + o = actualStruct.getAttributes(); + assertEquals(2, o.length); + assertEquals(4, o[0]); + assertEquals("four", o[1]); + } + } + + @Test public void testArrayWithOffsets() throws Exception { + // Define the struct type we're creating + ScalarType intType = ColumnMetaData.scalar(Types.INTEGER, "INTEGER", Rep.INTEGER); + ArrayImpl.Factory factory = new ArrayFactoryImpl(Unsafe.localCalendar().getTimeZone()); + // Create some arrays from the structs + Array array1 = factory.createArray(intType, Arrays.<Object>asList(1, 2)); + Array array3 = factory.createArray(intType, Arrays.<Object>asList(4, 5, 6)); + + Object[] data = (Object[]) array1.getArray(2, 1); + assertEquals(1, data.length); + assertEquals(2, data[0]); + data = (Object[]) array3.getArray(1, 1); + assertEquals(1, data.length); + assertEquals(4, data[0]); + data = (Object[]) array3.getArray(2, 2); + assertEquals(2, data.length); + assertEquals(5, data[0]); + assertEquals(6, data[1]); + data = (Object[]) array3.getArray(1, 3); + assertEquals(3, data.length); + assertEquals(4, data[0]); + assertEquals(5, data[1]); + assertEquals(6, data[2]); + } +} + +// End ArrayImplTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/core/src/test/java/org/apache/calcite/avatica/util/StructImplTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/util/StructImplTest.java b/core/src/test/java/org/apache/calcite/avatica/util/StructImplTest.java new file mode 100644 index 0000000..625c538 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/avatica/util/StructImplTest.java @@ -0,0 +1,92 @@ +/* + * 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.util; + +import org.apache.calcite.avatica.ColumnMetaData; +import org.apache.calcite.avatica.ColumnMetaData.StructType; +import org.apache.calcite.avatica.MetaImpl; +import org.apache.calcite.avatica.util.Cursor.Accessor; + +import org.junit.Test; + +import java.sql.Struct; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test class for StructImpl. + */ +public class StructImplTest { + + @Test public void structAccessor() throws Exception { + // Define the struct type we're creating + ColumnMetaData intMetaData = MetaImpl.columnMetaData("MY_INT", 1, int.class, false); + ColumnMetaData stringMetaData = MetaImpl.columnMetaData("MY_STRING", 2, String.class, true); + StructType structType = ColumnMetaData.struct(Arrays.asList(intMetaData, stringMetaData)); + // Create some structs + Struct struct1 = new StructImpl(Arrays.<Object>asList(1, "one")); + Struct struct2 = new StructImpl(Arrays.<Object>asList(2, "two")); + Struct struct3 = new StructImpl(Arrays.<Object>asList(3)); + Struct struct4 = new StructImpl(Arrays.<Object>asList(4, "four", "ignored")); + ColumnMetaData structMetaData = MetaImpl.columnMetaData("MY_STRUCT", 1, structType, false); + List<List<Object>> rows = Arrays.asList(Collections.<Object>singletonList(struct1), + Collections.<Object>singletonList(struct2), Collections.<Object>singletonList(struct3), + Collections.<Object>singletonList(struct4)); + // Create four rows, each with one (struct) column + try (Cursor cursor = new ListIteratorCursor(rows.iterator())) { + List<Accessor> accessors = cursor.createAccessors(Collections.singletonList(structMetaData), + Unsafe.localCalendar(), null); + assertEquals(1, accessors.size()); + Accessor accessor = accessors.get(0); + + assertTrue(cursor.next()); + Struct s = accessor.getObject(Struct.class); + Object[] structData = s.getAttributes(); + assertEquals(2, structData.length); + assertEquals(1, structData[0]); + assertEquals("one", structData[1]); + + assertTrue(cursor.next()); + s = accessor.getObject(Struct.class); + structData = s.getAttributes(); + assertEquals(2, structData.length); + assertEquals(2, structData[0]); + assertEquals("two", structData[1]); + + assertTrue(cursor.next()); + s = accessor.getObject(Struct.class); + structData = s.getAttributes(); + assertEquals(1, structData.length); + assertEquals(3, structData[0]); + + assertTrue(cursor.next()); + s = accessor.getObject(Struct.class); + structData = s.getAttributes(); + assertEquals(3, structData.length); + assertEquals(4, structData[0]); + assertEquals("four", structData[1]); + // We didn't provide metadata, but we still expect to see it. + assertEquals("ignored", structData[2]); + } + } +} + +// End StructImplTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java ---------------------------------------------------------------------- diff --git a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java index 4756e8d..a02aa69 100644 --- a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java +++ b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java @@ -37,6 +37,7 @@ import org.apache.calcite.avatica.remote.ProtobufMeta; import org.apache.calcite.avatica.remote.TypedValue; import org.apache.calcite.avatica.util.Unsafe; +import com.google.common.base.Optional; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; @@ -805,7 +806,7 @@ public class JdbcMeta implements ProtobufMeta { return Frame.EMPTY; } else { return JdbcResultSet.frame(statementInfo, statementInfo.getResultSet(), offset, - fetchMaxRowCount, calendar); + fetchMaxRowCount, calendar, Optional.<Meta.Signature>absent()); } } catch (SQLException e) { throw propagate(e); @@ -819,7 +820,6 @@ public class JdbcMeta implements ProtobufMeta { return typeList.toArray(new String[typeList.size()]); } - @SuppressWarnings("deprecation") @Override public ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues, long maxRowCount) throws NoSuchStatementException { return execute(h, parameterValues, AvaticaUtils.toSaturatedInt(maxRowCount)); @@ -848,7 +848,6 @@ public class JdbcMeta implements ProtobufMeta { } if (preparedStatement.execute()) { - final Meta.Frame frame; final Signature signature2; if (preparedStatement.isWrapperFor(AvaticaPreparedStatement.class)) { signature2 = h.signature; @@ -863,7 +862,6 @@ public class JdbcMeta implements ProtobufMeta { statementInfo.setResultSet(preparedStatement.getResultSet()); if (statementInfo.getResultSet() == null) { - frame = Frame.EMPTY; resultSets = Collections.<MetaResultSet>singletonList( JdbcResultSet.empty(h.connectionId, h.id, signature2)); } else { http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java ---------------------------------------------------------------------- diff --git a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java index 17b33f8..b879086 100644 --- a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java +++ b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java @@ -17,21 +17,31 @@ package org.apache.calcite.avatica.jdbc; import org.apache.calcite.avatica.AvaticaStatement; +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.Meta; +import org.apache.calcite.avatica.SqlType; import org.apache.calcite.avatica.util.DateTimeUtils; +import com.google.common.base.Optional; + import java.sql.Array; import java.sql.Date; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; import java.util.Calendar; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.TreeMap; /** Implementation of {@link org.apache.calcite.avatica.Meta.MetaResultSet} @@ -88,7 +98,8 @@ class JdbcResultSet extends Meta.MetaResultSet { } else { fetchRowCount = maxRowCount; } - final Meta.Frame firstFrame = frame(null, resultSet, 0, fetchRowCount, calendar); + final Meta.Frame firstFrame = frame(null, resultSet, 0, fetchRowCount, calendar, + Optional.of(signature)); if (firstFrame.done) { resultSet.close(); } @@ -115,12 +126,16 @@ class JdbcResultSet extends Meta.MetaResultSet { /** Creates a frame containing a given number or unlimited number of rows * from a result set. */ static Meta.Frame frame(StatementInfo info, ResultSet resultSet, long offset, - int fetchMaxRowCount, Calendar calendar) throws SQLException { + int fetchMaxRowCount, Calendar calendar, Optional<Meta.Signature> sig) throws SQLException { final ResultSetMetaData metaData = resultSet.getMetaData(); final int columnCount = metaData.getColumnCount(); final int[] types = new int[columnCount]; + Set<Integer> arrayOffsets = new HashSet<>(); for (int i = 0; i < types.length; i++) { types[i] = metaData.getColumnType(i + 1); + if (Types.ARRAY == types[i]) { + arrayOffsets.add(i); + } } final List<Object> rows = new ArrayList<>(); // Meta prepare/prepareAndExecute 0 return 0 row and done @@ -140,6 +155,28 @@ class JdbcResultSet extends Meta.MetaResultSet { Object[] columns = new Object[columnCount]; for (int j = 0; j < columnCount; j++) { columns[j] = getValue(resultSet, types[j], j, calendar); + if (arrayOffsets.contains(j)) { + // If we have an Array type, our Signature is lacking precision. We can't extract the + // component type of an Array from metadata, we have to update it as we're serializing + // the ResultSet. + final Array array = resultSet.getArray(j + 1); + // Only attempt to determine the component type for the array when non-null + if (null != array && sig.isPresent()) { + ColumnMetaData columnMetaData = sig.get().columns.get(j); + ArrayType arrayType = (ArrayType) columnMetaData.type; + SqlType componentSqlType = SqlType.valueOf(array.getBaseType()); + + // Avatica Server will always return non-primitives to ensure nullable is guaranteed. + ColumnMetaData.Rep rep = ColumnMetaData.Rep.serialRepOf(componentSqlType); + AvaticaType componentType = ColumnMetaData.scalar(array.getBaseType(), + array.getBaseTypeName(), rep); + // Update the ArrayType from the Signature + arrayType.updateComponentType(componentType); + + // We only need to update the array's type once. + arrayOffsets.remove(j); + } + } } rows.add(columns); } @@ -186,18 +223,14 @@ class JdbcResultSet extends Meta.MetaResultSet { if (null == array) { return null; } - ResultSet arrayValues = array.getResultSet(); - TreeMap<Integer, Object> map = new TreeMap<>(); - while (arrayValues.next()) { - // column 1 is the index in the array, column 2 is the value. - // Recurse on `getValue` to unwrap nested types correctly. - // `j` is zero-indexed and incremented for us, thus we have `1` being used twice. - map.put(arrayValues.getInt(1), getValue(arrayValues, array.getBaseType(), 1, calendar)); + try { + // Recursively extracts an Array using its ResultSet-representation + return extractUsingResultSet(array, calendar); + } catch (UnsupportedOperationException | SQLFeatureNotSupportedException e) { + // Not every database might implement Array.getResultSet(). This call + // assumes a non-nested array (depends on the db if that's a valid assumption) + return extractUsingArray(array, calendar); } - // If the result set is not in the same order as the actual Array, TreeMap fixes that. - // Need to make a concrete list to ensure Jackson serialization. - //return new ListLike<Object>(new ArrayList<>(map.values()), ListLikeType.ARRAY); - return new ArrayList<>(map.values()); case Types.STRUCT: Struct struct = resultSet.getObject(j + 1, Struct.class); Object[] attrs = struct.getAttributes(); @@ -210,6 +243,39 @@ class JdbcResultSet extends Meta.MetaResultSet { return resultSet.getObject(j + 1); } } + + /** + * Converts an Array into a List using {@link Array#getResultSet()}. This implementation is + * recursive and can parse multi-dimensional arrays. + */ + static List<?> extractUsingResultSet(Array array, Calendar calendar) throws SQLException { + ResultSet arrayValues = array.getResultSet(); + TreeMap<Integer, Object> map = new TreeMap<>(); + while (arrayValues.next()) { + // column 1 is the index in the array, column 2 is the value. + // Recurse on `getValue` to unwrap nested types correctly. + // `j` is zero-indexed and incremented for us, thus we have `1` being used twice. + map.put(arrayValues.getInt(1), getValue(arrayValues, array.getBaseType(), 1, calendar)); + } + // If the result set is not in the same order as the actual Array, TreeMap fixes that. + // Need to make a concrete list to ensure Jackson serialization. + return new ArrayList<>(map.values()); + } + + /** + * Converts an Array into a List using {@link Array#getArray()}. This implementation assumes + * a non-nested array. Use {link {@link #extractUsingResultSet(Array, Calendar)} if nested + * arrays may be possible. + */ + static List<?> extractUsingArray(Array array, Calendar calendar) throws SQLException { + // No option but to guess as to what the type actually is... + Object o = array.getArray(); + if (o instanceof List) { + return (List<?>) o; + } + // Assume that it's a Java array. + return AvaticaUtils.primitiveList(o); + } } // End JdbcResultSet.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java index 0823a12..a69fa21 100644 --- a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java +++ b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java @@ -138,7 +138,7 @@ public class RemoteDriverTest { } // Run each test with the LocalJsonService and LocalProtobufService - @Parameters + @Parameters(name = "{0}") public static List<Object[]> parameters() { List<Object[]> connections = new ArrayList<>(); @@ -147,6 +147,7 @@ public class RemoteDriverTest { connections.add( new Object[] { + "JSON", new Callable<Connection>() { public Connection call() { try { @@ -167,6 +168,7 @@ public class RemoteDriverTest { // TODO write the ConnectionInternals implementation connections.add( new Object[] { + "PROTOBUF", new Callable<Connection>() { public Connection call() { try { @@ -191,7 +193,7 @@ public class RemoteDriverTest { private final ConnectionInternals localConnectionInternals; private final Callable<RequestInspection> requestInspectionCallable; - public RemoteDriverTest(Callable<Connection> localConnectionCallable, + public RemoteDriverTest(String name, Callable<Connection> localConnectionCallable, ConnectionInternals internals, Callable<RequestInspection> requestInspectionCallable) { this.localConnectionCallable = localConnectionCallable; this.localConnectionInternals = internals; @@ -884,6 +886,7 @@ public class RemoteDriverTest { final ResultSet resultSet = ps.executeQuery(); fail("expected error, got " + resultSet); } catch (SQLException e) { + LOG.info("Caught expected error", e); assertThat(e.getMessage(), containsString("exception while executing query: unbound parameter")); } http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/dd65a2b1/server/src/test/java/org/apache/calcite/avatica/remote/ArrayTypeTest.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/calcite/avatica/remote/ArrayTypeTest.java b/server/src/test/java/org/apache/calcite/avatica/remote/ArrayTypeTest.java new file mode 100644 index 0000000..e1c3355 --- /dev/null +++ b/server/src/test/java/org/apache/calcite/avatica/remote/ArrayTypeTest.java @@ -0,0 +1,626 @@ +/* + * 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 { + if (null != SERVERS) { + 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(Short.valueOf(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(Short.valueOf(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, "float_arrays", "DOUBLE", 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(Byte.valueOf(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, new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws SQLException { + 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), new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws Exception { + 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, new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws SQLException { + 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), new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws Exception { + 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, + new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws SQLException { + 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), new Validator<Array>() { + @Override public void validate(Array expected, Array actual) throws Exception { + 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); + } + } + + /** + * 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); + assertTrue("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 + */ + 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 comparse 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
