All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.spanner.jdbc.JdbcResultSet Maven / Gradle / Ivy

There is a newer version: 2.25.2
Show newest version
/*
 * Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.cloud.spanner.jdbc;

import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.getMainTypeCode;

import com.google.cloud.spanner.ResultSets;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.Code;
import com.google.cloud.spanner.Value;
import com.google.cloud.spanner.connection.PartitionedQueryResultSet;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
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.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.annotation.Nonnull;

/** Implementation of {@link ResultSet} for Cloud Spanner */
class JdbcResultSet extends AbstractJdbcResultSet {

  static JdbcResultSet of(com.google.cloud.spanner.ResultSet resultSet) {
    return of(resultSet, ImmutableSet.of());
  }

  static JdbcResultSet of(
      com.google.cloud.spanner.ResultSet resultSet,
      ImmutableSet columnsAllowedUncheckedLongCastToShort) {
    return new JdbcResultSet(
        null,
        Preconditions.checkNotNull(resultSet),
        Preconditions.checkNotNull(columnsAllowedUncheckedLongCastToShort));
  }

  static JdbcResultSet of(Statement statement, com.google.cloud.spanner.ResultSet resultSet) {
    // Return a JDBC version of a PartitionedQueryResultSet if the Cloud Spanner Java client
    // returned a PartitionedQueryResultSet.
    if (resultSet instanceof PartitionedQueryResultSet) {
      return JdbcPartitionedQueryResultSet.of(statement, (PartitionedQueryResultSet) resultSet);
    }
    return new JdbcResultSet(
        Preconditions.checkNotNull(statement), Preconditions.checkNotNull(resultSet));
  }

  /**
   * Creates a JDBC result set by copying the given Spanner {@link
   * com.google.cloud.spanner.ResultSet}. This can be used for result sets that are known not to be
   * too large. This type of result set should be preferred for results that are unlikely to be
   * closed by the client application, such as the returned generated keys of an update statement.
   * The copy will not hold on to a reference to a Cloud Spanner session or result stream. All the
   * data in the given Spanner {@link com.google.cloud.spanner.ResultSet} have been consumed after
   * calling this method. The {@link com.google.cloud.spanner.ResultSet} is not closed by this
   * method.
   */
  static JdbcResultSet copyOf(@Nonnull com.google.cloud.spanner.ResultSet resultSet) {
    Preconditions.checkNotNull(resultSet);
    // Make the copy first. This ensures that ResultSet#next() has been called at least once, which
    // is necessary to get the type of the result set.
    ImmutableList rows = ImmutableList.copyOf(new ResultSetIterator(resultSet));
    return of(ResultSets.forRows(resultSet.getType(), rows));
  }

  /**
   * {@link Iterator} implementation for {@link com.google.cloud.spanner.ResultSet}. This is used to
   * create a copy of an existing result set without the need to iterate the rows more than once.
   */
  private static class ResultSetIterator implements Iterator {
    private final com.google.cloud.spanner.ResultSet resultSet;
    private boolean calculatedHasNext = false;
    private boolean hasNext = false;

    ResultSetIterator(com.google.cloud.spanner.ResultSet resultSet) {
      this.resultSet = resultSet;
    }

    @Override
    public boolean hasNext() {
      if (!calculatedHasNext) {
        calculatedHasNext = true;
        hasNext = resultSet.next();
      }
      return hasNext;
    }

    @Override
    public Struct next() {
      if (hasNext()) {
        // Indicate that the next call to hasNext() must re-check whether there are more results.
        calculatedHasNext = false;
        return resultSet.getCurrentRowAsStruct();
      }
      throw new NoSuchElementException();
    }
  }

  private boolean closed = false;
  private final Statement statement;
  private boolean wasNull = false;
  private boolean nextReturnedFalse = false;
  private boolean nextCalledForMetaData = false;
  private boolean nextCalledForMetaDataResult = false;
  private long currentRow = 0L;
  private final ImmutableSet columnsAllowedUncheckedLongCastToShort;

  JdbcResultSet(Statement statement, com.google.cloud.spanner.ResultSet spanner) {
    this(statement, spanner, ImmutableSet.of());
  }

