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

com.google.cloud.spanner.jdbc.JdbcParameterStore 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 com.google.cloud.ByteArray;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Statement.Builder;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Value;
import com.google.cloud.spanner.ValueBinder;
import com.google.common.io.CharStreams;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Message;
import com.google.protobuf.NullValue;
import com.google.protobuf.ProtocolMessageEnum;
import com.google.rpc.Code;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLType;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/** This class handles the parameters of a {@link PreparedStatement}. */
class JdbcParameterStore {
  /**
   * The initial size of the arrays that hold the parameter values. The array will automatically be
   * extended when needed.
   */
  private static final int INITIAL_PARAMETERS_ARRAY_SIZE = 10;

  private static final class JdbcParameter {
    private Object value;
    private Integer type;
    private Integer nullable;
    private Integer scaleOrLength;
    private String column;
  }

  private ArrayList parametersList = new ArrayList<>(INITIAL_PARAMETERS_ARRAY_SIZE);

  /** Name of the table that the parameters will be used to query/update. Can be null. */
  private String table;

  /**
   * The highest parameter index in use. Parameter values do not need to be set in order, it could
   * be that a parameter with for example index 10 is set first, and that the preceding parameters
   * are set at a later time.
   */
  private int highestIndex = 0;

  private final Dialect dialect;

  JdbcParameterStore(Dialect dialect) {
    this.dialect = dialect;
  }

  void clearParameters() {
    parametersList = new ArrayList<>(INITIAL_PARAMETERS_ARRAY_SIZE);
    highestIndex = 0;
    table = null;
  }

