content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to you under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.calcite.avatica.util;
  
  import org.apache.calcite.avatica.AvaticaSite;
  import org.apache.calcite.avatica.AvaticaUtils;
  import org.apache.calcite.avatica.ColumnMetaData;
  
  import org.joou.UByte;
  import org.joou.UInteger;
  import org.joou.ULong;
  import org.joou.UShort;
  
  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.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.SQLDataException;
  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 columnMetaData.signed ? new ByteAccessor(getter) : new UByteAccessor(getter);
      case Types.SMALLINT:
        return columnMetaData.signed ? new ShortAccessor(getter) : new UShortAccessor(getter);
      case Types.INTEGER:
        return columnMetaData.signed ? new IntAccessor(getter) : new UIntAccessor(getter);
      case Types.BIGINT:
        return columnMetaData.signed ? new LongAccessor(getter) : new ULongAccessor(getter);
      case Types.BOOLEAN:
      case Types.BIT:
        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 NumberAccessor(getter, columnMetaData.scale);
      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:
      case Types.NVARCHAR:
        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, localCalendar);
        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, columnMetaData.precision);
        case JAVA_SQL_TIME:
          return new TimeAccessor(getter, localCalendar);
        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, columnMetaData.precision);
        case JAVA_SQL_TIMESTAMP:
          return new TimestampAccessor(getter, localCalendar, columnMetaData.precision);
        case JAVA_UTIL_DATE:
          return new TimestampFromUtilDateAccessor(getter, localCalendar, columnMetaData.precision);
        default:
          throw new AssertionError("bad " + columnMetaData.type.rep);
        }
      case Types.TIME_WITH_TIMEZONE:
        switch (columnMetaData.type.rep) {
        case STRING:
          return new StringAccessor(getter);
        default:
          throw new AssertionError("bad " + columnMetaData.type.rep);
        }
      case Types.TIMESTAMP_WITH_TIMEZONE:
        switch (columnMetaData.type.rep) {
        case STRING:
          return new StringAccessor(getter);
        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.getComponent(), true),
                componentGetter, localCalendar, factory);
        return new ArrayAccessor(getter, arrayType.getComponent(), 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) {
            accessors.add(
                createAccessor(column, new StructGetter(getter, column), localCalendar, factory));
          }
          return new StructAccessor(getter, accessors);
        default:
          throw new AssertionError("bad " + columnMetaData.type.rep);
        }
      case Types.JAVA_OBJECT:
      case Types.NULL:
      case Types.OTHER: // e.g. map
        if (columnMetaData.type.getName().startsWith("INTERVAL_")) {
          int end = columnMetaData.type.getName().indexOf("(");
          if (end < 0) {
            end = columnMetaData.type.getName().length();
          }
          TimeUnitRange range =
              TimeUnitRange.valueOf(
                  columnMetaData.type.getName().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, int precision) {
      if (calendar != null) {
        v -= calendar.getTimeZone().getOffset(v);
      }
      return DateTimeUtils.unixTimestampToString(v, precision);
    }
  
    /** 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, int precision) {
      if (calendar != null) {
        v -= calendar.getTimeZone().getOffset(v);
      }
      return DateTimeUtils.unixTimeToString(v, precision);
    }
  
    /** Implementation of {@link Cursor.Accessor}. */
    static class AccessorImpl implements Accessor {
      protected final Getter getter;
  
      AccessorImpl(Getter getter) {
        assert getter != null;
        this.getter = getter;
      }
  
      public boolean wasNull() throws SQLException {
        return getter.wasNull();
      }
  
      public String getString() throws SQLException {
        final Object o = getObject();
        return o == null ? null : o.toString();
      }
  
      public boolean getBoolean() throws SQLException {
        return getLong() != 0L;
      }
  
      public byte getByte() throws SQLException {
        return (byte) getLong();
      }
  
      @Override
      public UByte getUByte() throws SQLException {
        return UByte.valueOf(getLong());
      }
  
      public short getShort() throws SQLException {
        return (short) getLong();
      }
  
      @Override
      public UShort getUShort() throws SQLException {
        return UShort.valueOf(String.valueOf(getLong()));
      }
  
      public int getInt() throws SQLException {
        return (int) getLong();
      }
  
      @Override
      public UInteger getUInt() throws SQLException {
        return UInteger.valueOf(getLong());
      }
  
      public long getLong() throws SQLException {
        throw cannotConvert("long");
      }
  
      @Override
      public ULong getULong() throws SQLException {
        return ULong.valueOf(getBigDecimal().toBigInteger());
      }
  
      public float getFloat() throws SQLException {
        return (float) getDouble();
      }
  
      public double getDouble() throws SQLException {
        throw cannotConvert("double");
      }
  
      public BigDecimal getBigDecimal() throws SQLException {
        throw cannotConvert("BigDecimal");
      }
  
      public BigDecimal getBigDecimal(int scale) throws SQLException {
        throw cannotConvert("BigDecimal with scale");
      }
  
      public byte[] getBytes() throws SQLException {
        throw cannotConvert("byte[]");
      }
  
      public InputStream getAsciiStream() throws SQLException {
        throw cannotConvert("InputStream (ascii)");
      }
  
      public InputStream getUnicodeStream() throws SQLException {
        throw cannotConvert("InputStream (unicode)");
      }
  
      public InputStream getBinaryStream() throws SQLException {
        throw cannotConvert("InputStream (binary)");
      }
  
      public Object getObject() throws SQLException {
        return getter.getObject();
      }
  
      public Reader getCharacterStream() throws SQLException {
        throw cannotConvert("Reader");
      }
  
      private SQLException cannotConvert(String targetType) throws SQLException {
        return new SQLDataException("cannot convert to " + targetType + " ("
            + this + ")");
      }
  
      public Object getObject(Map<String, Class<?>> map) throws SQLException {
        throw cannotConvert("Object (with map)");
      }
  
      public Ref getRef() throws SQLException {
        throw cannotConvert("Ref");
      }
  
      public Blob getBlob() throws SQLException {
        throw cannotConvert("Blob");
      }
  
      public Clob getClob() throws SQLException {
        throw cannotConvert("Clob");
      }
  
      public Array getArray() throws SQLException {
        throw cannotConvert("Array");
      }
  
      public Struct getStruct() throws SQLException {
        throw cannotConvert("Struct");
      }
  
      public Date getDate(Calendar calendar) throws SQLException {
        throw cannotConvert("Date");
      }
  
      public Time getTime(Calendar calendar) throws SQLException {
        throw cannotConvert("Time");
      }
  
      public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        throw cannotConvert("Timestamp");
      }
  
      public URL getURL() throws SQLException {
        throw cannotConvert("URL");
      }
  
      public NClob getNClob() throws SQLException {
        throw cannotConvert("NClob");
      }
  
      public SQLXML getSQLXML() throws SQLException {
        throw cannotConvert("SQLXML");
      }
  
      public String getNString() throws SQLException {
        throw cannotConvert("NString");
      }
  
      public Reader getNCharacterStream() throws SQLException {
        throw cannotConvert("NCharacterStream");
      }
  
      public <T> T getObject(Class<T> type) throws SQLException {
        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 {
      private ExactNumericAccessor(Getter getter) {
        super(getter);
      }
  
      public BigDecimal getBigDecimal(int scale) throws SQLException {
        final long v = getLong();
        if (v == 0 && getter.wasNull()) {
          return null;
        }
        return BigDecimal.valueOf(v).setScale(scale, RoundingMode.DOWN);
      }
  
      public BigDecimal getBigDecimal() throws SQLException {
        final long val = getLong();
        if (val == 0 && getter.wasNull()) {
          return null;
        }
        return BigDecimal.valueOf(val);
      }
  
      public double getDouble() throws SQLException {
        return getLong();
      }
  
      public float getFloat() throws SQLException {
        return getLong();
      }
  
      public abstract long getLong() throws SQLException;
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link Boolean};
     * corresponds to {@link java.sql.Types#BOOLEAN}.
     */
    private static class BooleanAccessor extends ExactNumericAccessor {
      private BooleanAccessor(Getter getter) {
        super(getter);
      }
  
      public boolean getBoolean() throws SQLException {
        Boolean o = (Boolean) getObject();
        return o != null && o;
      }
  
      public long getLong() throws SQLException {
        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 {
      private ByteAccessor(Getter getter) {
        super(getter);
      }
  
      public byte getByte() throws SQLException {
        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 {
        return getByte();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link UByte};
     * corresponds to {@link java.sql.Types#TINYINT} with unsigned flag.
     */
    private static class UByteAccessor extends ExactNumericAccessor {
      private UByteAccessor(Getter getter) {
        super(getter);
      }
  
      public UByte getUByte() throws SQLException {
        Object obj = getObject();
        if (null == obj) {
          return UByte.valueOf(0);
        } else if (obj instanceof Integer) {
          return UByte.valueOf(((Integer) obj).byteValue());
        }
        return UByte.valueOf(String.valueOf(obj));
      }
  
      public long getLong() throws SQLException {
        return getUByte().longValue();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link Short};
     * corresponds to {@link java.sql.Types#SMALLINT}.
     */
    private static class ShortAccessor extends ExactNumericAccessor {
      private ShortAccessor(Getter getter) {
        super(getter);
      }
  
      public short getShort() throws SQLException {
        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 {
        return getShort();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link UShort};
     * corresponds to {@link java.sql.Types#SMALLINT} with unsigned flag.
     */
    private static class UShortAccessor extends ExactNumericAccessor {
      private UShortAccessor(Getter getter) {
        super(getter);
      }
  
      public UShort getUShort() throws SQLException {
        Object obj = getObject();
        if (null == obj) {
          return UShort.valueOf(0);
        } else if (obj instanceof Integer) {
          return UShort.valueOf(((Integer) obj).shortValue());
        }
        return UShort.valueOf(String.valueOf(obj));
      }
  
      public long getLong() throws SQLException {
        return getUShort().longValue();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is an {@link Integer};
     * corresponds to {@link java.sql.Types#INTEGER}.
     */
    private static class IntAccessor extends ExactNumericAccessor {
      private IntAccessor(Getter getter) {
        super(getter);
      }
  
      public int getInt() throws SQLException {
        Integer o = (Integer) super.getObject();
        return o == null ? 0 : o;
      }
  
      public long getLong() throws SQLException {
        return getInt();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is an {@link UInteger};
     * corresponds to {@link java.sql.Types#INTEGER} with unsigned flag.
     */
    private static class UIntAccessor extends ExactNumericAccessor {
      private UIntAccessor(Getter getter) {
        super(getter);
      }
  
      public UInteger getUInt() throws SQLException {
        Object o = getObject();
        return UInteger.valueOf(o == null ? "0" : String.valueOf(o));
      }
  
      public long getLong() throws SQLException {
        return getUInt().longValue();
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link Long};
     * corresponds to {@link java.sql.Types#BIGINT}.
     */
    private static class LongAccessor extends ExactNumericAccessor {
      private LongAccessor(Getter getter) {
        super(getter);
      }
  
      public long getLong() throws SQLException {
        Long o = (Long) super.getObject();
        return o == null ? 0 : o;
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link ULong};
     * corresponds to {@link java.sql.Types#BIGINT} with unsigned flag.
     */
    private static class ULongAccessor extends ExactNumericAccessor {
      private ULongAccessor(Getter getter) {
        super(getter);
      }
  
      public ULong getULong() throws SQLException {
        Object o = getObject();
        return ULong.valueOf(o == null ? "0" : String.valueOf(o));
      }
  
      @Override
      public long getLong() throws SQLException {
        return getULong().longValue();
      }
    }
  
    /**
     * Accessor of values that are {@link Double} or null.
     */
    private abstract static class ApproximateNumericAccessor
        extends AccessorImpl {
      private ApproximateNumericAccessor(Getter getter) {
        super(getter);
      }
  
      public BigDecimal getBigDecimal(int scale) throws SQLException {
        final double v = getDouble();
        if (v == 0d && getter.wasNull()) {
          return null;
        }
        return BigDecimal.valueOf(v).setScale(scale, RoundingMode.DOWN);
      }
  
      public BigDecimal getBigDecimal() throws SQLException {
        final double v = getDouble();
        if (v == 0 && getter.wasNull()) {
          return null;
        }
        return BigDecimal.valueOf(v);
      }
  
      public abstract double getDouble() throws SQLException;
  
      public long getLong() throws SQLException {
        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 {
      private FloatAccessor(Getter getter) {
        super(getter);
      }
  
      public float getFloat() throws SQLException {
        Float o = (Float) getObject();
        return o == null ? 0f : o;
      }
  
      public double getDouble() throws SQLException {
        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 {
      private DoubleAccessor(Getter getter) {
        super(getter);
      }
  
      public double getDouble() throws SQLException {
        Object obj = getObject();
        if (null == obj) {
          return 0d;
        } else if (obj instanceof BigDecimal) {
          return ((BigDecimal) obj).doubleValue();
        }
        return (Double) obj;
      }
    }
  
    /**
     * Accessor of exact numeric values. The subclass must implement the
     * {@link #getLong()} method.
     */
    private abstract static class BigNumberAccessor extends AccessorImpl {
      private BigNumberAccessor(Getter getter) {
        super(getter);
      }
  
      protected abstract Number getNumber() throws SQLException;
  
      public double getDouble() throws SQLException {
        Number number = getNumber();
        return number == null ? 0d : number.doubleValue();
      }
  
      public float getFloat() throws SQLException {
        Number number = getNumber();
        return number == null ? 0f : number.floatValue();
      }
  
      public long getLong() throws SQLException {
        Number number = getNumber();
        return number == null ? 0L : number.longValue();
      }
  
      public int getInt() throws SQLException {
        Number number = getNumber();
        return number == null ? 0 : number.intValue();
      }
  
      public short getShort() throws SQLException {
        Number number = getNumber();
        return number == null ? 0 : number.shortValue();
      }
  
      public byte getByte() throws SQLException {
        Number number = getNumber();
        return number == null ? 0 : number.byteValue();
      }
  
      public boolean getBoolean() throws SQLException {
        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 {
      private BigDecimalAccessor(Getter getter) {
        super(getter);
      }
  
      protected Number getNumber() throws SQLException {
        return (Number) getObject();
      }
  
      public BigDecimal getBigDecimal(int scale) throws SQLException {
        Number number = getNumber();
        return number == null || number instanceof BigDecimal
            ? (BigDecimal) number : BigDecimal.valueOf(number.longValue());
      }
  
      public BigDecimal getBigDecimal() throws SQLException {
        Number number = getNumber();
        return number == null || number instanceof BigDecimal
            ? (BigDecimal) number : BigDecimal.valueOf(number.longValue());
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a {@link Number};
     * corresponds to {@link java.sql.Types#NUMERIC}
     * or {@link java.sql.Types#DECIMAL}.
     *
     * <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;
  
      NumberAccessor(Getter getter, int scale) {
        super(getter);
        this.scale = scale;
      }
  
      protected Number getNumber() throws SQLException {
        return (Number) super.getObject();
      }
  
      //FIXME There are several issues with this, the code below simply implements
      //a previous behaviour codified by the Calcite test suite.
      //
      // 1. It interprets a scale of 0 as a NOOP parameter, it should in fact drop all fractionals
      // 2. The scale from MetaData is NOT applied to BigDecimal values. Why ?
      // 3. Metadata scale is only applied for getBigDecimal(), and only in this Accessor. Why ?
      public BigDecimal getBigDecimal(int scale) throws SQLException {
        Number n = getNumber();
        if (n == null) {
          return null;
        }
        BigDecimal decimal = AvaticaSite.toBigDecimal(n);
        if (0 != scale && !(n instanceof BigDecimal)) {
          return decimal.setScale(scale, RoundingMode.DOWN);
        }
        return decimal;
      }
  
      public BigDecimal getBigDecimal() throws SQLException {
        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 {
      private StringAccessor(Getter getter) {
        super(getter);
      }
  
      public String getString() throws SQLException {
        final Object obj = getObject();
        if (obj instanceof String) {
          return (String) obj;
        }
        return null == obj ? null : obj.toString();
      }
  
      @Override public byte[] getBytes() throws SQLException {
        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;
  
      private FixedStringAccessor(Getter getter, int length) {
        super(getter);
        this.spacer = new Spacer(length);
      }
  
      public String getString() throws SQLException {
        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 {
      private StringFromCharAccessor(Getter getter, int length) {
        super(getter, length);
      }
  
      public String getString() throws SQLException {
        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 {
      private BinaryAccessor(Getter getter) {
        super(getter);
      }
  
      //FIXME: Protobuf gets byte[]
      @Override public byte[] getBytes() throws SQLException {
        Object obj = getObject();
        if (null == obj) {
          return null;
        }
        if (obj instanceof ByteString) {
          return ((ByteString) obj).getBytes();
        } else if (obj instanceof String) {
          // 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");
        }
      }
  
      @Override public String getString() throws SQLException {
        Object o = getObject();
        if (null == o) {
          return null;
        }
        if (o instanceof byte[]) {
          return AvaticaUtils.newStringUtf8((byte[]) o);
        } 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 {
      private BinaryFromStringAccessor(Getter getter) {
        super(getter);
      }
  
      @Override public Object getObject() throws SQLException {
        return super.getObject();
      }
  
      @Override public byte[] getBytes() throws SQLException {
        // 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() throws SQLException {
        final String string = super.getString();
        if (null == string) {
          return null;
        }
        // Need to base64 decode the string.
        return ByteString.parseBase64(string);
      }
  
      @Override public String getString() throws SQLException {
        final byte[] bytes = getBase64Decoded();
        if (null == bytes) {
          return null;
        }
        // Need to base64 decode the string.
        return AvaticaUtils.newStringUtf8(bytes);
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a DATE,
     * in its default representation {@code int};
     * corresponds to {@link java.sql.Types#DATE}.
     *
     * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
     * timezone instead of to UTC. Providing the default calendar will return the underlying value
     * unmodified. This differs from {@link DateAccessor} which uses the timezone offset from UTC.
     */
    static class DateFromNumberAccessor extends NumberAccessor {
      private final Calendar localCalendar;
  
      DateFromNumberAccessor(Getter getter, Calendar localCalendar) {
        super(getter, 0);
        this.localCalendar = localCalendar;
      }
  
      @Override public Object getObject() throws SQLException {
        return getDate(localCalendar);
      }
  
      @Override public Date getDate(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return DateTimeUtils.unixDateToSqlDate(v.intValue(), calendar);
      }
  
      @Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return DateTimeUtils.unixTimestampToSqlTimestamp(
            v.longValue() * DateTimeUtils.MILLIS_PER_DAY,
            calendar);
      }
  
      @Override public String getString() throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return dateAsString(v.intValue(), null);
      }
  
      protected Number getNumber() throws SQLException {
        final Object value = super.getObject();
        if (value == null) {
          return null;
        }
        if (value instanceof Date) {
          long time = ((Date) value).getTime();
          time -= localCalendar.getTimeZone().getOffset(time);
          return time / DateTimeUtils.MILLIS_PER_DAY;
        }
        return (Number) value;
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a Time,
     * in its default representation {@code int};
     * corresponds to {@link java.sql.Types#TIME}.
     *
     * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
     * timezone instead of to UTC. Passing in the default calendar will return the underlying value
     * unmodified. This differs from {@link TimeAccessor} which uses the timezone offset from UTC.
     */
    static class TimeFromNumberAccessor extends NumberAccessor {
      private final Calendar localCalendar;
      private final int precision;
  
      TimeFromNumberAccessor(Getter getter, Calendar localCalendar, int precision) {
        super(getter, 0);
        this.localCalendar = localCalendar;
        this.precision = precision;
      }
  
      @Override public Object getObject() throws SQLException {
        return getTime(localCalendar);
      }
  
      @Override public Time getTime(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return DateTimeUtils.unixTimeToSqlTime(v.intValue(), calendar);
      }
  
      @Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), calendar);
      }
  
      @Override public String getString() throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return timeAsString(v.intValue(), null, this.precision);
      }
  
      protected Number getNumber() throws SQLException {
        final Object v = super.getObject();
        if (v == null) {
          return null;
        }
        if (v instanceof Time) {
          return ((Time) v).getTime();
        }
        return (Number) v;
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a TIMESTAMP,
     * in its default representation {@code long};
     * corresponds to {@link java.sql.Types#TIMESTAMP}.
     *
     * <p>For backwards compatibility, timezone offsets are calculated in relation to the local
     * timezone instead of to UTC. Passing in the default calendar will return the underlying value
     * unmodified. This differs from {@link TimestampAccessor} which uses the timezone offset from
     * UTC.
     */
    static class TimestampFromNumberAccessor extends NumberAccessor {
      private final Calendar localCalendar;
      private final int precision;
  
      TimestampFromNumberAccessor(Getter getter, Calendar localCalendar, int precision) {
        super(getter, 0);
        this.localCalendar = localCalendar;
        this.precision = precision;
      }
  
      @Override public Object getObject() throws SQLException {
        return getTimestamp(localCalendar);
      }
  
      @Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), calendar);
      }
  
      @Override public Date getDate(Calendar calendar) throws SQLException {
        final Timestamp timestamp = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Date(timestamp.getTime());
      }
  
      @Override public Time getTime(Calendar calendar) throws SQLException {
        final Timestamp timestamp = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Time(timestamp.getTime());
      }
  
      @Override public String getString() throws SQLException {
        final Number v = getNumber();
        if (v == null) {
          return null;
        }
        return timestampAsString(v.longValue(), null, this.precision);
      }
  
      protected Number getNumber() throws SQLException {
        final Object v = super.getObject();
        if (v == null) {
          return null;
        }
        if (v instanceof Timestamp) {
          return ((Timestamp) v).getTime();
        }
        return (Number) v;
      }
    }
  
    /**
     * Accessor that assumes that the underlying value is a DATE,
     * represented as a java.sql.Date;
     * corresponds to {@link java.sql.Types#DATE}.
     *
     * <p>Since the underlying value does not provide a timezone, timezone offsets are calculated in
     * relation to UTC. Passing in the default calendar will convert the date from UTC to local time.
     * This differs from {@link DateFromNumberAccessor} which calculates timezone offsets in relation
     * to local time, and {@link Date} which provides its value in local time unless otherwise
     * specified.
     */
    static class DateAccessor extends ObjectAccessor {
      private final Calendar localCalendar;
  
      DateAccessor(Getter getter, Calendar localCalendar) {
        super(getter);
        this.localCalendar = localCalendar;
      }
  
      @Override public Date getDate(Calendar calendar) throws SQLException {
        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() throws SQLException {
        final java.sql.Date date = getDate(null);
        if (date == null) {
          return null;
        }
        final int unix = DateTimeUtils.sqlDateToUnixDate(date, localCalendar);
        return dateAsString(unix, null);
  
      }
  
      @Override public long getLong() throws SQLException {
        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}.
     *
     * <p>Since the underlying value does not provide a timezone, timezone offsets are calculated in
     * relation to UTC. Passing in the default calendar will convert the time from UTC to local time.
     * This differs from {@link TimeFromNumberAccessor} which calculates timezone offsets in relation
     * to local time, and {@link Time} which provides its value in local time unless otherwise
     * specified.
     */
    static class TimeAccessor extends ObjectAccessor {
      private final Calendar localCalendar;
  
      TimeAccessor(Getter getter, Calendar localCalendar) {
        super(getter);
        this.localCalendar = localCalendar;
      }
  
      @Override public Time getTime(Calendar calendar) throws SQLException {
        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() throws SQLException {
        final Time time = getTime(null);
        if (time == null) {
          return null;
        }
        final int unix = DateTimeUtils.sqlTimeToUnixTime(time, localCalendar);
        // java.sql.Time only supports a precision of 0
        return timeAsString(unix, null, 0);
      }
  
      @Override public long getLong() throws SQLException {
        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}.
     *
     * <p>Since the underlying value does not provide a timezone, timezone offsets are calculated in
     * relation to UTC. Passing in the default calendar will convert the timestamp from UTC to local
     * time. This differs from {@link TimeFromNumberAccessor} which calculates timezone offsets in
     * relation to local time, and {@link Timestamp} which provides its value in local time unless
     * otherwise specified.
     */
    static class TimestampAccessor extends ObjectAccessor {
      private final Calendar localCalendar;
      private final int precision;
  
      TimestampAccessor(Getter getter, Calendar localCalendar, int precision) {
        super(getter);
        this.localCalendar = localCalendar;
        this.precision = precision;
      }
  
      @Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        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) throws SQLException {
        final Timestamp timestamp  = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Date(timestamp.getTime());
      }
  
      @Override public Time getTime(Calendar calendar) throws SQLException {
        final Timestamp timestamp  = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Time(timestamp.getTime());
      }
  
      @Override public String getString() throws SQLException {
        final Timestamp timestamp = getTimestamp(null);
        if (timestamp == null) {
          return null;
        }
        final long unix =
            DateTimeUtils.sqlTimestampToUnixTimestamp(timestamp, localCalendar);
        return timestampAsString(unix, null, this.precision);
      }
  
      @Override public long getLong() throws SQLException {
        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}.
     *
     * <p>Since the underlying value does not provide a timezone, timezone offsets are calculated in
     * relation to UTC. Passing in the default calendar will convert the timestamp from UTC to local
     * time. This differs from {@link java.util.Date} which provides its value in local time unless
     * otherwise specified.
     */
    static class TimestampFromUtilDateAccessor extends ObjectAccessor {
      private final Calendar localCalendar;
      private final int precision;
  
      TimestampFromUtilDateAccessor(Getter getter,
          Calendar localCalendar, int precision) {
        super(getter);
        this.localCalendar = localCalendar;
        this.precision = precision;
      }
  
      @Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
        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) throws SQLException {
        final Timestamp timestamp  = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Date(timestamp.getTime());
      }
  
      @Override public Time getTime(Calendar calendar) throws SQLException {
        final Timestamp timestamp  = getTimestamp(calendar);
        if (timestamp == null) {
          return null;
        }
        return new Time(timestamp.getTime());
      }
  
      @Override public String getString() throws SQLException {
        final java.util.Date date = (java.util.Date) getObject();
        if (date == null) {
          return null;
        }
        final long unix = DateTimeUtils.utilDateToUnixTimestamp(date, localCalendar);
        return timestampAsString(unix, null, this.precision);
      }
  
      @Override public long getLong() throws SQLException {
        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;
  
      private IntervalYearMonthAccessor(Getter getter, TimeUnitRange range) {
        super(getter);
        this.range = range;
      }
  
      @Override public String getString() throws SQLException {
        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;
  
      private IntervalDayTimeAccessor(Getter getter, TimeUnitRange range,
          int scale) {
        super(getter);
        this.range = range;
        this.scale = scale;
      }
  
      @Override public String getString() throws SQLException {
        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}.
     */
    public 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() throws SQLException {
        final Object object = super.getObject();
        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 instanceof Object[] && componentType.id != Types.STRUCT
                      ? ((Object[]) val)[0]
                      : val;
              convertedValues.add(convertValue());
            }
          }
          return convertedValues;
        }
        // The object can be java array in case of user-provided class for row storage.
        return AvaticaUtils.primitiveList(object);
      }
  
      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.REAL:
          return componentAccessor.getFloat();
        case Types.FLOAT:
        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:
        case Types.OTHER:
          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;
        }
        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 {
        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;
  
      private StructAccessor(Getter getter, List<Accessor> fieldAccessors) {
        super(getter);
        this.fieldAccessors = fieldAccessors;
      }
  
      @Override public Object getObject() throws SQLException {
        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 {
          final List<Object> list = new ArrayList<>();
          for (Accessor fieldAccessor : fieldAccessors) {
            list.add(fieldAccessor.getObject());
          }
          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 {
      private ObjectAccessor(Getter getter) {
        super(getter);
      }
    }
  
    /** Gets a value from a particular field of the current record of this
     * cursor. */
    protected interface Getter {
      Object getObject() throws SQLException;
  
      boolean wasNull() throws SQLException;
    }
  
    /** Abstract implementation of {@link Getter}. */
    protected abstract class AbstractGetter implements Getter {
      public boolean wasNull() throws SQLException {
        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() throws SQLException {
        return slot;
      }
  
      public boolean wasNull() throws SQLException {
        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() throws SQLException {
        try {
          final Object o = getter.getObject();
          if (o instanceof Object[]) {
            Object[] objects = (Object[]) o;
            return objects[columnMetaData.ordinal];
          }
          final Field field = o.getClass().getField(columnMetaData.label);
          return field.get(getter.getObject());
        } catch (IllegalAccessException | NoSuchFieldException e) {
          throw new SQLException(e);
        }
      }
  
      public boolean wasNull() throws SQLException {
        return getObject() == null;
      }
    }
  }
  
  // End AbstractCursor.java
  