  JdbcResultSet(
      Statement statement,
      com.google.cloud.spanner.ResultSet spanner,
      ImmutableSet columnsAllowedUncheckedLongCastToShort) {
    super(spanner);
    this.statement = statement;
    this.columnsAllowedUncheckedLongCastToShort = columnsAllowedUncheckedLongCastToShort;
  }

  void checkClosedAndValidRow() throws SQLException {
    checkClosed();
    if (currentRow == 0L) {
      throw JdbcSqlExceptionFactory.of(
          "ResultSet is before first row. Call next() first.",
          com.google.rpc.Code.FAILED_PRECONDITION);
    }
    if (nextReturnedFalse) {
      throw JdbcSqlExceptionFactory.of(
          "ResultSet is after last row. There is no more data available.",
          com.google.rpc.Code.FAILED_PRECONDITION);
    }
  }

  @Override
  public boolean next() throws SQLException {
    checkClosed();
    currentRow++;
    if (nextCalledForMetaData) {
      nextReturnedFalse = !nextCalledForMetaDataResult;
      nextCalledForMetaData = false;
    } else {
      nextReturnedFalse = !spanner.next();
    }

    return !nextReturnedFalse;
  }

  @Override
  public void close() {
    spanner.close();
    this.closed = true;
  }

  @Override
  public boolean wasNull() throws SQLException {
    checkClosedAndValidRow();
    return wasNull;
  }

  private boolean isNull(int columnIndex) {
    wasNull = spanner.isNull(columnIndex - 1);
    return wasNull;
  }

  SQLException createInvalidToGetAs(String sqlType, Code type) {
    return JdbcSqlExceptionFactory.of(
        String.format("Invalid column type to get as %s: %s", sqlType, type.name()),
        com.google.rpc.Code.INVALID_ARGUMENT);
  }

  SQLException createCastException(String sqlType, Object value) {
    return JdbcSqlExceptionFactory.of(
        String.format("Cannot cast to %s: %s", sqlType, value),
        com.google.rpc.Code.INVALID_ARGUMENT);
  }