  /** Get parameter value. Index is 1-based. */
  Object getParameter(int parameterIndex) {
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) return null;
    return parametersList.get(arrayIndex).value;
  }

  /** Get parameter type code according to the values in {@link Types}. Index is 1-based. */
  Integer getType(int parameterIndex) {
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) return null;
    return parametersList.get(arrayIndex).type;
  }

  Integer getNullable(int parameterIndex) {
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) return null;
    return parametersList.get(arrayIndex).nullable;
  }

  Integer getScaleOrLength(int parameterIndex) {
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) return null;
    return parametersList.get(arrayIndex).scaleOrLength;
  }

  String getColumn(int parameterIndex) {
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) return null;
    return parametersList.get(arrayIndex).column;
  }

  String getTable() {
    return table;
  }

  void setTable(String table) {
    this.table = table;
  }

  void setColumn(int parameterIndex, String column) throws SQLException {
    setParameter(
        parameterIndex,
        getParameter(parameterIndex),
        getType(parameterIndex),
        getScaleOrLength(parameterIndex),
        column,
        null);
  }

  void setType(int parameterIndex, Integer type) throws SQLException {
    setParameter(
        parameterIndex,
        getParameter(parameterIndex),
        type,
        getScaleOrLength(parameterIndex),
        getColumn(parameterIndex),
        null);
  }

  /** Sets a parameter value. The type will be determined based on the type of the value. */
  void setParameter(int parameterIndex, Object value) throws SQLException {
    setParameter(parameterIndex, value, null, null, null, null);
  }

  /** Sets a parameter value as the specified vendor-specific {@link SQLType}. */
  void setParameter(int parameterIndex, Object value, SQLType sqlType) throws SQLException {
    setParameter(parameterIndex, value, null, null, null, sqlType);
  }

  /**
   * Sets a parameter value as the specified vendor-specific {@link SQLType} with the specified
   * scale or length. This method is only here to support the {@link
   * PreparedStatement#setObject(int, Object, SQLType, int)} method.
   */
  void setParameter(int parameterIndex, Object value, SQLType sqlType, Integer scaleOrLength)
      throws SQLException {
    setParameter(parameterIndex, value, null, scaleOrLength, null, sqlType);
  }

  /**
   * Sets a parameter value as the specified sql type. The type can be one of the constants in
   * {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
   */
  void setParameter(int parameterIndex, Object value, Integer sqlType) throws SQLException {
    setParameter(parameterIndex, value, sqlType, null);
  }

  /**
   * Sets a parameter value as the specified sql type with the specified scale or length. The type
   * can be one of the constants in {@link Types} or a vendor specific type code supplied by a
   * vendor specific {@link SQLType}.
   */
  void setParameter(int parameterIndex, Object value, Integer sqlType, Integer scaleOrLength)
      throws SQLException {
    setParameter(parameterIndex, value, sqlType, scaleOrLength, null, null);
  }

  /**
   * Sets a parameter value as the specified sql type with the specified scale or length. Any {@link
   * SQLType} instance will take precedence over sqlType. The type can be one of the constants in
   * {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
   */
  void setParameter(
      int parameterIndex,
      Object value,
      Integer sqlType,
      Integer scaleOrLength,
      String column,
      SQLType sqlTypeObject)
      throws SQLException {
    // Ignore the sql type if the application has created a Spanner Value object.
    if (!(value instanceof Value)) {
      // check that only valid type/value combinations are entered
      if (sqlTypeObject != null && sqlType == null) {
        sqlType = sqlTypeObject.getVendorTypeNumber();
      }
      if (sqlType != null) {
        checkTypeAndValueSupported(value, sqlType);
      }
    } // set the parameter
    highestIndex = Math.max(parameterIndex, highestIndex);
    int arrayIndex = parameterIndex - 1;
    if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) {
      parametersList.ensureCapacity(parameterIndex);
      while (parametersList.size() < parameterIndex) {
        parametersList.add(null);
      }
      parametersList.set(arrayIndex, new JdbcParameter());
    }
    JdbcParameter param = parametersList.get(arrayIndex);
    param.value = value;
    param.type = sqlType;
    param.scaleOrLength = scaleOrLength;
    param.column = column;
  }

  private void checkTypeAndValueSupported(Object value, int sqlType) throws SQLException {
    if (value == null) {
      // null is always supported, as we will just fall back to an untyped NULL value.
      return;
    }
    if (!isTypeSupported(sqlType)) {
      throw JdbcSqlExceptionFactory.of(
          "Type " + sqlType + " is not supported", Code.INVALID_ARGUMENT);
    }
    if (!isValidTypeAndValue(value, sqlType)) {
      throw JdbcSqlExceptionFactory.of(
          value + " is not a valid value for type " + sqlType, Code.INVALID_ARGUMENT);
    }
  }

  private boolean isTypeSupported(int sqlType) {
    switch (sqlType) {
      case Types.BIT:
      case Types.BOOLEAN:
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
      case Types.FLOAT:
      case Types.REAL:
      case Types.DOUBLE:
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.LONGVARCHAR:
      case Types.NCHAR:
      case Types.NVARCHAR:
      case Types.LONGNVARCHAR:
      case Types.DATE:
      case Types.TIME:
      case Types.TIME_WITH_TIMEZONE:
      case Types.TIMESTAMP:
      case Types.TIMESTAMP_WITH_TIMEZONE:
      case Types.BINARY:
      case Types.VARBINARY:
      case Types.LONGVARBINARY:
      case Types.ARRAY:
      case Types.BLOB:
      case Types.CLOB:
      case Types.NCLOB:
      case Types.NUMERIC:
      case Types.DECIMAL:
      case JsonType.VENDOR_TYPE_NUMBER:
      case JsonType.SHORT_VENDOR_TYPE_NUMBER:
      case PgJsonbType.VENDOR_TYPE_NUMBER:
      case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
      case ProtoMessageType.VENDOR_TYPE_NUMBER:
      case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
      case ProtoEnumType.VENDOR_TYPE_NUMBER:
      case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
        return true;
    }
    return false;
  }

  private boolean isValidTypeAndValue(Object value, int sqlType) {
    if (value == null) {
      return true;
    }
    switch (sqlType) {
      case Types.BIT:
      case Types.BOOLEAN:
        return value instanceof Boolean || value instanceof Number;
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
      case Types.FLOAT:
      case Types.REAL:
      case Types.DOUBLE:
      case Types.NUMERIC:
      case Types.DECIMAL:
        return value instanceof Number || value instanceof ProtocolMessageEnum;
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.LONGVARCHAR:
      case Types.NCHAR:
      case Types.NVARCHAR:
      case Types.LONGNVARCHAR:
        return value instanceof String
            || value instanceof InputStream
            || value instanceof Reader
            || value instanceof URL;
      case Types.DATE:
        return value instanceof Date
            || value instanceof Time
            || value instanceof Timestamp
            || value instanceof LocalDate;
      case Types.TIME:
      case Types.TIME_WITH_TIMEZONE:
      case Types.TIMESTAMP:
      case Types.TIMESTAMP_WITH_TIMEZONE:
        return value instanceof Date
            || value instanceof Time
            || value instanceof Timestamp
            || value instanceof OffsetDateTime;
      case Types.BINARY:
      case Types.VARBINARY:
      case Types.LONGVARBINARY:
        return value instanceof byte[]
            || value instanceof InputStream
            || value instanceof AbstractMessage;
      case Types.ARRAY:
        return value instanceof Array;
      case Types.BLOB:
        return value instanceof Blob || value instanceof InputStream;
      case Types.CLOB:
        return value instanceof Clob || value instanceof Reader;
      case Types.NCLOB:
        return value instanceof NClob || value instanceof Reader;
      case JsonType.VENDOR_TYPE_NUMBER:
      case JsonType.SHORT_VENDOR_TYPE_NUMBER:
        return value instanceof String
            || value instanceof InputStream
            || value instanceof Reader
            || (value instanceof Value && ((Value) value).getType().getCode() == Type.Code.JSON);
      case PgJsonbType.VENDOR_TYPE_NUMBER:
      case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
        return value instanceof String
            || value instanceof InputStream
            || value instanceof Reader
            || (value instanceof Value
                && ((Value) value).getType().getCode() == Type.Code.PG_JSONB);
      case ProtoMessageType.VENDOR_TYPE_NUMBER:
      case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
        return value instanceof AbstractMessage || value instanceof byte[];
      case ProtoEnumType.VENDOR_TYPE_NUMBER:
      case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
        return value instanceof ProtocolMessageEnum || value instanceof Number;
    }
    return false;
  }

  /** Return the highest param index in use in this store. */
  int getHighestIndex() {
    return highestIndex;
  }

  /** Fetch parameter metadata from the database. */
  void fetchMetaData(Connection connection) throws SQLException {
    if (table != null && !"".equals(table)) {
      try (ResultSet rsCols = connection.getMetaData().getColumns(null, null, table, null)) {
        while (rsCols.next()) {
          String col = rsCols.getString("COLUMN_NAME");
          int arrayIndex = getParameterArrayIndex(col);
          if (arrayIndex > -1) {
            JdbcParameter param = parametersList.get(arrayIndex);
            if (param != null) {
              param.scaleOrLength = rsCols.getInt("COLUMN_SIZE");
              param.type = rsCols.getInt("DATA_TYPE");
              param.nullable = rsCols.getInt("NULLABLE");
            }
          }
        }
      }
    }
  }

  private int getParameterArrayIndex(String columnName) {
    if (columnName != null) {
      for (int index = 0; index < highestIndex; index++) {
        JdbcParameter param = parametersList.get(index);
        if (param != null && param.column != null) {
          if (columnName.equalsIgnoreCase(param.column)) {
            return index;
          }
        }
      }
    }
    return -1;
  }

  /** Bind a JDBC parameter to a parameter on a Spanner {@link Statement}. */
  Builder bindParameterValue(ValueBinder binder, int index) throws SQLException {
    return setValue(binder, getParameter(index), getType(index));
  }

  /** Set a value from a JDBC parameter on a Spanner {@link Statement}. */
  Builder setValue(ValueBinder binder, Object value, Integer sqlType) throws SQLException {
    Builder res;
    if (value instanceof Value) {
      // If a Value has been constructed, then that should override any sqlType that might have been
      // supplied.
      res = binder.to((Value) value);
    } else if (sqlType != null && sqlType == Types.ARRAY) {
      if (value instanceof Array) {
        Array array = (Array) value;
        value = array.getArray();
        sqlType = array.getBaseType();
      }
      res = setArrayValue(binder, sqlType, value);
    } else {
      res = setSingleValue(binder, value, sqlType);
    }
    if (res == null && value != null) {
      throw JdbcSqlExceptionFactory.of(
          "Unsupported parameter type: " + value.getClass().getName() + " - " + value,
          Code.INVALID_ARGUMENT);
    }
    return res;
  }

  private Builder setSingleValue(ValueBinder binder, Object value, Integer sqlType)
      throws SQLException {
    if (value == null) {
      return setNullValue(binder, sqlType);
    } else if (sqlType == null || sqlType.equals(Types.OTHER)) {
      return setParamWithUnknownType(binder, value);
    } else {
      return setParamWithKnownType(binder, value, sqlType);
    }
  }

  /** Set a JDBC parameter value on a Spanner {@link Statement} with a known SQL type. */
  private Builder setParamWithKnownType(ValueBinder binder, Object value, Integer sqlType)
      throws SQLException {
    if (sqlType == null) {
      return null;
    }
    int type = sqlType;

    switch (type) {
      case Types.BIT:
      case Types.BOOLEAN:
        if (value instanceof Boolean) {
          return binder.to((Boolean) value);
        } else if (value instanceof Number) {
          return binder.to(((Number) value).longValue() != 0L);
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid boolean", Code.INVALID_ARGUMENT);
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
        if (value instanceof Number) {
          return binder.to(((Number) value).longValue());
        } else if (value instanceof ProtocolMessageEnum) {
          return binder.to((ProtocolMessageEnum) value);
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid long", Code.INVALID_ARGUMENT);
      case Types.REAL:
        if (value instanceof Number) {
          return binder.to(((Number) value).floatValue());
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid float", Code.INVALID_ARGUMENT);
      case Types.FLOAT:
      case Types.DOUBLE:
        if (value instanceof Number) {
          return binder.to(((Number) value).doubleValue());
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid double", Code.INVALID_ARGUMENT);
      case Types.NUMERIC:
      case Types.DECIMAL:
        if (dialect == Dialect.POSTGRESQL) {
          if (value instanceof Number) {
            return binder.to(Value.pgNumeric(value.toString()));
          }
          throw JdbcSqlExceptionFactory.of(value + " is not a valid Number", Code.INVALID_ARGUMENT);
        } else {
          if (value instanceof Number) {
            if (value instanceof BigDecimal) {
              return binder.to((BigDecimal) value);
            }
            try {
              return binder.to(new BigDecimal(value.toString()));
            } catch (NumberFormatException e) {
              // ignore and fall through to the exception.
            }
          }
          throw JdbcSqlExceptionFactory.of(
              value + " is not a valid BigDecimal", Code.INVALID_ARGUMENT);
        }
      case Types.CHAR:
      case Types.VARCHAR:
      case Types.LONGVARCHAR:
      case Types.NCHAR:
      case Types.NVARCHAR:
      case Types.LONGNVARCHAR:
        String stringValue;
        if (value instanceof String) {
          stringValue = (String) value;
        } else if (value instanceof InputStream) {
          stringValue = getStringFromInputStream((InputStream) value);
        } else if (value instanceof Reader) {
          stringValue = getStringFromReader((Reader) value);
        } else if (value instanceof URL) {
          stringValue = value.toString();
        } else if (value instanceof UUID) {
          stringValue = ((UUID) value).toString();
        } else {
          throw JdbcSqlExceptionFactory.of(value + " is not a valid string", Code.INVALID_ARGUMENT);
        }
        return binder.to(stringValue);
      case JsonType.VENDOR_TYPE_NUMBER:
      case JsonType.SHORT_VENDOR_TYPE_NUMBER:
      case PgJsonbType.VENDOR_TYPE_NUMBER:
      case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
        String jsonValue;
        if (value instanceof String) {
          jsonValue = (String) value;
        } else if (value instanceof InputStream) {
          jsonValue = getStringFromInputStream((InputStream) value);
        } else if (value instanceof Reader) {
          jsonValue = getStringFromReader((Reader) value);
        } else {
          throw JdbcSqlExceptionFactory.of(
              value + " is not a valid JSON value", Code.INVALID_ARGUMENT);
        }
        if (type == PgJsonbType.VENDOR_TYPE_NUMBER
            || type == PgJsonbType.SHORT_VENDOR_TYPE_NUMBER) {
          return binder.to(Value.pgJsonb(jsonValue));
        }
        return binder.to(Value.json(jsonValue));
      case Types.DATE:
        if (value instanceof Date) {
          return binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
        } else if (value instanceof Time) {
          return binder.to(JdbcTypeConverter.toGoogleDate((Time) value));
        } else if (value instanceof Timestamp) {
          return binder.to(JdbcTypeConverter.toGoogleDate((Timestamp) value));
        } else if (value instanceof LocalDate) {
          LocalDate localDate = (LocalDate) value;
          return binder.to(
              com.google.cloud.Date.fromYearMonthDay(
                  localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid date", Code.INVALID_ARGUMENT);
      case Types.TIME:
      case Types.TIME_WITH_TIMEZONE:
      case Types.TIMESTAMP:
      case Types.TIMESTAMP_WITH_TIMEZONE:
        if (value instanceof Date) {
          return binder.to(JdbcTypeConverter.toGoogleTimestamp((Date) value));
        } else if (value instanceof Time) {
          return binder.to(JdbcTypeConverter.toGoogleTimestamp((Time) value));
        } else if (value instanceof Timestamp) {
          return binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
        } else if (value instanceof OffsetDateTime) {
          OffsetDateTime offsetDateTime = (OffsetDateTime) value;
          return binder.to(
              com.google.cloud.Timestamp.ofTimeSecondsAndNanos(
                  offsetDateTime.toEpochSecond(), offsetDateTime.getNano()));
        }
        throw JdbcSqlExceptionFactory.of(
            value + " is not a valid timestamp", Code.INVALID_ARGUMENT);
      case Types.BINARY:
      case Types.VARBINARY:
      case Types.LONGVARBINARY:
        if (value instanceof byte[]) {
          return binder.to(ByteArray.copyFrom((byte[]) value));
        } else if (value instanceof InputStream) {
          try {
            return binder.to(ByteArray.copyFrom((InputStream) value));
          } catch (IOException e) {
            throw JdbcSqlExceptionFactory.of(
                "Could not copy bytes from input stream: " + e.getMessage(),
                Code.INVALID_ARGUMENT,
                e);
          }
        } else if (value instanceof AbstractMessage) {
          return binder.to((AbstractMessage) value);
        }
        throw JdbcSqlExceptionFactory.of(
            value + " is not a valid byte array", Code.INVALID_ARGUMENT);
      case Types.ARRAY:
        if (value instanceof Array) {
          Array jdbcArray = (Array) value;
          return setArrayValue(binder, sqlType, jdbcArray.getArray());
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid array", Code.INVALID_ARGUMENT);
      case Types.BLOB:
        if (value instanceof Blob) {
          try {
            return binder.to(ByteArray.copyFrom(((Blob) value).getBinaryStream()));
          } catch (IOException e) {
            throw JdbcSqlExceptionFactory.of(
                "could not set bytes from blob", Code.INVALID_ARGUMENT, e);
          }
        } else if (value instanceof InputStream) {
          try {
            return binder.to(ByteArray.copyFrom((InputStream) value));
          } catch (IOException e) {
            throw JdbcSqlExceptionFactory.of(
                "could not set bytes from input stream", Code.INVALID_ARGUMENT, e);
          }
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid blob", Code.INVALID_ARGUMENT);
      case Types.CLOB:
      case Types.NCLOB:
        if (value instanceof Clob) {
          try {
            return binder.to(CharStreams.toString(((Clob) value).getCharacterStream()));
          } catch (IOException e) {
            throw JdbcSqlExceptionFactory.of(
                "could not set string from clob", Code.INVALID_ARGUMENT, e);
          }
        } else if (value instanceof Reader) {
          try {
            return binder.to(CharStreams.toString((Reader) value));
          } catch (IOException e) {
            throw JdbcSqlExceptionFactory.of(
                "could not set string from reader", Code.INVALID_ARGUMENT, e);
          }
        }
        throw JdbcSqlExceptionFactory.of(value + " is not a valid clob", Code.INVALID_ARGUMENT);
      case ProtoMessageType.VENDOR_TYPE_NUMBER:
      case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
        if (value instanceof AbstractMessage) {
          return binder.to((AbstractMessage) value);
        } else if (value instanceof byte[]) {
          return binder.to(ByteArray.copyFrom((byte[]) value));
        } else {
          throw JdbcSqlExceptionFactory.of(
              value + " is not a valid PROTO value", Code.INVALID_ARGUMENT);
        }
      case ProtoEnumType.VENDOR_TYPE_NUMBER:
      case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
        if (value instanceof ProtocolMessageEnum) {
          return binder.to((ProtocolMessageEnum) value);
        } else if (value instanceof Number) {
          return binder.to(((Number) value).longValue());
        }
        throw JdbcSqlExceptionFactory.of(
            value + " is not a valid ENUM value", Code.INVALID_ARGUMENT);
    }
    return null;
  }

  private String getStringFromInputStream(InputStream inputStream) throws SQLException {
    InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII);
    try {
      return CharStreams.toString(reader);
    } catch (IOException e) {
      throw JdbcSqlExceptionFactory.of(
          "could not set string from input stream", Code.INVALID_ARGUMENT, e);
    }
  }

  private String getStringFromReader(Reader reader) throws SQLException {
    try {
      return CharStreams.toString(reader);
    } catch (IOException e) {
      throw JdbcSqlExceptionFactory.of(
          "could not set string from reader", Code.INVALID_ARGUMENT, e);
    }
  }

  /** Set the parameter value based purely on the type of the value. */
  private Builder setParamWithUnknownType(ValueBinder binder, Object value)
      throws SQLException {
    if (Boolean.class.isAssignableFrom(value.getClass())) {
      return binder.to((Boolean) value);
    } else if (Byte.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Byte) value).longValue());
    } else if (Short.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Short) value).longValue());
    } else if (Integer.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Integer) value).longValue());
    } else if (Long.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Long) value).longValue());
    } else if (Float.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Float) value).doubleValue());
    } else if (Double.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Double) value).doubleValue());
    } else if (BigDecimal.class.isAssignableFrom(value.getClass())) {
      if (dialect == Dialect.POSTGRESQL) {
        return binder.to(Value.pgNumeric(value.toString()));
      } else {
        return binder.to((BigDecimal) value);
      }
    } else if (Date.class.isAssignableFrom(value.getClass())) {
      Date dateValue = (Date) value;
      return binder.to(JdbcTypeConverter.toGoogleDate(dateValue));
    } else if (LocalDate.class.isAssignableFrom(value.getClass())) {
      LocalDate localDate = (LocalDate) value;
      return binder.to(
          com.google.cloud.Date.fromYearMonthDay(
              localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
    } else if (Timestamp.class.isAssignableFrom(value.getClass())) {
      return binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
    } else if (OffsetDateTime.class.isAssignableFrom(value.getClass())) {
      OffsetDateTime offsetDateTime = (OffsetDateTime) value;
      return binder.to(
          com.google.cloud.Timestamp.ofTimeSecondsAndNanos(
              offsetDateTime.toEpochSecond(), offsetDateTime.getNano()));
    } else if (Time.class.isAssignableFrom(value.getClass())) {
      Time timeValue = (Time) value;
      return binder.to(JdbcTypeConverter.toGoogleTimestamp(new Timestamp(timeValue.getTime())));
    } else if (String.class.isAssignableFrom(value.getClass())) {
      String stringVal = (String) value;
      return binder.to(stringVal);
    } else if (Reader.class.isAssignableFrom(value.getClass())) {
      try {
        Reader readable = (Reader) value;
        return binder.to(CharStreams.toString(readable));
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not read from readable", e);
      }
    } else if (Clob.class.isAssignableFrom(value.getClass())) {
      try {
        Clob clob = (Clob) value;
        return binder.to(CharStreams.toString(clob.getCharacterStream()));
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not read from readable", e);
      }
    } else if (Character.class.isAssignableFrom(value.getClass())) {
      return binder.to(((Character) value).toString());
    } else if (Character[].class.isAssignableFrom(value.getClass())) {
      List list = Arrays.asList((Character[]) value);
      StringBuilder s = new StringBuilder();
      for (Character c : list) {
        s.append(c.charValue());
      }
      return binder.to(s.toString());
    } else if (char[].class.isAssignableFrom(value.getClass())) {
      return binder.to(String.valueOf((char[]) value));
    } else if (URL.class.isAssignableFrom(value.getClass())) {
      return binder.to(value.toString());
    } else if (UUID.class.isAssignableFrom(value.getClass())) {
      return binder.to(((UUID) value).toString());
    } else if (byte[].class.isAssignableFrom(value.getClass())) {
      return binder.to(ByteArray.copyFrom((byte[]) value));
    } else if (InputStream.class.isAssignableFrom(value.getClass())) {
      try {
        return binder.to(ByteArray.copyFrom((InputStream) value));
      } catch (IOException e) {
        throw new IllegalArgumentException(
            "Could not copy bytes from input stream: " + e.getMessage(), e);
      }
    } else if (Blob.class.isAssignableFrom(value.getClass())) {
      try {
        return binder.to(ByteArray.copyFrom(((Blob) value).getBinaryStream()));
      } catch (IOException e) {
        throw new IllegalArgumentException(
            "Could not copy bytes from input stream: " + e.getMessage(), e);
      }
    } else if (Array.class.isAssignableFrom(value.getClass())) {
      try {
        Array jdbcArray = (Array) value;
        return setArrayValue(binder, jdbcArray.getBaseType(), jdbcArray.getArray());
      } catch (SQLException e) {
        throw new IllegalArgumentException(
            "Unsupported parameter type: " + value.getClass().getName() + " - " + value);
      }
    } else if (AbstractMessage.class.isAssignableFrom(value.getClass())) {
      return binder.to((AbstractMessage) value);
    } else if (ProtocolMessageEnum.class.isAssignableFrom(value.getClass())) {
      return binder.to((ProtocolMessageEnum) value);
    }
    return null;
  }

  private Builder setArrayValue(ValueBinder binder, int type, Object value)
      throws SQLException {
    if (value == null) {
      switch (type) {
        case Types.BIT:
        case Types.BOOLEAN:
          return binder.toBoolArray((boolean[]) null);
        case Types.TINYINT:
        case Types.SMALLINT:
        case Types.INTEGER:
        case Types.BIGINT:
          return binder.toInt64Array((long[]) null);
        case Types.REAL:
          return binder.toFloat32Array((float[]) null);
        case Types.FLOAT:
        case Types.DOUBLE:
          return binder.toFloat64Array((double[]) null);
        case Types.NUMERIC:
        case Types.DECIMAL:
          if (dialect == Dialect.POSTGRESQL) {
            return binder.toPgNumericArray(null);
          } else {
            return binder.toNumericArray(null);
          }
        case Types.CHAR:
        case Types.VARCHAR:
        case Types.LONGVARCHAR:
        case Types.NCHAR:
        case Types.NVARCHAR:
        case Types.LONGNVARCHAR:
        case Types.CLOB:
        case Types.NCLOB:
          return binder.toStringArray(null);
        case JsonType.VENDOR_TYPE_NUMBER:
        case JsonType.SHORT_VENDOR_TYPE_NUMBER:
          return binder.toJsonArray(null);
        case PgJsonbType.VENDOR_TYPE_NUMBER:
        case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
          return binder.toPgJsonbArray(null);
        case Types.DATE:
          return binder.toDateArray(null);
        case Types.TIME:
        case Types.TIME_WITH_TIMEZONE:
        case Types.TIMESTAMP:
        case Types.TIMESTAMP_WITH_TIMEZONE:
          return binder.toTimestampArray(null);
        case Types.BINARY:
        case Types.VARBINARY:
        case Types.LONGVARBINARY:
        case Types.BLOB:
          return binder.toBytesArray(null);
        case ProtoMessageType.VENDOR_TYPE_NUMBER:
        case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
        case ProtoEnumType.VENDOR_TYPE_NUMBER:
        case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
          return binder.to(
              Value.untyped(
                  com.google.protobuf.Value.newBuilder()
                      .setNullValue(NullValue.NULL_VALUE)
                      .build()));
        default:
          return binder.to(
              Value.untyped(
                  com.google.protobuf.Value.newBuilder()
                      .setNullValue(NullValue.NULL_VALUE)
                      .build()));
      }
    }

    if (boolean[].class.isAssignableFrom(value.getClass())) {
      return binder.toBoolArray((boolean[]) value);
    } else if (Boolean[].class.isAssignableFrom(value.getClass())) {
      return binder.toBoolArray(Arrays.asList((Boolean[]) value));
    } else if (short[].class.isAssignableFrom(value.getClass())) {
      long[] l = new long[((short[]) value).length];
      for (int i = 0; i < l.length; i++) {
        l[i] = ((short[]) value)[i];
      }
      return binder.toInt64Array(l);
    } else if (Short[].class.isAssignableFrom(value.getClass())) {
      return binder.toInt64Array(toLongList((Short[]) value));
    } else if (int[].class.isAssignableFrom(value.getClass())) {
      long[] l = new long[((int[]) value).length];
      for (int i = 0; i < l.length; i++) {
        l[i] = ((int[]) value)[i];
      }
      return binder.toInt64Array(l);
    } else if (Integer[].class.isAssignableFrom(value.getClass())) {
      return binder.toInt64Array(toLongList((Integer[]) value));
    } else if (long[].class.isAssignableFrom(value.getClass())) {
      return binder.toInt64Array((long[]) value);
    } else if (Long[].class.isAssignableFrom(value.getClass())) {
      return binder.toInt64Array(toLongList((Long[]) value));
    } else if (float[].class.isAssignableFrom(value.getClass())) {
      return binder.toFloat32Array((float[]) value);
    } else if (Float[].class.isAssignableFrom(value.getClass())) {
      return binder.toFloat32Array(toFloatList((Float[]) value));
    } else if (double[].class.isAssignableFrom(value.getClass())) {
      return binder.toFloat64Array((double[]) value);
    } else if (Double[].class.isAssignableFrom(value.getClass())) {
      return binder.toFloat64Array(toDoubleList((Double[]) value));
    } else if (BigDecimal[].class.isAssignableFrom(value.getClass())) {
      if (dialect == Dialect.POSTGRESQL) {
        return binder.toPgNumericArray(
            Arrays.stream((BigDecimal[]) value)
                .map(bigDecimal -> bigDecimal == null ? null : bigDecimal.toString())
                .collect(Collectors.toList()));
      } else {
        return binder.toNumericArray(Arrays.asList((BigDecimal[]) value));
      }
    } else if (Date[].class.isAssignableFrom(value.getClass())) {
      return binder.toDateArray(JdbcTypeConverter.toGoogleDates((Date[]) value));
    } else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
      return binder.toTimestampArray(JdbcTypeConverter.toGoogleTimestamps((Timestamp[]) value));
    } else if (String[].class.isAssignableFrom(value.getClass())) {
      if (type == JsonType.VENDOR_TYPE_NUMBER || type == JsonType.SHORT_VENDOR_TYPE_NUMBER) {
        return binder.toJsonArray(Arrays.asList((String[]) value));
      } else if (type == PgJsonbType.VENDOR_TYPE_NUMBER
          || type == PgJsonbType.SHORT_VENDOR_TYPE_NUMBER) {
        return binder.toPgJsonbArray(Arrays.asList((String[]) value));
      } else {
        return binder.toStringArray(Arrays.asList((String[]) value));
      }
    } else if (byte[][].class.isAssignableFrom(value.getClass())) {
      return binder.toBytesArray(JdbcTypeConverter.toGoogleBytes((byte[][]) value));
    } else if (AbstractMessage[].class.isAssignableFrom(value.getClass())) {
      return bindProtoMessageArray(binder, value);
    } else if (ProtocolMessageEnum[].class.isAssignableFrom(value.getClass())) {
      return bindProtoEnumArray(binder, value);
    }
    return null;
  }

  private Builder bindProtoMessageArray(ValueBinder binder, Object value)
      throws SQLException {
    Class componentType = value.getClass().getComponentType();
    int length = java.lang.reflect.Array.getLength(value);
    List convertedArray = new ArrayList<>();
    try {
      Method method = componentType.getMethod("toByteArray");
      for (int i = 0; i < length; i++) {
        Object element = java.lang.reflect.Array.get(value, i);
        if (element != null) {
          byte[] l = (byte[]) method.invoke(element);
          convertedArray.add(ByteArray.copyFrom(l));
        } else {
          convertedArray.add(null);
        }
      }

      Message.Builder builder =
          (Message.Builder) componentType.getMethod("newBuilder").invoke(null);
      Descriptor msgDescriptor = builder.getDescriptorForType();

      return binder.toProtoMessageArray(convertedArray, msgDescriptor.getFullName());
    } catch (Exception e) {
      throw JdbcSqlExceptionFactory.of(
          "Error occurred when binding Array of Proto Message input", Code.UNKNOWN, e);
    }
  }

  private Builder bindProtoEnumArray(ValueBinder binder, Object value)
      throws SQLException {
    Class componentType = value.getClass().getComponentType();
    int length = java.lang.reflect.Array.getLength(value);
    List convertedArray = new ArrayList<>();
    try {
      Method method = componentType.getMethod("getNumber");
      for (int i = 0; i < length; i++) {
        Object element = java.lang.reflect.Array.get(value, i);
        if (element != null) {
          int op = (int) method.invoke(element);
          convertedArray.add((long) op);
        } else {
          convertedArray.add(null);
        }
      }

      Descriptors.EnumDescriptor enumDescriptor =
          (Descriptors.EnumDescriptor) componentType.getMethod("getDescriptor").invoke(null);

      return binder.toProtoEnumArray(convertedArray, enumDescriptor.getFullName());
    } catch (Exception e) {
      throw JdbcSqlExceptionFactory.of(
          "Error occurred when binding Array of Proto Enum input", Code.UNKNOWN, e);
    }
  }

  private List toLongList(Number[] input) {
    List res = new ArrayList<>(input.length);
    for (Number number : input) {
      res.add(number == null ? null : number.longValue());
    }
    return res;
  }

  private List toFloatList(Number[] input) {
    List res = new ArrayList<>(input.length);
    for (Number number : input) {
      res.add(number == null ? null : number.floatValue());
    }
    return res;
  }

  private List toDoubleList(Number[] input) {
    List res = new ArrayList<>(input.length);
    for (Number number : input) {
      res.add(number == null ? null : number.doubleValue());
    }
    return res;
  }

  /**
   * Sets a null value with a specific SQL type. If the sqlType is null, the value will be set as a
   * String.
   */
  private Builder setNullValue(ValueBinder binder, Integer sqlType) {
    if (sqlType == null) {
      return binder.to(
          Value.untyped(
              com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()));
    }
    int type = sqlType;
    switch (type) {
      case Types.BIGINT:
        return binder.to((Long) null);
      case Types.BINARY:
      case ProtoMessageType.VENDOR_TYPE_NUMBER:
      case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
        return binder.to((ByteArray) null);
      case Types.BLOB:
        return binder.to((ByteArray) null);
      case Types.BIT:
      case Types.BOOLEAN:
        return binder.to((Boolean) null);
      case Types.CHAR:
        return binder.to((String) null);
      case Types.CLOB:
        return binder.to((String) null);
      case Types.DATE:
        return binder.to((com.google.cloud.Date) null);
      case Types.NUMERIC:
      case Types.DECIMAL:
        if (dialect == Dialect.POSTGRESQL) {
          return binder.to(Value.pgNumeric(null));
        } else {
          return binder.to((BigDecimal) null);
        }
      case Types.FLOAT:
      case Types.DOUBLE:
        return binder.to((Double) null);
      case Types.INTEGER:
      case ProtoEnumType.VENDOR_TYPE_NUMBER:
      case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
        return binder.to((Long) null);
      case Types.LONGNVARCHAR:
        return binder.to((String) null);
      case Types.LONGVARBINARY:
        return binder.to((ByteArray) null);
      case Types.LONGVARCHAR:
        return binder.to((String) null);
      case Types.NCHAR:
        return binder.to((String) null);
      case Types.NCLOB:
        return binder.to((String) null);
      case Types.NVARCHAR:
        return binder.to((String) null);
      case Types.REAL:
        return binder.to((Float) null);
      case Types.SMALLINT:
        return binder.to((Long) null);
      case Types.SQLXML:
        return binder.to((String) null);
      case Types.TIME:
      case Types.TIME_WITH_TIMEZONE:
      case Types.TIMESTAMP:
      case Types.TIMESTAMP_WITH_TIMEZONE:
        return binder.to((com.google.cloud.Timestamp) null);
      case Types.TINYINT:
        return binder.to((Long) null);
      case Types.VARBINARY:
        return binder.to((ByteArray) null);
      case Types.VARCHAR:
        return binder.to((String) null);
      case JsonType.VENDOR_TYPE_NUMBER:
      case JsonType.SHORT_VENDOR_TYPE_NUMBER:
        return binder.to(Value.json(null));
      case PgJsonbType.VENDOR_TYPE_NUMBER:
      case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
        return binder.to(Value.pgJsonb(null));
      default:
        return binder.to(
            Value.untyped(
                com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()));
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy