http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java ---------------------------------------------------------------------- diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java new file mode 100644 index 0000000..d96293b --- /dev/null +++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java @@ -0,0 +1,533 @@ +/* + * 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.ColumnMetaData; +import org.apache.calcite.avatica.proto.Common; +import org.apache.calcite.avatica.util.ByteString; +import org.apache.calcite.avatica.util.DateTimeUtils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.protobuf.HBaseZeroCopyByteString; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** Value and type. + * + * <p>There are 3 representations: + * <ul> + * <li>JDBC - the representation used by JDBC get and set methods + * <li>Serial - suitable for serializing using JSON + * <li>Local - used by Calcite for efficient computation + * </ul> + * + * <p>The following table shows the Java type(s) that may represent each SQL + * type in each representation. + * + * <table> + * <caption>SQL types and their representations</caption> + * <tr> + * <th>Type</th> <th>JDBC</th> <th>Serial</th> <th>Local</th> + * </tr> + * <tr> + * <td>BOOLEAN</td> <td>boolean</td> <td>boolean</td> <td>boolean</td> + * </tr> + * <tr> + * <td>BINARY, VARBINARY</td> <td>byte[]</td> + * <td>String (base64)</td> <td>{@link ByteString}</td> + * </tr> + * <tr> + * <td>DATE</td> <td>{@link java.sql.Date}</td> + * <td>int</td> <td>int</td> + * </tr> + * <tr> + * <td>TIME</td> <td>{@link java.sql.Time}</td> + * <td>int</td> <td>int</td> + * </tr> + * <tr> + * <td>DATE</td> <td>{@link java.sql.Timestamp}</td> + * <td>long</td> <td>long</td> + * </tr> + * <tr> + * <td>CHAR, VARCHAR</td> + * <td>String</td> <td>String</td> <td>String</td> + * </tr> + * <tr> + * <td>TINYINT</td> <td>byte</td> <td>Number</td> <td>byte</td> + * </tr> + * <tr> + * <td>SMALLINT</td> <td>short</td> <td>Number</td> <td>short</td> + * </tr> + * <tr> + * <td>INTEGER</td> <td>int</td> <td>Number</td> <td>int</td> + * </tr> + * <tr> + * <td>BIGINT</td> <td>long</td> <td>Number</td> <td>long</td> + * </tr> + * <tr> + * <td>REAL</td> <td>float</td> <td>Number</td> <td>float</td> + * </tr> + * <tr> + * <td>FLOAT, DOUBLE</td> + * <td>double</td> <td>Number</td> <td>double</td> + * </tr> + * <tr> + * <td>DECIMAL</td> + * <td>BigDecimal</td> <td>Number</td> <td>BigDecimal</td> + * </tr> + * </table> + * + * Note: + * <ul> + * <li>The various numeric types (TINYINT, SMALLINT, INTEGER, BIGINT, REAL, + * FLOAT, DOUBLE) are represented by {@link Number} in serial format because + * JSON numbers are not strongly typed. A {@code float} value {@code 3.0} is + * transmitted as {@code 3}, and is therefore decoded as an {@code int}. + * + * <li>The date-time types (DATE, TIME, TIMESTAMP) are represented in JDBC as + * {@link java.sql.Date}, {@link java.sql.Time}, {@link java.sql.Timestamp}, + * all sub-classes of {@link java.util.Date}. When they are passed to and + * from the server, they are interpreted in terms of a time zone, by default + * the current connection's time zone. Their serial and local representations + * as {@code int} (days since 1970-01-01 for DATE, milliseconds since + * 00:00:00.000 for TIME), and long (milliseconds since 1970-01-01 + * 00:00:00.000 for TIMESTAMP) are easier to work with, because it is clear + * that time zone is not involved. + * + * <li>BINARY and VARBINARY values are represented as base64-encoded strings + * for serialization over JSON. + * </ul> + */ +public class TypedValue { + public static final TypedValue NULL = + new TypedValue(ColumnMetaData.Rep.OBJECT, null); + + /** Type of the value. */ + public final ColumnMetaData.Rep type; + + /** Value. + * + * <p>Always in a form that can be serialized to JSON by Jackson. + * For example, byte arrays are represented as String. */ + public final Object value; + + private TypedValue(ColumnMetaData.Rep rep, Object value) { + this.type = rep; + this.value = value; + assert isSerial(rep, value) : "rep: " + rep + ", value: " + value; + } + + private boolean isSerial(ColumnMetaData.Rep rep, Object value) { + if (value == null) { + return true; + } + switch (rep) { + case BYTE_STRING: + return value instanceof String; + case JAVA_SQL_DATE: + case JAVA_SQL_TIME: + return value instanceof Integer; + case JAVA_SQL_TIMESTAMP: + case JAVA_UTIL_DATE: + return value instanceof Long; + default: + return true; + } + } + + @JsonCreator + public static TypedValue create(@JsonProperty("type") String type, + @JsonProperty("value") Object value) { + if (value == null) { + return NULL; + } + ColumnMetaData.Rep rep = ColumnMetaData.Rep.valueOf(type); + return ofLocal(rep, serialToLocal(rep, value)); + } + + /** Creates a TypedValue from a value in local representation. */ + public static TypedValue ofLocal(ColumnMetaData.Rep rep, Object value) { + return new TypedValue(rep, localToSerial(rep, value)); + } + + /** Creates a TypedValue from a value in serial representation. */ + public static TypedValue ofSerial(ColumnMetaData.Rep rep, Object value) { + return new TypedValue(rep, value); + } + + /** Creates a TypedValue from a value in JDBC representation. */ + public static TypedValue ofJdbc(ColumnMetaData.Rep rep, Object value, + Calendar calendar) { + if (value == null) { + return NULL; + } + return new TypedValue(rep, jdbcToSerial(rep, value, calendar)); + } + + /** Creates a TypedValue from a value in JDBC representation, + * deducing its type. */ + public static TypedValue ofJdbc(Object value, Calendar calendar) { + if (value == null) { + return NULL; + } + final ColumnMetaData.Rep rep = ColumnMetaData.Rep.of(value.getClass()); + return new TypedValue(rep, jdbcToSerial(rep, value, calendar)); + } + + /** Converts the value into the local representation. + * + * <p>For example, a byte string is represented as a {@link ByteString}; + * a long is represented as a {@link Long} (not just some {@link Number}). + */ + public Object toLocal() { + if (value == null) { + return null; + } + return serialToLocal(type, value); + } + + /** Converts a value to the exact type required for the given + * representation. */ + private static Object serialToLocal(ColumnMetaData.Rep rep, Object value) { + assert value != null; + if (value.getClass() == rep.clazz) { + return value; + } + switch (rep) { + case BYTE: + return ((Number) value).byteValue(); + case SHORT: + return ((Number) value).shortValue(); + case INTEGER: + case JAVA_SQL_DATE: + case JAVA_SQL_TIME: + return ((Number) value).intValue(); + case LONG: + case JAVA_UTIL_DATE: + case JAVA_SQL_TIMESTAMP: + return ((Number) value).longValue(); + case FLOAT: + return ((Number) value).floatValue(); + case DOUBLE: + return ((Number) value).doubleValue(); + case NUMBER: + return value instanceof BigDecimal ? value + : value instanceof BigInteger ? new BigDecimal((BigInteger) value) + : value instanceof Double ? new BigDecimal((Double) value) + : value instanceof Float ? new BigDecimal((Float) value) + : new BigDecimal(((Number) value).longValue()); + case BYTE_STRING: + return ByteString.ofBase64((String) value); + default: + throw new IllegalArgumentException("cannot convert " + value + " (" + + value.getClass() + ") to " + rep); + } + } + + /** Converts the value into the JDBC representation. + * + * <p>For example, a byte string is represented as a {@link ByteString}; + * a long is represented as a {@link Long} (not just some {@link Number}). + */ + public Object toJdbc(Calendar calendar) { + if (value == null) { + return null; + } + return serialToJdbc(type, value, calendar); + } + + private static Object serialToJdbc(ColumnMetaData.Rep type, Object value, + Calendar calendar) { + switch (type) { + case BYTE_STRING: + return ByteString.ofBase64((String) value).getBytes(); + case JAVA_UTIL_DATE: + return new java.util.Date(adjust((Number) value, calendar)); + case JAVA_SQL_DATE: + return new java.sql.Date( + adjust(((Number) value).longValue() * DateTimeUtils.MILLIS_PER_DAY, + calendar)); + case JAVA_SQL_TIME: + return new java.sql.Time(adjust((Number) value, calendar)); + case JAVA_SQL_TIMESTAMP: + return new java.sql.Timestamp(adjust((Number) value, calendar)); + default: + return serialToLocal(type, value); + } + } + + private static long adjust(Number number, Calendar calendar) { + long t = number.longValue(); + if (calendar != null) { + t -= calendar.getTimeZone().getOffset(t); + } + return t; + } + + /** Converts a value from JDBC format to a type that can be serialized as + * JSON. */ + private static Object jdbcToSerial(ColumnMetaData.Rep rep, Object value, + Calendar calendar) { + switch (rep) { + case BYTE_STRING: + return new ByteString((byte[]) value).toBase64String(); + case JAVA_UTIL_DATE: + case JAVA_SQL_TIMESTAMP: + case JAVA_SQL_DATE: + case JAVA_SQL_TIME: + long t = ((Date) value).getTime(); + if (calendar != null) { + t += calendar.getTimeZone().getOffset(t); + } + switch (rep) { + case JAVA_SQL_DATE: + return (int) DateTimeUtils.floorDiv(t, DateTimeUtils.MILLIS_PER_DAY); + case JAVA_SQL_TIME: + return (int) DateTimeUtils.floorMod(t, DateTimeUtils.MILLIS_PER_DAY); + default: + return t; + } + default: + return value; + } + } + + /** Converts a value from internal format to a type that can be serialized + * as JSON. */ + private static Object localToSerial(ColumnMetaData.Rep rep, Object value) { + switch (rep) { + case BYTE_STRING: + return ((ByteString) value).toBase64String(); + default: + return value; + } + } + + /** Converts a list of {@code TypedValue} to a list of values. */ + public static List<Object> values(List<TypedValue> typedValues) { + final List<Object> list = new ArrayList<>(); + for (TypedValue typedValue : typedValues) { + list.add(typedValue.toLocal()); + } + return list; + } + + public Common.TypedValue toProto() { + final Common.TypedValue.Builder builder = Common.TypedValue.newBuilder(); + + Common.Rep protoRep = type.toProto(); + builder.setType(protoRep); + + // Serialize the type into the protobuf + switch (protoRep) { + case BOOLEAN: + case PRIMITIVE_BOOLEAN: + builder.setBoolValue((boolean) value); + break; + case BYTE_STRING: + case STRING: + builder.setStringValueBytes(HBaseZeroCopyByteString.wrap(((String) value).getBytes())); + break; + case PRIMITIVE_CHAR: + case CHARACTER: + builder.setStringValue(Character.toString((char) value)); + break; + case BYTE: + case PRIMITIVE_BYTE: + builder.setNumberValue(Byte.valueOf((byte) value).longValue()); + break; + case DOUBLE: + case PRIMITIVE_DOUBLE: + builder.setDoubleValue((double) value); + break; + case FLOAT: + case PRIMITIVE_FLOAT: + builder.setNumberValue(Float.floatToIntBits((float) value)); + break; + case INTEGER: + case PRIMITIVE_INT: + builder.setNumberValue(Integer.valueOf((int) value).longValue()); + break; + case PRIMITIVE_SHORT: + case SHORT: + builder.setNumberValue(Short.valueOf((short) value).longValue()); + break; + case LONG: + case PRIMITIVE_LONG: + builder.setNumberValue((long) value); + break; + case JAVA_SQL_DATE: + case JAVA_SQL_TIME: + // Persisted as integers + builder.setNumberValue(Integer.valueOf((int) value).longValue()); + break; + case JAVA_SQL_TIMESTAMP: + case JAVA_UTIL_DATE: + // Persisted as longs + builder.setNumberValue((long) value); + break; + case BIG_INTEGER: + byte[] bytes = ((BigInteger) value).toByteArray(); + builder.setBytesValues(com.google.protobuf.ByteString.copyFrom(bytes)); + break; + case BIG_DECIMAL: + final BigDecimal bigDecimal = (BigDecimal) value; + final int scale = bigDecimal.scale(); + final BigInteger bigInt = bigDecimal.toBigInteger(); + builder.setBytesValues(com.google.protobuf.ByteString.copyFrom(bigInt.toByteArray())) + .setNumberValue(scale); + break; + case NUMBER: + builder.setNumberValue(((Number) value).longValue()); + break; + case OBJECT: + if (null == value) { + // We can persist a null value through easily + builder.setNull(true); + break; + } + // Intentional fall-through to RTE because we can't serialize something we have no type + // insight into. + case UNRECOGNIZED: + // Fail? + throw new RuntimeException("Unhandled value: " + protoRep + " " + value.getClass()); + default: + // Fail? + throw new RuntimeException("Unknown serialized type: " + protoRep); + } + + return builder.build(); + } + + public static TypedValue fromProto(Common.TypedValue proto) { + ColumnMetaData.Rep rep = ColumnMetaData.Rep.fromProto(proto.getType()); + + Object value = null; + + // Deserialize the value again + switch (proto.getType()) { + case BOOLEAN: + case PRIMITIVE_BOOLEAN: + value = proto.getBoolValue(); + break; + case BYTE_STRING: + case STRING: + value = proto.getStringValue(); + break; + case PRIMITIVE_CHAR: + case CHARACTER: + value = proto.getStringValue().charAt(0); + break; + case BYTE: + case PRIMITIVE_BYTE: + value = Long.valueOf(proto.getNumberValue()).byteValue(); + break; + case DOUBLE: + case PRIMITIVE_DOUBLE: + value = proto.getDoubleValue(); + break; + case FLOAT: + case PRIMITIVE_FLOAT: + value = Float.intBitsToFloat((int) proto.getNumberValue()); + break; + case INTEGER: + case PRIMITIVE_INT: + value = Long.valueOf(proto.getNumberValue()).intValue(); + break; + case PRIMITIVE_SHORT: + case SHORT: + value = Long.valueOf(proto.getNumberValue()).shortValue(); + break; + case LONG: + case PRIMITIVE_LONG: + value = Long.valueOf(proto.getNumberValue()); + break; + case JAVA_SQL_DATE: + case JAVA_SQL_TIME: + value = Long.valueOf(proto.getNumberValue()).intValue(); + break; + case JAVA_SQL_TIMESTAMP: + case JAVA_UTIL_DATE: + value = proto.getNumberValue(); + break; + case BIG_INTEGER: + value = new BigInteger(proto.getBytesValues().toByteArray()); + break; + case BIG_DECIMAL: + BigInteger bigInt = new BigInteger(proto.getBytesValues().toByteArray()); + value = new BigDecimal(bigInt, (int) proto.getNumberValue()); + break; + case NUMBER: + value = Long.valueOf(proto.getNumberValue()); + break; + case OBJECT: + if (proto.getNull()) { + value = null; + break; + } + // Intentional fall through to RTE. If we sent an object over the wire, it could only + // possibly be null (at this point). Anything else has to be an error. + case UNRECOGNIZED: + // Fail? + throw new RuntimeException("Unhandled type: " + proto.getType()); + default: + // Fail? + throw new RuntimeException("Unknown type: " + proto.getType()); + } + + return new TypedValue(rep, value); + } + + @Override public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof TypedValue) { + TypedValue other = (TypedValue) o; + + if (type != other.type) { + return false; + } + + if (null == value) { + if (null != other.value) { + return false; + } + } + + return value.equals(other.value); + } + + return false; + } +} + +// End TypedValue.java
http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/core/src/main/java/org/apache/calcite/avatica/remote/package-info.java ---------------------------------------------------------------------- diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/remote/package-info.java b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/package-info.java new file mode 100644 index 0000000..1cf3e14 --- /dev/null +++ b/avatica/core/src/main/java/org/apache/calcite/avatica/remote/package-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * JDBC driver that uses remote procedure calls. + */ +@PackageMarker +package org.apache.calcite.avatica.remote; + +import org.apache.calcite.avatica.util.PackageMarker; + +// End package-info.java http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java ---------------------------------------------------------------------- diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java b/avatica/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java new file mode 100644 index 0000000..70f87a7 --- /dev/null +++ b/avatica/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java @@ -0,0 +1,1377 @@ +/* + * 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.AvaticaSite; +import org.apache.calcite.avatica.AvaticaUtils; +import org.apache.calcite.avatica.ColumnMetaData; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.SQLException; +import java.sql.SQLXML; +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.List; +import java.util.Map; + +/** + * Base class for implementing a cursor. + * + * <p>Derived class needs to provide {@link Getter} and can override + * {@link org.apache.calcite.avatica.util.Cursor.Accessor} implementations if it + * wishes.</p> + */ +public abstract class AbstractCursor implements Cursor { + /** + * Slot into which each accessor should write whether the + * value returned was null. + */ + protected final boolean[] wasNull = {false}; + + protected AbstractCursor() { + } + + public boolean wasNull() { + return wasNull[0]; + } + + public List<Accessor> createAccessors(List<ColumnMetaData> types, + Calendar localCalendar, ArrayImpl.Factory factory) { + List<Accessor> accessors = new ArrayList<>(); + for (ColumnMetaData type : types) { + accessors.add( + createAccessor(type, accessors.size(), localCalendar, factory)); + } + return accessors; + } + + protected Accessor createAccessor(ColumnMetaData columnMetaData, int ordinal, + Calendar localCalendar, ArrayImpl.Factory factory) { + // Create an accessor appropriate to the underlying type; the accessor + // can convert to any type in the same family. + Getter getter = createGetter(ordinal); + return createAccessor(columnMetaData, getter, localCalendar, factory); + } + + protected Accessor createAccessor(ColumnMetaData columnMetaData, + Getter getter, Calendar localCalendar, ArrayImpl.Factory factory) { + switch (columnMetaData.type.rep) { + case NUMBER: + switch (columnMetaData.type.id) { + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + case Types.REAL: + case Types.FLOAT: + case Types.DOUBLE: + case Types.NUMERIC: + case Types.DECIMAL: + return new NumberAccessor(getter, columnMetaData.scale); + } + } + switch (columnMetaData.type.id) { + case Types.TINYINT: + return new ByteAccessor(getter); + case Types.SMALLINT: + return new ShortAccessor(getter); + case Types.INTEGER: + return new IntAccessor(getter); + case Types.BIGINT: + return new LongAccessor(getter); + case Types.BOOLEAN: + return new BooleanAccessor(getter); + case Types.REAL: + return new FloatAccessor(getter); + case Types.FLOAT: + case Types.DOUBLE: + return new DoubleAccessor(getter); + case Types.DECIMAL: + return new BigDecimalAccessor(getter); + case Types.CHAR: + switch (columnMetaData.type.rep) { + case PRIMITIVE_CHAR: + case CHARACTER: + return new StringFromCharAccessor(getter, columnMetaData.displaySize); + default: + return new FixedStringAccessor(getter, columnMetaData.displaySize); + } + case Types.VARCHAR: + return new StringAccessor(getter); + case Types.BINARY: + case Types.VARBINARY: + switch (columnMetaData.type.rep) { + case STRING: + return new BinaryFromStringAccessor(getter); + default: + return new BinaryAccessor(getter); + } + case Types.DATE: + switch (columnMetaData.type.rep) { + case PRIMITIVE_INT: + case INTEGER: + case NUMBER: + return new DateFromNumberAccessor(getter, localCalendar); + case JAVA_SQL_DATE: + return new DateAccessor(getter); + default: + throw new AssertionError("bad " + columnMetaData.type.rep); + } + case Types.TIME: + switch (columnMetaData.type.rep) { + case PRIMITIVE_INT: + case INTEGER: + case NUMBER: + return new TimeFromNumberAccessor(getter, localCalendar); + case JAVA_SQL_TIME: + return new TimeAccessor(getter); + default: + throw new AssertionError("bad " + columnMetaData.type.rep); + } + case Types.TIMESTAMP: + switch (columnMetaData.type.rep) { + case PRIMITIVE_LONG: + case LONG: + case NUMBER: + return new TimestampFromNumberAccessor(getter, localCalendar); + case JAVA_SQL_TIMESTAMP: + return new TimestampAccessor(getter); + case JAVA_UTIL_DATE: + return new TimestampFromUtilDateAccessor(getter, localCalendar); + default: + throw new AssertionError("bad " + columnMetaData.type.rep); + } + case Types.ARRAY: + final ColumnMetaData.ArrayType arrayType = + (ColumnMetaData.ArrayType) columnMetaData.type; + final SlotGetter componentGetter = new SlotGetter(); + final Accessor componentAccessor = + createAccessor(ColumnMetaData.dummy(arrayType.component, true), + componentGetter, localCalendar, factory); + return new ArrayAccessor(getter, arrayType.component, componentAccessor, + componentGetter, factory); + case Types.STRUCT: + switch (columnMetaData.type.rep) { + case OBJECT: + final ColumnMetaData.StructType structType = + (ColumnMetaData.StructType) columnMetaData.type; + List<Accessor> accessors = new ArrayList<>(); + for (ColumnMetaData column : structType.columns) { + final Getter fieldGetter = + structType.columns.size() == 1 + ? getter + : new StructGetter(getter, column); + accessors.add( + createAccessor(column, fieldGetter, localCalendar, factory)); + } + return new StructAccessor(getter, accessors); + default: + throw new AssertionError("bad " + columnMetaData.type.rep); + } + case Types.JAVA_OBJECT: + case Types.OTHER: // e.g. map + if (columnMetaData.type.name.startsWith("INTERVAL_")) { + int end = columnMetaData.type.name.indexOf("("); + if (end < 0) { + end = columnMetaData.type.name.length(); + } + TimeUnitRange range = + TimeUnitRange.valueOf( + columnMetaData.type.name.substring("INTERVAL_".length(), end)); + if (range.monthly()) { + return new IntervalYearMonthAccessor(getter, range); + } else { + return new IntervalDayTimeAccessor(getter, range, + columnMetaData.scale); + } + } + return new ObjectAccessor(getter); + default: + throw new RuntimeException("unknown type " + columnMetaData.type.id); + } + } + + protected abstract Getter createGetter(int ordinal); + + public abstract boolean next(); + + /** Accesses a timestamp value as a string. + * The timestamp is in SQL format (e.g. "2013-09-22 22:30:32"), + * not Java format ("2013-09-22 22:30:32.123"). */ + private static String timestampAsString(long v, Calendar calendar) { + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return DateTimeUtils.unixTimestampToString(v); + } + + /** Accesses a date value as a string, e.g. "2013-09-22". */ + private static String dateAsString(int v, Calendar calendar) { + AvaticaUtils.discard(calendar); // time zone shift doesn't make sense + return DateTimeUtils.unixDateToString(v); + } + + /** Accesses a time value as a string, e.g. "22:30:32". */ + private static String timeAsString(int v, Calendar calendar) { + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return DateTimeUtils.unixTimeToString(v); + } + + private static Date longToDate(long v, Calendar calendar) { + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return new Date(v); + } + + static Time intToTime(int v, Calendar calendar) { + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return new Time(v); + } + + static Timestamp longToTimestamp(long v, Calendar calendar) { + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return new Timestamp(v); + } + + /** Implementation of {@link Cursor.Accessor}. */ + static class AccessorImpl implements Accessor { + protected final Getter getter; + + public AccessorImpl(Getter getter) { + assert getter != null; + this.getter = getter; + } + + public boolean wasNull() { + return getter.wasNull(); + } + + public String getString() { + final Object o = getObject(); + return o == null ? null : o.toString(); + } + + public boolean getBoolean() { + return getLong() != 0L; + } + + public byte getByte() { + return (byte) getLong(); + } + + public short getShort() { + return (short) getLong(); + } + + public int getInt() { + return (int) getLong(); + } + + public long getLong() { + throw cannotConvert("long"); + } + + public float getFloat() { + return (float) getDouble(); + } + + public double getDouble() { + throw cannotConvert("double"); + } + + public BigDecimal getBigDecimal() { + throw cannotConvert("BigDecimal"); + } + + public BigDecimal getBigDecimal(int scale) { + throw cannotConvert("BigDecimal with scale"); + } + + public byte[] getBytes() { + throw cannotConvert("byte[]"); + } + + public InputStream getAsciiStream() { + throw cannotConvert("InputStream (ascii)"); + } + + public InputStream getUnicodeStream() { + throw cannotConvert("InputStream (unicode)"); + } + + public InputStream getBinaryStream() { + throw cannotConvert("InputStream (binary)"); + } + + public Object getObject() { + return getter.getObject(); + } + + public Reader getCharacterStream() { + throw cannotConvert("Reader"); + } + + private RuntimeException cannotConvert(String targetType) { + return new RuntimeException("cannot convert to " + targetType + " (" + + this + ")"); + } + + public Object getObject(Map<String, Class<?>> map) { + throw cannotConvert("Object (with map)"); + } + + public Ref getRef() { + throw cannotConvert("Ref"); + } + + public Blob getBlob() { + throw cannotConvert("Blob"); + } + + public Clob getClob() { + throw cannotConvert("Clob"); + } + + public Array getArray() { + throw cannotConvert("Array"); + } + + public Struct getStruct() { + throw cannotConvert("Struct"); + } + + public Date getDate(Calendar calendar) { + throw cannotConvert("Date"); + } + + public Time getTime(Calendar calendar) { + throw cannotConvert("Time"); + } + + public Timestamp getTimestamp(Calendar calendar) { + throw cannotConvert("Timestamp"); + } + + public URL getURL() { + throw cannotConvert("URL"); + } + + public NClob getNClob() { + throw cannotConvert("NClob"); + } + + public SQLXML getSQLXML() { + throw cannotConvert("SQLXML"); + } + + public String getNString() { + throw cannotConvert("NString"); + } + + public Reader getNCharacterStream() { + throw cannotConvert("NCharacterStream"); + } + + public <T> T getObject(Class<T> type) { + throw cannotConvert("Object (with type)"); + } + } + + /** + * Accessor of exact numeric values. The subclass must implement the + * {@link #getLong()} method. + */ + private abstract static class ExactNumericAccessor extends AccessorImpl { + public ExactNumericAccessor(Getter getter) { + super(getter); + } + + public BigDecimal getBigDecimal(int scale) { + final long v = getLong(); + if (v == 0 && getter.wasNull()) { + return null; + } + return BigDecimal.valueOf(v).setScale(scale, RoundingMode.DOWN); + } + + public BigDecimal getBigDecimal() { + final long val = getLong(); + if (val == 0 && getter.wasNull()) { + return null; + } + return BigDecimal.valueOf(val); + } + + public double getDouble() { + return getLong(); + } + + public float getFloat() { + return getLong(); + } + + public abstract long getLong(); + } + + /** + * Accessor that assumes that the underlying value is a {@link Boolean}; + * corresponds to {@link java.sql.Types#BOOLEAN}. + */ + private static class BooleanAccessor extends ExactNumericAccessor { + public BooleanAccessor(Getter getter) { + super(getter); + } + + public boolean getBoolean() { + Boolean o = (Boolean) getObject(); + return o != null && o; + } + + public long getLong() { + return getBoolean() ? 1 : 0; + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Byte}; + * corresponds to {@link java.sql.Types#TINYINT}. + */ + private static class ByteAccessor extends ExactNumericAccessor { + public ByteAccessor(Getter getter) { + super(getter); + } + + public byte getByte() { + Byte o = (Byte) getObject(); + return o == null ? 0 : o; + } + + public long getLong() { + return getByte(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Short}; + * corresponds to {@link java.sql.Types#SMALLINT}. + */ + private static class ShortAccessor extends ExactNumericAccessor { + public ShortAccessor(Getter getter) { + super(getter); + } + + public short getShort() { + Short o = (Short) getObject(); + return o == null ? 0 : o; + } + + public long getLong() { + return getShort(); + } + } + + /** + * Accessor that assumes that the underlying value is an {@link Integer}; + * corresponds to {@link java.sql.Types#INTEGER}. + */ + private static class IntAccessor extends ExactNumericAccessor { + public IntAccessor(Getter getter) { + super(getter); + } + + public int getInt() { + Integer o = (Integer) super.getObject(); + return o == null ? 0 : o; + } + + public long getLong() { + return getInt(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Long}; + * corresponds to {@link java.sql.Types#BIGINT}. + */ + private static class LongAccessor extends ExactNumericAccessor { + public LongAccessor(Getter getter) { + super(getter); + } + + public long getLong() { + Long o = (Long) super.getObject(); + return o == null ? 0 : o; + } + } + + /** + * Accessor of values that are {@link Double} or null. + */ + private abstract static class ApproximateNumericAccessor + extends AccessorImpl { + public ApproximateNumericAccessor(Getter getter) { + super(getter); + } + + public BigDecimal getBigDecimal(int scale) { + final double v = getDouble(); + if (v == 0d && getter.wasNull()) { + return null; + } + return BigDecimal.valueOf(v).setScale(scale, RoundingMode.DOWN); + } + + public BigDecimal getBigDecimal() { + final double v = getDouble(); + if (v == 0 && getter.wasNull()) { + return null; + } + return BigDecimal.valueOf(v); + } + + public abstract double getDouble(); + + public long getLong() { + return (long) getDouble(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Float}; + * corresponds to {@link java.sql.Types#FLOAT}. + */ + private static class FloatAccessor extends ApproximateNumericAccessor { + public FloatAccessor(Getter getter) { + super(getter); + } + + public float getFloat() { + Float o = (Float) getObject(); + return o == null ? 0f : o; + } + + public double getDouble() { + return getFloat(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Double}; + * corresponds to {@link java.sql.Types#DOUBLE}. + */ + private static class DoubleAccessor extends ApproximateNumericAccessor { + public DoubleAccessor(Getter getter) { + super(getter); + } + + public double getDouble() { + Double o = (Double) getObject(); + return o == null ? 0d : o; + } + } + + /** + * Accessor of exact numeric values. The subclass must implement the + * {@link #getLong()} method. + */ + private abstract static class BigNumberAccessor extends AccessorImpl { + public BigNumberAccessor(Getter getter) { + super(getter); + } + + protected abstract Number getNumber(); + + public double getDouble() { + Number number = getNumber(); + return number == null ? 0d : number.doubleValue(); + } + + public float getFloat() { + Number number = getNumber(); + return number == null ? 0f : number.floatValue(); + } + + public long getLong() { + Number number = getNumber(); + return number == null ? 0L : number.longValue(); + } + + public int getInt() { + Number number = getNumber(); + return number == null ? 0 : number.intValue(); + } + + public short getShort() { + Number number = getNumber(); + return number == null ? 0 : number.shortValue(); + } + + public byte getByte() { + Number number = getNumber(); + return number == null ? 0 : number.byteValue(); + } + + public boolean getBoolean() { + Number number = getNumber(); + return number != null && number.doubleValue() != 0; + } + } + + /** + * Accessor that assumes that the underlying value is a {@link BigDecimal}; + * corresponds to {@link java.sql.Types#DECIMAL}. + */ + private static class BigDecimalAccessor extends BigNumberAccessor { + public BigDecimalAccessor(Getter getter) { + super(getter); + } + + protected Number getNumber() { + return (Number) getObject(); + } + + public BigDecimal getBigDecimal(int scale) { + return (BigDecimal) getObject(); + } + + public BigDecimal getBigDecimal() { + return (BigDecimal) getObject(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link Number}; + * corresponds to {@link java.sql.Types#NUMERIC}. + * + * <p>This is useful when numbers have been translated over JSON. JSON + * converts a 0L (0 long) value to the string "0" and back to 0 (0 int). + * So you cannot be sure that the source and target type are the same. + */ + static class NumberAccessor extends BigNumberAccessor { + private final int scale; + + public NumberAccessor(Getter getter, int scale) { + super(getter); + this.scale = scale; + } + + protected Number getNumber() { + return (Number) super.getObject(); + } + + public BigDecimal getBigDecimal(int scale) { + Number n = getNumber(); + if (n == null) { + return null; + } + BigDecimal decimal = AvaticaSite.toBigDecimal(n); + if (0 != scale) { + return decimal.setScale(scale, BigDecimal.ROUND_UNNECESSARY); + } + return decimal; + } + + public BigDecimal getBigDecimal() { + return getBigDecimal(scale); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link String}; + * corresponds to {@link java.sql.Types#CHAR} + * and {@link java.sql.Types#VARCHAR}. + */ + private static class StringAccessor extends AccessorImpl { + public StringAccessor(Getter getter) { + super(getter); + } + + public String getString() { + return (String) getObject(); + } + + @Override public byte[] getBytes() { + return super.getBytes(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link String}; + * corresponds to {@link java.sql.Types#CHAR}. + */ + private static class FixedStringAccessor extends StringAccessor { + protected final Spacer spacer; + + public FixedStringAccessor(Getter getter, int length) { + super(getter); + this.spacer = new Spacer(length); + } + + public String getString() { + String s = super.getString(); + if (s == null) { + return null; + } + return spacer.padRight(s); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link String}; + * corresponds to {@link java.sql.Types#CHAR}. + */ + private static class StringFromCharAccessor extends FixedStringAccessor { + public StringFromCharAccessor(Getter getter, int length) { + super(getter, length); + } + + public String getString() { + Character s = (Character) super.getObject(); + if (s == null) { + return null; + } + return spacer.padRight(s.toString()); + } + } + + /** + * Accessor that assumes that the underlying value is an array of + * {@link org.apache.calcite.avatica.util.ByteString} values; + * corresponds to {@link java.sql.Types#BINARY} + * and {@link java.sql.Types#VARBINARY}. + */ + private static class BinaryAccessor extends AccessorImpl { + public BinaryAccessor(Getter getter) { + super(getter); + } + + //FIXME: Protobuf gets byte[] + @Override public byte[] getBytes() { + Object obj = getObject(); + try { + final ByteString o = (ByteString) obj; + return o == null ? null : o.getBytes(); + } catch (Exception ex) { + return obj == null ? null : (byte[]) obj; + } + } + + @Override public String getString() { + Object o = getObject(); + if (null == o) { + return null; + } + if (o instanceof byte[]) { + return new String((byte[]) o, StandardCharsets.UTF_8); + } else if (o instanceof ByteString) { + return ((ByteString) o).toString(); + } + throw new IllegalStateException("Unhandled value type: " + o.getClass()); + } + } + + /** + * Accessor that assumes that the underlying value is a {@link String}, + * encoding {@link java.sql.Types#BINARY} + * and {@link java.sql.Types#VARBINARY} values in Base64 format. + */ + private static class BinaryFromStringAccessor extends StringAccessor { + public BinaryFromStringAccessor(Getter getter) { + super(getter); + } + + @Override public Object getObject() { + return super.getObject(); + } + + @Override public byte[] getBytes() { + // JSON sends this as a base64-enc string, protobuf can do binary. + Object obj = getObject(); + + if (obj instanceof byte[]) { + // If we already have bytes, just send them back. + return (byte[]) obj; + } + + return getBase64Decoded(); + } + + private byte[] getBase64Decoded() { + final String string = super.getString(); + if (null == string) { + return null; + } + // Need to base64 decode the string. + return ByteString.parseBase64(string); + } + + @Override public String getString() { + final byte[] bytes = getBase64Decoded(); + if (null == bytes) { + return null; + } + // Need to base64 decode the string. + return new String(bytes, StandardCharsets.UTF_8); + } + } + + /** + * Accessor that assumes that the underlying value is a DATE, + * in its default representation {@code int}; + * corresponds to {@link java.sql.Types#DATE}. + */ + private static class DateFromNumberAccessor extends NumberAccessor { + private final Calendar localCalendar; + + public DateFromNumberAccessor(Getter getter, Calendar localCalendar) { + super(getter, 0); + this.localCalendar = localCalendar; + } + + @Override public Object getObject() { + return getDate(localCalendar); + } + + @Override public Date getDate(Calendar calendar) { + final Number v = getNumber(); + if (v == null) { + return null; + } + return longToDate(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, calendar); + } + + @Override public Timestamp getTimestamp(Calendar calendar) { + final Number v = getNumber(); + if (v == null) { + return null; + } + return longToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, + calendar); + } + + @Override public String getString() { + final Number v = getNumber(); + if (v == null) { + return null; + } + return dateAsString(v.intValue(), null); + } + } + + /** + * Accessor that assumes that the underlying value is a Time, + * in its default representation {@code int}; + * corresponds to {@link java.sql.Types#TIME}. + */ + private static class TimeFromNumberAccessor extends NumberAccessor { + private final Calendar localCalendar; + + public TimeFromNumberAccessor(Getter getter, Calendar localCalendar) { + super(getter, 0); + this.localCalendar = localCalendar; + } + + @Override public Object getObject() { + return getTime(localCalendar); + } + + @Override public Time getTime(Calendar calendar) { + final Number v = getNumber(); + if (v == null) { + return null; + } + return intToTime(v.intValue(), calendar); + } + + @Override public Timestamp getTimestamp(Calendar calendar) { + final Number v = getNumber(); + if (v == null) { + return null; + } + return longToTimestamp(v.longValue(), calendar); + } + + @Override public String getString() { + final Number v = getNumber(); + if (v == null) { + return null; + } + return timeAsString(v.intValue(), null); + } + } + + /** + * Accessor that assumes that the underlying value is a TIMESTAMP, + * in its default representation {@code long}; + * corresponds to {@link java.sql.Types#TIMESTAMP}. + */ + private static class TimestampFromNumberAccessor extends NumberAccessor { + private final Calendar localCalendar; + + public TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) { + super(getter, 0); + this.localCalendar = localCalendar; + } + + @Override public Object getObject() { + return getTimestamp(localCalendar); + } + + @Override public Timestamp getTimestamp(Calendar calendar) { + final Number v = getNumber(); + if (v == null) { + return null; + } + return longToTimestamp(v.longValue(), calendar); + } + + @Override public Date getDate(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Date(timestamp.getTime()); + } + + @Override public Time getTime(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Time( + DateTimeUtils.floorMod(timestamp.getTime(), + DateTimeUtils.MILLIS_PER_DAY)); + } + + @Override public String getString() { + final Number v = getNumber(); + if (v == null) { + return null; + } + return timestampAsString(v.longValue(), null); + } + } + + /** + * Accessor that assumes that the underlying value is a DATE, + * represented as a java.sql.Date; + * corresponds to {@link java.sql.Types#DATE}. + */ + private static class DateAccessor extends ObjectAccessor { + public DateAccessor(Getter getter) { + super(getter); + } + + @Override public Date getDate(Calendar calendar) { + java.sql.Date date = (Date) getObject(); + if (date == null) { + return null; + } + if (calendar != null) { + long v = date.getTime(); + v -= calendar.getTimeZone().getOffset(v); + date = new Date(v); + } + return date; + } + + @Override public String getString() { + final int v = getInt(); + if (v == 0 && wasNull()) { + return null; + } + return dateAsString(v, null); + } + + @Override public long getLong() { + Date date = getDate(null); + return date == null + ? 0L + : (date.getTime() / DateTimeUtils.MILLIS_PER_DAY); + } + } + + /** + * Accessor that assumes that the underlying value is a TIME, + * represented as a java.sql.Time; + * corresponds to {@link java.sql.Types#TIME}. + */ + private static class TimeAccessor extends ObjectAccessor { + public TimeAccessor(Getter getter) { + super(getter); + } + + @Override public Time getTime(Calendar calendar) { + Time date = (Time) getObject(); + if (date == null) { + return null; + } + if (calendar != null) { + long v = date.getTime(); + v -= calendar.getTimeZone().getOffset(v); + date = new Time(v); + } + return date; + } + + @Override public String getString() { + final int v = getInt(); + if (v == 0 && wasNull()) { + return null; + } + return timeAsString(v, null); + } + + @Override public long getLong() { + Time time = getTime(null); + return time == null ? 0L + : (time.getTime() % DateTimeUtils.MILLIS_PER_DAY); + } + } + + /** + * Accessor that assumes that the underlying value is a TIMESTAMP, + * represented as a java.sql.Timestamp; + * corresponds to {@link java.sql.Types#TIMESTAMP}. + */ + private static class TimestampAccessor extends ObjectAccessor { + public TimestampAccessor(Getter getter) { + super(getter); + } + + @Override public Timestamp getTimestamp(Calendar calendar) { + Timestamp timestamp = (Timestamp) getObject(); + if (timestamp == null) { + return null; + } + if (calendar != null) { + long v = timestamp.getTime(); + v -= calendar.getTimeZone().getOffset(v); + timestamp = new Timestamp(v); + } + return timestamp; + } + + @Override public Date getDate(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Date(timestamp.getTime()); + } + + @Override public Time getTime(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Time( + DateTimeUtils.floorMod(timestamp.getTime(), + DateTimeUtils.MILLIS_PER_DAY)); + } + + @Override public String getString() { + final long v = getLong(); + if (v == 0 && wasNull()) { + return null; + } + return timestampAsString(v, null); + } + + @Override public long getLong() { + Timestamp timestamp = getTimestamp(null); + return timestamp == null ? 0 : timestamp.getTime(); + } + } + + /** + * Accessor that assumes that the underlying value is a TIMESTAMP, + * represented as a java.util.Date; + * corresponds to {@link java.sql.Types#TIMESTAMP}. + */ + private static class TimestampFromUtilDateAccessor extends ObjectAccessor { + private final Calendar localCalendar; + + public TimestampFromUtilDateAccessor(Getter getter, + Calendar localCalendar) { + super(getter); + this.localCalendar = localCalendar; + } + + @Override public Timestamp getTimestamp(Calendar calendar) { + java.util.Date date = (java.util.Date) getObject(); + if (date == null) { + return null; + } + long v = date.getTime(); + if (calendar != null) { + v -= calendar.getTimeZone().getOffset(v); + } + return new Timestamp(v); + } + + @Override public Date getDate(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Date(timestamp.getTime()); + } + + @Override public Time getTime(Calendar calendar) { + final Timestamp timestamp = getTimestamp(calendar); + if (timestamp == null) { + return null; + } + return new Time( + DateTimeUtils.floorMod(timestamp.getTime(), + DateTimeUtils.MILLIS_PER_DAY)); + } + + @Override public String getString() { + java.util.Date date = (java.util.Date) getObject(); + if (date == null) { + return null; + } + return timestampAsString(date.getTime(), null); + } + + @Override public long getLong() { + Timestamp timestamp = getTimestamp(localCalendar); + return timestamp == null ? 0 : timestamp.getTime(); + } + } + + /** + * Accessor that assumes that the underlying value is a {@code int}; + * corresponds to {@link java.sql.Types#OTHER}. + */ + private static class IntervalYearMonthAccessor extends IntAccessor { + private final TimeUnitRange range; + + public IntervalYearMonthAccessor(Getter getter, TimeUnitRange range) { + super(getter); + this.range = range; + } + + @Override public String getString() { + final int v = getInt(); + if (v == 0 && wasNull()) { + return null; + } + return DateTimeUtils.intervalYearMonthToString(v, range); + } + } + + /** + * Accessor that assumes that the underlying value is a {@code long}; + * corresponds to {@link java.sql.Types#OTHER}. + */ + private static class IntervalDayTimeAccessor extends LongAccessor { + private final TimeUnitRange range; + private final int scale; + + public IntervalDayTimeAccessor(Getter getter, TimeUnitRange range, + int scale) { + super(getter); + this.range = range; + this.scale = scale; + } + + @Override public String getString() { + final long v = getLong(); + if (v == 0 && wasNull()) { + return null; + } + return DateTimeUtils.intervalDayTimeToString(v, range, scale); + } + } + + /** + * Accessor that assumes that the underlying value is an ARRAY; + * corresponds to {@link java.sql.Types#ARRAY}. + */ + static class ArrayAccessor extends AccessorImpl { + final ColumnMetaData.AvaticaType componentType; + final Accessor componentAccessor; + final SlotGetter componentSlotGetter; + final ArrayImpl.Factory factory; + + public ArrayAccessor(Getter getter, + ColumnMetaData.AvaticaType componentType, Accessor componentAccessor, + SlotGetter componentSlotGetter, ArrayImpl.Factory factory) { + super(getter); + this.componentType = componentType; + this.componentAccessor = componentAccessor; + this.componentSlotGetter = componentSlotGetter; + this.factory = factory; + } + + @Override public Object getObject() { + final Object object = super.getObject(); + if (object == null || object instanceof List) { + return object; + } + // The object can be java array in case of user-provided class for row + // storage. + return AvaticaUtils.primitiveList(object); + } + + @Override public Array getArray() { + final List list = (List) getObject(); + if (list == null) { + return null; + } + return new ArrayImpl(list, this); + } + + @Override public String getString() { + final Array array = getArray(); + return array == null ? null : array.toString(); + } + } + + /** + * Accessor that assumes that the underlying value is a STRUCT; + * corresponds to {@link java.sql.Types#STRUCT}. + */ + private static class StructAccessor extends AccessorImpl { + private final List<Accessor> fieldAccessors; + + public StructAccessor(Getter getter, List<Accessor> fieldAccessors) { + super(getter); + this.fieldAccessors = fieldAccessors; + } + + @Override public Object getObject() { + return getStruct(); + } + + @Override public Struct getStruct() { + final Object o = super.getObject(); + if (o == null) { + return null; + } else if (o instanceof List) { + return new StructImpl((List) o); + } else { + final List<Object> list = new ArrayList<>(); + for (Accessor fieldAccessor : fieldAccessors) { + try { + list.add(fieldAccessor.getObject()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + return new StructImpl(list); + } + } + } + + /** + * Accessor that assumes that the underlying value is an OBJECT; + * corresponds to {@link java.sql.Types#JAVA_OBJECT}. + */ + private static class ObjectAccessor extends AccessorImpl { + public ObjectAccessor(Getter getter) { + super(getter); + } + } + + /** Gets a value from a particular field of the current record of this + * cursor. */ + protected interface Getter { + Object getObject(); + + boolean wasNull(); + } + + /** Abstract implementation of {@link Getter}. */ + protected abstract class AbstractGetter implements Getter { + public boolean wasNull() { + return wasNull[0]; + } + } + + /** Implementation of {@link Getter} that returns the current contents of + * a mutable slot. */ + public class SlotGetter implements Getter { + public Object slot; + + public Object getObject() { + return slot; + } + + public boolean wasNull() { + return slot == null; + } + } + + /** Implementation of {@link Getter} that returns the value of a given field + * of the current contents of another getter. */ + public class StructGetter implements Getter { + public final Getter getter; + private final ColumnMetaData columnMetaData; + + public StructGetter(Getter getter, ColumnMetaData columnMetaData) { + this.getter = getter; + this.columnMetaData = columnMetaData; + } + + public Object getObject() { + final Object o = getter.getObject(); + if (o instanceof Object[]) { + Object[] objects = (Object[]) o; + return objects[columnMetaData.ordinal]; + } + try { + final Field field = o.getClass().getField(columnMetaData.label); + return field.get(getter.getObject()); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + public boolean wasNull() { + return getObject() == null; + } + } +} + +// End AbstractCursor.java http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java ---------------------------------------------------------------------- diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java b/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java new file mode 100644 index 0000000..b2d5ae9 --- /dev/null +++ b/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayImpl.java @@ -0,0 +1,204 @@ +/* + * 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.AvaticaUtils; +import org.apache.calcite.avatica.ColumnMetaData; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +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 AbstractCursor.ArrayAccessor accessor; + + public ArrayImpl(List list, AbstractCursor.ArrayAccessor accessor) { + this.list = list; + this.accessor = accessor; + } + + public String getBaseTypeName() throws SQLException { + return accessor.componentType.name; + } + + public int getBaseType() throws SQLException { + return accessor.componentType.id; + } + + public Object getArray() throws SQLException { + return getArray(list); + } + + @Override public String toString() { + final Iterator iterator = list.iterator(); + if (!iterator.hasNext()) { + return "[]"; + } + final StringBuilder buf = new StringBuilder("["); + for (;;) { + accessor.componentSlotGetter.slot = iterator.next(); + try { + append(buf, accessor.componentAccessor.getString()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + accessor.componentSlotGetter.slot = null; + if (!iterator.hasNext()) { + return buf.append("]").toString(); + } + buf.append(", "); + } + } + + private void append(StringBuilder buf, Object o) { + if (o == null) { + buf.append("null"); + } else if (o.getClass().isArray()) { + append(buf, AvaticaUtils.primitiveList(o)); + } else { + buf.append(o); + } + } + + /** + * Converts a list into an array. + * + * <p>If the elements of the list are primitives, converts to an array of + * primitives (e.g. {@code boolean[]}.</p> + * + * @param list List of objects + * + * @return array + * @throws ClassCastException if any element is not of the box type + * @throws NullPointerException if any element is null + */ + @SuppressWarnings("unchecked") + protected Object getArray(List list) throws SQLException { + int i = 0; + switch (accessor.componentType.rep) { + case PRIMITIVE_DOUBLE: + final double[] doubles = new double[list.size()]; + for (double v : (List<Double>) list) { + doubles[i++] = v; + } + return doubles; + case PRIMITIVE_FLOAT: + final float[] floats = new float[list.size()]; + for (float v : (List<Float>) list) { + floats[i++] = v; + } + return floats; + case PRIMITIVE_INT: + final int[] ints = new int[list.size()]; + for (int v : (List<Integer>) list) { + ints[i++] = v; + } + return ints; + case PRIMITIVE_LONG: + final long[] longs = new long[list.size()]; + for (long v : (List<Long>) list) { + longs[i++] = v; + } + return longs; + case PRIMITIVE_SHORT: + final short[] shorts = new short[list.size()]; + for (short v : (List<Short>) list) { + shorts[i++] = v; + } + return shorts; + case PRIMITIVE_BOOLEAN: + final boolean[] booleans = new boolean[list.size()]; + for (boolean v : (List<Boolean>) list) { + booleans[i++] = v; + } + return booleans; + case PRIMITIVE_BYTE: + final byte[] bytes = new byte[list.size()]; + for (byte v : (List<Byte>) list) { + bytes[i++] = v; + } + return bytes; + case PRIMITIVE_CHAR: + final char[] chars = new char[list.size()]; + for (char v : (List<Character>) list) { + chars[i++] = v; + } + return chars; + default: + // fall through + } + final Object[] objects = list.toArray(); + switch (accessor.componentType.id) { + case Types.ARRAY: + final AbstractCursor.ArrayAccessor componentAccessor = + (AbstractCursor.ArrayAccessor) accessor.componentAccessor; + for (i = 0; i < objects.length; i++) { + objects[i] = new ArrayImpl((List) objects[i], componentAccessor); + } + } + return objects; + } + + 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)); + } + + public Object getArray(long index, int count, Map<String, Class<?>> map) + throws SQLException { + throw new UnsupportedOperationException(); // TODO + } + + public ResultSet getResultSet() throws SQLException { + return accessor.factory.create(accessor.componentType, list); + } + + public ResultSet getResultSet(Map<String, Class<?>> map) + throws SQLException { + throw new UnsupportedOperationException(); // TODO + } + + public ResultSet getResultSet(long index, int count) throws SQLException { + throw new UnsupportedOperationException(); // TODO + } + + public ResultSet getResultSet(long index, int count, + Map<String, Class<?>> map) throws SQLException { + throw new UnsupportedOperationException(); // TODO + } + + public void free() throws SQLException { + // nothing to do + } + + /** Factory that can create a result set based on a list of values. */ + public interface Factory { + ResultSet create(ColumnMetaData.AvaticaType elementType, + Iterable<Object> iterable); + } +} + +// End ArrayImpl.java http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayIteratorCursor.java ---------------------------------------------------------------------- diff --git a/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayIteratorCursor.java b/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayIteratorCursor.java new file mode 100644 index 0000000..0477984 --- /dev/null +++ b/avatica/core/src/main/java/org/apache/calcite/avatica/util/ArrayIteratorCursor.java @@ -0,0 +1,41 @@ +/* + * 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 java.util.Iterator; + +/** + * Implementation of {@link Cursor} on top of an + * {@link java.util.Iterator} that + * returns an array of {@link Object} for each row. + */ +public class ArrayIteratorCursor extends IteratorCursor<Object[]> { + /** + * Creates an ArrayEnumeratorCursor. + * + * @param iterator Iterator + */ + public ArrayIteratorCursor(Iterator<Object[]> iterator) { + super(iterator); + } + + protected Getter createGetter(int ordinal) { + return new ArrayGetter(ordinal); + } +} + +// End ArrayIteratorCursor.java