  @Override
  public String getString(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? null : String.valueOf(spanner.getBoolean(spannerIndex));
      case BYTES:
      case PROTO:
        return isNull ? null : spanner.getBytes(spannerIndex).toBase64();
      case DATE:
        return isNull ? null : spanner.getDate(spannerIndex).toString();
      case FLOAT32:
        return isNull ? null : Float.toString(spanner.getFloat(spannerIndex));
      case FLOAT64:
        return isNull ? null : Double.toString(spanner.getDouble(spannerIndex));
      case INT64:
      case ENUM:
        return isNull ? null : Long.toString(spanner.getLong(spannerIndex));
      case NUMERIC:
        return isNull ? null : spanner.getBigDecimal(spannerIndex).toString();
      case PG_NUMERIC:
        return isNull ? null : spanner.getString(spannerIndex);
      case STRING:
        return isNull ? null : spanner.getString(spannerIndex);
      case JSON:
        return isNull ? null : spanner.getJson(spannerIndex);
      case PG_JSONB:
        return isNull ? null : spanner.getPgJsonb(spannerIndex);
      case TIMESTAMP:
        return isNull ? null : spanner.getTimestamp(spannerIndex).toString();
      case STRUCT:
      case ARRAY:
      default:
        throw createInvalidToGetAs("string", type);
    }
  }

  @Override
  public boolean getBoolean(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return !isNull && spanner.getBoolean(spannerIndex);
      case FLOAT32:
        return !isNull && spanner.getFloat(spannerIndex) != 0f;
      case FLOAT64:
        return !isNull && spanner.getDouble(spannerIndex) != 0D;
      case INT64:
      case ENUM:
        return !isNull && spanner.getLong(spannerIndex) != 0L;
      case NUMERIC:
        return !isNull && !spanner.getBigDecimal(spannerIndex).equals(BigDecimal.ZERO);
      case PG_NUMERIC:
        return !isNull && !spanner.getString(spannerIndex).equals("0");
      case STRING:
        return !isNull && Boolean.parseBoolean(spanner.getString(spannerIndex));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("boolean", type);
    }
  }

  @Override
  public byte getByte(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? (byte) 0 : (spanner.getBoolean(spannerIndex) ? (byte) 1 : 0);
      case FLOAT32:
        return isNull
            ? (byte) 0
            : checkedCastToByte(Float.valueOf(spanner.getFloat(spannerIndex)).longValue());
      case FLOAT64:
        return isNull
            ? (byte) 0
            : checkedCastToByte(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
      case INT64:
      case ENUM:
        return isNull ? (byte) 0 : checkedCastToByte(spanner.getLong(spannerIndex));
      case NUMERIC:
        return isNull ? (byte) 0 : checkedCastToByte(spanner.getBigDecimal(spannerIndex));
      case PG_NUMERIC:
        return isNull
            ? (byte) 0
            : checkedCastToByte(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
      case STRING:
        return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex)));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("byte", type);
    }
  }

  @Override
  public short getShort(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (short) 1 : 0);
      case FLOAT32:
        return isNull
            ? 0
            : checkedCastToShort(Float.valueOf(spanner.getFloat(spannerIndex)).longValue());
      case FLOAT64:
        return isNull
            ? 0
            : checkedCastToShort(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
      case INT64:
      case ENUM:
        if (this.columnsAllowedUncheckedLongCastToShort.contains(columnIndex)) {
          // This is used to allow frameworks that call getShort(int) on the ResultSet that is
          // returned by DatabaseMetadata#getTypeInfo() to get the type code as a short, even when
          // the value is out of range for a short.
          return isNull ? 0 : (short) spanner.getLong(spannerIndex);
        }
        return isNull ? 0 : checkedCastToShort(spanner.getLong(spannerIndex));
      case NUMERIC:
        return isNull ? 0 : checkedCastToShort(spanner.getBigDecimal(spannerIndex));
      case PG_NUMERIC:
        return isNull
            ? 0
            : checkedCastToShort(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
      case STRING:
        return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex)));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("short", type);
    }
  }

  @Override
  public int getInt(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? 1 : 0);
      case FLOAT32:
        return isNull
            ? 0
            : checkedCastToInt(Float.valueOf(spanner.getFloat(spannerIndex)).longValue());
      case FLOAT64:
        return isNull
            ? 0
            : checkedCastToInt(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
      case INT64:
      case ENUM:
        return isNull ? 0 : checkedCastToInt(spanner.getLong(spannerIndex));
      case NUMERIC:
        return isNull ? 0 : checkedCastToInt(spanner.getBigDecimal(spannerIndex));
      case PG_NUMERIC:
        return isNull
            ? 0
            : checkedCastToInt(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
      case STRING:
        return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex)));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("int", type);
    }
  }

  @Override
  public long getLong(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? 0L : (spanner.getBoolean(spannerIndex) ? 1L : 0L);
      case FLOAT32:
        return isNull ? 0L : Float.valueOf(spanner.getFloat(spannerIndex)).longValue();
      case FLOAT64:
        return isNull ? 0L : Double.valueOf(spanner.getDouble(spannerIndex)).longValue();
      case INT64:
      case ENUM:
        return isNull ? 0L : spanner.getLong(spannerIndex);
      case NUMERIC:
        return isNull ? 0 : checkedCastToLong(parseBigDecimal(spanner.getString(spannerIndex)));
      case PG_NUMERIC:
        return isNull
            ? 0L
            : checkedCastToLong(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
      case STRING:
        return isNull ? 0L : parseLong(spanner.getString(spannerIndex));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("long", type);
    }
  }

  @Override
  public float getFloat(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (float) 1 : 0);
      case FLOAT32:
        return isNull ? 0 : spanner.getFloat(spannerIndex);
      case FLOAT64:
        return isNull ? 0 : checkedCastToFloat(spanner.getDouble(spannerIndex));
      case INT64:
      case ENUM:
        return isNull ? 0 : checkedCastToFloat(spanner.getLong(spannerIndex));
      case NUMERIC:
        return isNull ? 0 : spanner.getBigDecimal(spannerIndex).floatValue();
      case PG_NUMERIC:
        return isNull ? 0 : parseFloat(spanner.getString(spannerIndex));
      case STRING:
        return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex)));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("float", type);
    }
  }

  @Override
  public double getDouble(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case BOOL:
        return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (double) 1 : 0);
      case FLOAT32:
        return isNull ? 0 : spanner.getFloat(spannerIndex);
      case FLOAT64:
        return isNull ? 0 : spanner.getDouble(spannerIndex);
      case INT64:
      case ENUM:
        return isNull ? 0 : spanner.getLong(spannerIndex);
      case NUMERIC:
        return isNull ? 0 : spanner.getBigDecimal(spannerIndex).doubleValue();
      case PG_NUMERIC:
        return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
      case STRING:
        return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case STRUCT:
      case TIMESTAMP:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("double", type);
    }
  }

  @Override
  public byte[] getBytes(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    final boolean isNull = isNull(columnIndex);
    final int spannerIndex = columnIndex - 1;
    return isNull ? null : spanner.getBytes(spannerIndex).toByteArray();
  }

  @Override
  public Date getDate(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case DATE:
        return isNull ? null : JdbcTypeConverter.toSqlDate(spanner.getDate(spannerIndex));
      case STRING:
        return isNull ? null : parseDate(spanner.getString(spannerIndex));
      case TIMESTAMP:
        return isNull
            ? null
            : new Date(spanner.getTimestamp(spannerIndex).toSqlTimestamp().getTime());
      case BOOL:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case PG_NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("date", type);
    }
  }

  @Override
  public Time getTime(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case STRING:
        return isNull ? null : parseTime(spanner.getString(spannerIndex));
      case TIMESTAMP:
        return isNull ? null : JdbcTypeConverter.toSqlTime(spanner.getTimestamp(spannerIndex));
      case BOOL:
      case DATE:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case PG_NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("time", type);
    }
  }

  @Override
  public Timestamp getTimestamp(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case DATE:
        return isNull ? null : JdbcTypeConverter.toSqlTimestamp(spanner.getDate(spannerIndex));
      case STRING:
        return isNull ? null : parseTimestamp(spanner.getString(spannerIndex));
      case TIMESTAMP:
        return isNull ? null : JdbcTypeConverter.toSqlTimestamp(spanner.getTimestamp(spannerIndex));
      case BOOL:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case PG_NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("timestamp", type);
    }
  }

  private InputStream getInputStream(String val, Charset charset) {
    if (val == null) return null;
    byte[] b = val.getBytes(charset);
    return new ByteArrayInputStream(b);
  }

  @Override
  public InputStream getAsciiStream(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    return getInputStream(getString(columnIndex), StandardCharsets.US_ASCII);
  }

  @Override
  @Deprecated
  public InputStream getUnicodeStream(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    return getInputStream(getString(columnIndex), StandardCharsets.UTF_16LE);
  }

  @Override
  public InputStream getBinaryStream(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    byte[] val = getBytes(columnIndex);
    return val == null ? null : new ByteArrayInputStream(val);
  }

  @Override
  public String getString(String columnLabel) throws SQLException {
    return getString(findColumn(columnLabel));
  }

  @Override
  public boolean getBoolean(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getBoolean(findColumn(columnLabel));
  }

  @Override
  public byte getByte(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getByte(findColumn(columnLabel));
  }

  @Override
  public short getShort(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getShort(findColumn(columnLabel));
  }

  @Override
  public int getInt(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getInt(findColumn(columnLabel));
  }

  @Override
  public long getLong(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getLong(findColumn(columnLabel));
  }

  @Override
  public float getFloat(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getFloat(findColumn(columnLabel));
  }

  @Override
  public double getDouble(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getDouble(findColumn(columnLabel));
  }

  @Override
  public byte[] getBytes(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getBytes(findColumn(columnLabel));
  }

  @Override
  public Date getDate(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getDate(findColumn(columnLabel));
  }

  @Override
  public Time getTime(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getTime(findColumn(columnLabel));
  }

  @Override
  public Timestamp getTimestamp(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getTimestamp(findColumn(columnLabel));
  }

  @Override
  public InputStream getAsciiStream(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getAsciiStream(findColumn(columnLabel));
  }

  @Override
  @Deprecated
  public InputStream getUnicodeStream(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getUnicodeStream(findColumn(columnLabel));
  }

  @Override
  public InputStream getBinaryStream(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getBinaryStream(findColumn(columnLabel));
  }

  @Override
  public JdbcResultSetMetaData getMetaData() throws SQLException {
    checkClosed();
    if (isBeforeFirst() && !nextCalledForMetaData) {
      // do a call to next() on the underlying resultset to initialize metadata
      nextCalledForMetaData = true;
      nextCalledForMetaDataResult = spanner.next();
    }
    return new JdbcResultSetMetaData(this, statement);
  }

  @Override
  public Object getObject(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getObject(findColumn(columnLabel));
  }

  @Override
  public Object getObject(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    Type type = spanner.getColumnType(columnIndex - 1);
    return isNull(columnIndex) ? null : getObject(type, columnIndex);
  }

  private Object getObject(Type type, int columnIndex) throws SQLException {
    // TODO: Refactor to check based on type code.
    if (type == Type.bool()) return getBoolean(columnIndex);
    if (type == Type.bytes()) return getBytes(columnIndex);
    if (type == Type.date()) return getDate(columnIndex);
    if (type == Type.float32()) {
      return getFloat(columnIndex);
    }
    if (type == Type.float64()) return getDouble(columnIndex);
    if (type == Type.int64() || type == Type.pgOid()) {
      return getLong(columnIndex);
    }
    if (type == Type.numeric()) return getBigDecimal(columnIndex);
    if (type == Type.pgNumeric()) {
      final String value = getString(columnIndex);
      try {
        return parseBigDecimal(value);
      } catch (Exception e) {
        return parseDouble(value);
      }
    }
    if (type == Type.string()) return getString(columnIndex);
    if (type == Type.json() || type == Type.pgJsonb()) {
      return getString(columnIndex);
    }
    if (type == Type.timestamp()) return getTimestamp(columnIndex);
    if (type.getCode() == Code.PROTO) return getBytes(columnIndex);
    if (type.getCode() == Code.ENUM) return getLong(columnIndex);
    if (type.getCode() == Code.ARRAY) return getArray(columnIndex);
    throw JdbcSqlExceptionFactory.of("Unknown type: " + type, com.google.rpc.Code.INVALID_ARGUMENT);
  }

  @Override
  public int findColumn(String columnLabel) throws SQLException {
    checkClosed();
    try {
      return spanner.getColumnIndex(columnLabel) + 1;
    } catch (IllegalArgumentException e) {
      throw JdbcSqlExceptionFactory.of(
          "no column with label " + columnLabel + " found", com.google.rpc.Code.INVALID_ARGUMENT);
    }
  }

  @Override
  public Reader getCharacterStream(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    String val = getString(columnIndex);
    return val == null ? null : new StringReader(val);
  }

  @Override
  public Reader getCharacterStream(String columnLabel) throws SQLException {
    return getCharacterStream(findColumn(columnLabel));
  }

  @Override
  public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    return getBigDecimal(columnIndex, false, 0);
  }

  @Override
  public BigDecimal getBigDecimal(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getBigDecimal(findColumn(columnLabel), false, 0);
  }

  @Override
  @Deprecated
  public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
    checkClosedAndValidRow();
    return getBigDecimal(columnIndex, true, scale);
  }

  @Override
  @Deprecated
  public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
    checkClosedAndValidRow();
    return getBigDecimal(findColumn(columnLabel), true, scale);
  }

  private BigDecimal getBigDecimal(int columnIndex, boolean fixedScale, int scale)
      throws SQLException {
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    boolean isNull = isNull(columnIndex);
    BigDecimal res;
    switch (type) {
      case BOOL:
        res =
            isNull
                ? null
                : (spanner.getBoolean(columnIndex - 1) ? BigDecimal.ONE : BigDecimal.ZERO);
        break;
      case FLOAT32:
        res = isNull ? null : BigDecimal.valueOf(spanner.getFloat(spannerIndex));
        break;
      case FLOAT64:
        res = isNull ? null : BigDecimal.valueOf(spanner.getDouble(spannerIndex));
        break;
      case INT64:
      case ENUM:
        res = isNull ? null : BigDecimal.valueOf(spanner.getLong(spannerIndex));
        break;
      case NUMERIC:
        res = isNull ? null : spanner.getBigDecimal(spannerIndex);
        break;
      case PG_NUMERIC:
        res = isNull ? null : parseBigDecimal(spanner.getString(spannerIndex));
        break;
      case STRING:
        try {
          res = isNull ? null : new BigDecimal(spanner.getString(spannerIndex));
          break;
        } catch (NumberFormatException e) {
          throw JdbcSqlExceptionFactory.of(
              "The column does not contain a valid BigDecimal",
              com.google.rpc.Code.INVALID_ARGUMENT,
              e);
        }
      case BYTES:
      case JSON:
      case PG_JSONB:
      case DATE:
      case TIMESTAMP:
      case STRUCT:
      case PROTO:
      case ARRAY:
      default:
        throw createInvalidToGetAs("BigDecimal", type);
    }
    if (res != null && fixedScale) {
      res = res.setScale(scale, RoundingMode.HALF_UP);
    }
    return res;
  }

  @Override
  public boolean isBeforeFirst() throws SQLException {
    checkClosed();
    return currentRow == 0L;
  }

  @Override
  public boolean isAfterLast() throws SQLException {
    checkClosed();
    return nextReturnedFalse;
  }

  @Override
  public boolean isFirst() throws SQLException {
    checkClosed();
    return currentRow == 1L;
  }

  @Override
  public int getRow() throws SQLException {
    checkClosed();
    return checkedCastToInt(currentRow);
  }

  @Override
  public Statement getStatement() throws SQLException {
    checkClosed();
    return statement;
  }

  @Override
  public Array getArray(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getArray(findColumn(columnLabel));
  }

  @Override
  public Array getArray(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    if (isNull(columnIndex)) return null;
    Type type = spanner.getColumnType(columnIndex - 1);
    if (type.getCode() != Code.ARRAY)
      throw JdbcSqlExceptionFactory.of(
          "Column with index " + columnIndex + " does not contain an array",
          com.google.rpc.Code.INVALID_ARGUMENT);
    final Code elementCode = getMainTypeCode(type.getArrayElementType());
    final JdbcDataType dataType = JdbcDataType.getType(elementCode);
    try {
      List elements = dataType.getArrayElements(spanner, columnIndex - 1);
      return JdbcArray.createArray(dataType, elements);
    } catch (NumberFormatException e) {
      final String sqlType = "ARRAY<" + type.getArrayElementType() + ">";
      final Value value = spanner.getValue(columnIndex - 1);
      throw createCastException(sqlType, value);
    }
  }

  @Override
  public Date getDate(int columnIndex, Calendar cal) throws SQLException {
    checkClosedAndValidRow();
    if (isNull(columnIndex)) {
      return null;
    }
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case DATE:
        return JdbcTypeConverter.toSqlDate(spanner.getDate(spannerIndex), cal);
      case STRING:
        return parseDate(spanner.getString(spannerIndex), cal);
      case TIMESTAMP:
        return new Date(
            JdbcTypeConverter.getAsSqlTimestamp(spanner.getTimestamp(spannerIndex), cal).getTime());
      case BOOL:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("date", type);
    }
  }

  @Override
  public Date getDate(String columnLabel, Calendar cal) throws SQLException {
    return getDate(findColumn(columnLabel), cal);
  }

  @Override
  public Time getTime(int columnIndex, Calendar cal) throws SQLException {
    checkClosedAndValidRow();
    boolean isNull = isNull(columnIndex);
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case STRING:
        return isNull ? null : parseTime(spanner.getString(spannerIndex), cal);
      case TIMESTAMP:
        return isNull ? null : JdbcTypeConverter.toSqlTime(spanner.getTimestamp(spannerIndex), cal);
      case BOOL:
      case DATE:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("time", type);
    }
  }

  @Override
  public Time getTime(String columnLabel, Calendar cal) throws SQLException {
    return getTime(findColumn(columnLabel), cal);
  }

  @Override
  public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
    checkClosedAndValidRow();
    if (isNull(columnIndex)) {
      return null;
    }
    int spannerIndex = columnIndex - 1;
    Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
    switch (type) {
      case DATE:
        return JdbcTypeConverter.toSqlTimestamp(spanner.getDate(spannerIndex), cal);
      case STRING:
        return parseTimestamp(spanner.getString(spannerIndex), cal);
      case TIMESTAMP:
        return JdbcTypeConverter.getAsSqlTimestamp(spanner.getTimestamp(spannerIndex), cal);
      case BOOL:
      case FLOAT32:
      case FLOAT64:
      case INT64:
      case NUMERIC:
      case BYTES:
      case JSON:
      case PG_JSONB:
      case STRUCT:
      case PROTO:
      case ENUM:
      case ARRAY:
      default:
        throw createInvalidToGetAs("timestamp", type);
    }
  }

  @Override
  public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException {
    return getTimestamp(findColumn(columnLabel), cal);
  }

  @Override
  public URL getURL(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    try {
      return isNull(columnIndex) ? null : new URL(getString(columnIndex));
    } catch (MalformedURLException e) {
      throw JdbcSqlExceptionFactory.of(
          "Invalid URL: " + spanner.getString(columnIndex - 1),
          com.google.rpc.Code.INVALID_ARGUMENT);
    }
  }

  @Override
  public URL getURL(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getURL(findColumn(columnLabel));
  }

  @Override
  public int getHoldability() throws SQLException {
    checkClosed();
    return CLOSE_CURSORS_AT_COMMIT;
  }

  @Override
  public boolean isClosed() {
    return closed;
  }

  @Override
  public String getNString(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    return getString(columnIndex);
  }

  @Override
  public String getNString(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getString(columnLabel);
  }

  @Override
  public Reader getNCharacterStream(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    return getCharacterStream(columnIndex);
  }

  @Override
  public Reader getNCharacterStream(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    return getCharacterStream(columnLabel);
  }

  @Override
  public  T getObject(int columnIndex, Class type) throws SQLException {
    checkClosedAndValidRow();
    return convertObject(getObject(columnIndex), type, spanner.getColumnType(columnIndex - 1));
  }

  @Override
  public  T getObject(String columnLabel, Class type) throws SQLException {
    checkClosedAndValidRow();
    return convertObject(getObject(columnLabel), type, spanner.getColumnType(columnLabel));
  }

  @Override
  public Object getObject(int columnIndex, Map> map) throws SQLException {
    checkClosedAndValidRow();
    return convertObject(getObject(columnIndex), map, spanner.getColumnType(columnIndex - 1));
  }

  @Override
  public Object getObject(String columnLabel, Map> map) throws SQLException {
    checkClosedAndValidRow();
    return convertObject(getObject(columnLabel), map, spanner.getColumnType(columnLabel));
  }

  @Override
  public Blob getBlob(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    byte[] val = getBytes(columnIndex);
    return val == null ? null : new JdbcBlob(val);
  }

  @Override
  public Blob getBlob(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    byte[] val = getBytes(columnLabel);
    return val == null ? null : new JdbcBlob(val);
  }

  @Override
  public Clob getClob(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    String val = getString(columnIndex);
    return val == null ? null : new JdbcClob(val);
  }

  @Override
  public Clob getClob(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    String val = getString(columnLabel);
    return val == null ? null : new JdbcClob(val);
  }

  @Override
  public NClob getNClob(int columnIndex) throws SQLException {
    checkClosedAndValidRow();
    String val = getString(columnIndex);
    return val == null ? null : new JdbcClob(val);
  }

  @Override
  public NClob getNClob(String columnLabel) throws SQLException {
    checkClosedAndValidRow();
    String val = getString(columnLabel);
    return val == null ? null : new JdbcClob(val);
  }

  @SuppressWarnings("unchecked")
  private  T convertObject(Object o, Class javaType, Type type) throws SQLException {
    return (T) JdbcTypeConverter.convert(o, type, javaType);
  }

  private Object convertObject(Object o, Map> map, Type type) throws SQLException {
    if (map == null)
      throw JdbcSqlExceptionFactory.of("Map may not be null", com.google.rpc.Code.INVALID_ARGUMENT);
    if (o == null) return null;
    Class javaType = map.get(type.getCode().name());
    if (javaType == null) return o;
    return JdbcTypeConverter.convert(o, type, javaType);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy