// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2025 MariaDB Corporation Ab
package org.mariadb.jdbc;

import static org.mariadb.jdbc.util.constants.Capabilities.BULK_UNIT_RESULTS;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.sql.Date;
import java.sql.ParameterMetaData;
import java.util.*;
import org.mariadb.jdbc.client.ColumnDecoder;
import org.mariadb.jdbc.client.Completion;
import org.mariadb.jdbc.client.DataType;
import org.mariadb.jdbc.client.result.CompleteResult;
import org.mariadb.jdbc.client.result.Result;
import org.mariadb.jdbc.client.util.ClosableLock;
import org.mariadb.jdbc.client.util.Parameters;
import org.mariadb.jdbc.codec.*;
import org.mariadb.jdbc.export.ExceptionFactory;
import org.mariadb.jdbc.export.Prepare;
import org.mariadb.jdbc.message.ClientMessage;
import org.mariadb.jdbc.message.client.BulkExecutePacket;
import org.mariadb.jdbc.message.client.PreparePacket;
import org.mariadb.jdbc.message.server.OkPacket;
import org.mariadb.jdbc.message.server.PrepareResultPacket;
import org.mariadb.jdbc.plugin.Codec;
import org.mariadb.jdbc.plugin.array.FloatArray;
import org.mariadb.jdbc.plugin.codec.*;
import org.mariadb.jdbc.util.ParameterList;
import org.mariadb.jdbc.util.constants.ColumnFlags;
import org.mariadb.jdbc.util.timeout.QueryTimeoutHandler;

/** Common methods for prepare statement, for client and server prepare statement. */
public abstract class BasePreparedStatement extends Statement implements PreparedStatement {

  /** prepare statement sql command */
  protected final String sql;

  /** parameters */
  protected Parameters parameters;

  /** batching parameters */
  protected List<Parameters> batchParameters;

  /** PREPARE command result */
  protected Prepare prepareResult = null;

  /**
   * Constructor
   *
   * @param sql sql command
   * @param con connection
   * @param lock thread safe lock
   * @param autoGeneratedKeys indicate if automatif generated key retrival is required
   * @param resultSetType resultset type
   * @param resultSetConcurrency resultset concurrency
   * @param defaultFetchSize default fetch size
   */
  public BasePreparedStatement(
      String sql,
      Connection con,
      ClosableLock lock,
      int autoGeneratedKeys,
      int resultSetType,
      int resultSetConcurrency,
      int defaultFetchSize) {
    super(con, lock, autoGeneratedKeys, resultSetType, resultSetConcurrency, defaultFetchSize);
    this.sql = sql;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("sql:'" + sql + "'");
    sb.append(", parameters:[");
    for (int i = 0; i < parameters.size(); i++) {
      org.mariadb.jdbc.client.util.Parameter param = parameters.get(i);
      if (param == null) {
        sb.append("null");
      } else {
        sb.append(param.bestEffortStringValue(con.getContext()));
      }
      if (i != parameters.size() - 1) {
        sb.append(",");
      }
    }
    sb.append("]");
    return sb.toString();
  }

  @Override
  public String getLastSql() {
    return sql;
  }

  /**
   * Set PREPARE result
   *
   * @param prepareResult prepare result
   */
  public void setPrepareResult(Prepare prepareResult) {
    this.prepareResult = prepareResult;
  }

  /**
   * Get cached metadata list
   *
   * @return metadata list
   */
  public ColumnDecoder[] getMeta() {
    return this.prepareResult.getColumns();
  }

  /**
   * update cached metadata list
   *
   * @param ci metadata columns
   */
  public void updateMeta(ColumnDecoder[] ci) {
    this.prepareResult.setColumns(ci);
  }

  public abstract boolean execute() throws SQLException;

  public abstract ResultSet executeQuery() throws SQLException;

  public abstract int executeUpdate() throws SQLException;

  public abstract long executeLargeUpdate() throws SQLException;

  public abstract void addBatch() throws SQLException;

  public abstract ResultSetMetaData getMetaData() throws SQLException;

  public abstract ParameterMetaData getParameterMetaData() throws SQLException;

  /**
   * Set parameter
   *
   * @param index parameter index
   * @param param parameter
   */
  public void setParameter(int index, org.mariadb.jdbc.client.util.Parameter param) {
    parameters.set(index, param);
  }

  // ***************************************************************************************************
  // methods inherited from Statement that are disabled
  // ***************************************************************************************************

  @Override
  public void addBatch(String sql) throws SQLException {
    throw exceptionFactory().create("addBatch(String sql) cannot be called on preparedStatement");
  }

  @Override
  public boolean execute(String sql) throws SQLException {
    throw exceptionFactory().create("execute(String sql) cannot be called on preparedStatement");
  }

  @Override
  public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
    throw exceptionFactory()
        .create("execute(String sql, int autoGeneratedKeys) cannot be called on preparedStatement");
  }

  @Override
  public boolean execute(String sql, int[] columnIndexes) throws SQLException {
    throw exceptionFactory()
        .create("execute(String sql, int[] columnIndexes) cannot be called on preparedStatement");
  }

  @Override
  public boolean execute(String sql, String[] columnNames) throws SQLException {
    throw exceptionFactory()
        .create("execute(String sql, String[] columnNames) cannot be called on preparedStatement");
  }

  @Override
  public ResultSet executeQuery(String sql) throws SQLException {
    throw exceptionFactory()
        .create("executeQuery(String sql) cannot be called on preparedStatement");
  }

  @Override
  public int executeUpdate(String sql) throws SQLException {
    throw exceptionFactory()
        .create("executeUpdate(String sql) cannot be called on preparedStatement");
  }

  @Override
  public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeUpdate(String sql, int autoGeneratedKeys) cannot be called on"
                + " preparedStatement");
  }

  @Override
  public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeUpdate(String sql, int[] columnIndexes) cannot be called on preparedStatement");
  }

  @Override
  public int executeUpdate(String sql, String[] columnNames) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeUpdate(String sql, String[] columnNames) cannot be called on"
                + " preparedStatement");
  }

  @Override
  public long executeLargeUpdate(String sql) throws SQLException {
    throw exceptionFactory()
        .create("executeLargeUpdate(String sql) cannot be called on preparedStatement");
  }

  @Override
  public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeLargeUpdate(String sql, int autoGeneratedKeys) cannot be called on"
                + " preparedStatement");
  }

  @Override
  public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeLargeUpdate(String sql, int[] columnIndexes) cannot be called on"
                + " preparedStatement");
  }

  @Override
  public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
    throw exceptionFactory()
        .create(
            "executeLargeUpdate(String sql, String[] columnNames) cannot be called on"
                + " preparedStatement");
  }

  // ***************************************************************************************************
  // Setters
  // ***************************************************************************************************

  private void checkIndex(int index) throws SQLException {
    if (index <= 0) {
      throw exceptionFactory().create(String.format("wrong parameter index %s", index));
    }
  }

  /**
   * Sets the designated parameter to SQL <code>NULL</code>.
   *
   * <p><B>Note:</B> You must specify the parameter's SQL type.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param sqlType the SQL type code defined in <code>java.sql.Types</code>
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> data type and
   *     the JDBC driver does not support this data type
   */
  @Override
  public void setNull(int parameterIndex, int sqlType) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
  }

  /**
   * Sets the designated parameter to the given Java <code>boolean</code> value. The driver converts
   * this to an SQL <code>BIT</code> or <code>BOOLEAN</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setBoolean(int parameterIndex, boolean x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(BooleanCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>byte</code> value. The driver converts
   * this to an SQL <code>TINYINT</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setByte(int parameterIndex, byte x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(ByteCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>short</code> value. The driver converts
   * this to an SQL <code>SMALLINT</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setShort(int parameterIndex, short x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(ShortCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>int</code> value. The driver converts
   * this to an SQL <code>INTEGER</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setInt(int parameterIndex, int x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(IntCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>long</code> value. The driver converts
   * this to an SQL <code>BIGINT</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setLong(int parameterIndex, long x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(LongCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>float</code> value. The driver converts
   * this to an SQL <code>REAL</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setFloat(int parameterIndex, float x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(FloatCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>double</code> value. The driver converts
   * this to an SQL <code>DOUBLE</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setDouble(int parameterIndex, double x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new NonNullParameter<>(DoubleCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.math.BigDecimal</code> value. The driver
   * converts this to an SQL <code>NUMERIC</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(BigDecimalCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java <code>String</code> value. The driver converts
   * this to an SQL <code>VARCHAR</code> or <code>LONGVARCHAR</code> value (depending on the
   * argument's size relative to the driver's limits on <code>VARCHAR</code> values) when it sends
   * it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setString(int parameterIndex, String x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given Java array of bytes. The driver converts this to an
   * SQL <code>VARBINARY</code> or <code>LONGVARBINARY</code> (depending on the argument's size
   * relative to the driver's limits on <code>VARBINARY</code> values) when it sends it to the
   * database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setBytes(int parameterIndex, byte[] x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ByteArrayCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Date</code> value using the default
   * time zone of the virtual machine that is running the application. The driver converts this to
   * an SQL <code>DATE</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setDate(int parameterIndex, Date x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(DateCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Time</code> value. The driver
   * converts this to an SQL <code>TIME</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setTime(int parameterIndex, Time x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Timestamp</code> value. The driver
   * converts this to an SQL <code>TIMESTAMP</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given input stream, which will have the specified number
   * of bytes. When a very large ASCII value is input to a <code>LONGVARCHAR</code> parameter, it
   * may be more practical to send it via a <code>java.io.InputStream</code>. Data will be read from
   * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary
   * conversion from ASCII to the database char format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the Java input stream that contains the ASCII parameter value
   * @param length the number of bytes in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
  }

  /**
   * Sets the designated parameter to the given input stream, which will have the specified number
   * of bytes.
   *
   * <p>When a very large Unicode value is input to a <code>LONGVARCHAR</code> parameter, it may be
   * more practical to send it via a <code>java.io.InputStream</code> object. The data will be read
   * from the stream as needed until end-of-file is reached. The JDBC driver will do any necessary
   * conversion from Unicode to the database char format.
   *
   * <p>The byte format of the Unicode stream must be a Java UTF-8, as defined in the Java Virtual
   * Machine Specification.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x a <code>java.io.InputStream</code> object that contains the Unicode parameter value
   * @param length the number of bytes in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @deprecated Use {@code setCharacterStream}
   */
  @Override
  @Deprecated
  public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
  }

  /**
   * Sets the designated parameter to the given input stream, which will have the specified number
   * of bytes. When a very large binary value is input to a <code>LONGVARBINARY</code> parameter, it
   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
   * be read from the stream as needed until end-of-file is reached.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the java input stream which contains the binary parameter value
   * @param length the number of bytes in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   */
  @Override
  public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
  }

  /**
   * Clears the current parameter values immediately.
   *
   * <p>In general, parameter values remain in force for repeated use of a statement. Setting a
   * parameter value automatically clears its previous value. However, in some cases it is useful to
   * immediately release the resources used by the current parameter values; this can be done by
   * calling the method <code>clearParameters</code>.
   *
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>PreparedStatement</code>
   */
  @Override
  public void clearParameters() throws SQLException {
    checkNotClosed();
    parameters = new ParameterList();
  }

  @Override
  public void clearBatch() throws SQLException {
    batchParameters = new ArrayList<>();
    super.clearBatch();
  }

  /**
   * Sets the value of the designated parameter with the given object.
   *
   * <p>This method is similar to {@link #setObject(int parameterIndex, Object x, int targetSqlType,
   * int scaleOrLength)}, except that it assumes a scale of zero.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the object containing the input parameter value
   * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed
   *     PreparedStatement
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified
   *     targetSqlType
   * @see Types
   */
  @Override
  public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
    setInternalObject(parameterIndex, x, targetSqlType, null);
  }

  /**
   * Sets the value of the designated parameter using the given object.
   *
   * <p>The JDBC specification specifies a standard mapping from Java <code>Object</code> types to
   * SQL types. The given argument will be converted to the corresponding SQL type before being sent
   * to the database.
   *
   * <p>Note that this method may be used to pass datatabase- specific abstract data types, by using
   * a driver-specific Java type.
   *
   * <p>If the object is of a class implementing the interface <code>SQLData</code>, the JDBC driver
   * should call the method <code>SQLData.writeSQL</code> to write it to the SQL data stream. If, on
   * the other hand, the object is of a class implementing <code>Ref</code>, <code>Blob</code>,
   * <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>java.net.URL</code>, <code>
   * RowId</code>, <code>SQLXML</code> or <code>Array</code>, the driver should pass it to the
   * database as a value of the corresponding SQL type.
   *
   * <p><b>Note:</b> Not all databases allow for a non-typed Null to be sent to the backend. For
   * maximum portability, the <code>setNull</code> or the <code>
   * setObject(int parameterIndex, Object x, int sqlType)</code> method should be used instead of
   * <code>setObject(int parameterIndex, Object x)</code>.
   *
   * <p><b>Note:</b> This method throws an exception if there is an ambiguity, for example, if the
   * object is of a class implementing more than one of the interfaces named above.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the object containing the input parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code> or the type of the given object is ambiguous
   */
  @Override
  public void setObject(int parameterIndex, Object x) throws SQLException {
    setInternalObject(parameterIndex, x, null, null);
  }

  /**
   * Sets the designated parameter to the given <code>Reader</code> object, which is the given
   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
   * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC
   * driver will do any necessary conversion from UNICODE to the database char format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param reader the <code>java.io.Reader</code> object that contains the Unicode data
   * @param length the number of characters in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.2
   */
  @Override
  public void setCharacterStream(int parameterIndex, Reader reader, int length)
      throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(
        parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, (long) length));
  }

  /**
   * Sets the designated parameter to the given <code>REF(&lt;structured-type&gt;)</code> value. The
   * driver converts this to an SQL <code>REF</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x an SQL <code>REF</code> value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.2
   */
  @Override
  public void setRef(int parameterIndex, Ref x) throws SQLException {
    throw exceptionFactory().notSupported("REF parameter are not supported");
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Blob</code> object. The driver
   * converts this to an SQL <code>BLOB</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x a <code>Blob</code> object that maps an SQL <code>BLOB</code> value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.2
   */
  @Override
  public void setBlob(int parameterIndex, Blob x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(BlobCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Clob</code> object. The driver
   * converts this to an SQL <code>CLOB</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x a <code>Clob</code> object that maps an SQL <code>CLOB</code> value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.2
   */
  @Override
  public void setClob(int parameterIndex, Clob x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Array</code> object. The driver
   * converts this to an SQL <code>ARRAY</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x an <code>Array</code> object that maps an SQL <code>ARRAY</code> value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.2
   */
  @Override
  public void setArray(int parameterIndex, Array x) throws SQLException {
    checkIndex(parameterIndex);
    if (x == null) {
      parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
      return;
    }
    if (x instanceof FloatArray) {
      parameters.set(
          parameterIndex - 1, new Parameter<>(FloatArrayCodec.INSTANCE, (float[]) x.getArray()));
      return;
    }
    throw exceptionFactory()
        .notSupported(
            String.format("this type of Array parameter %s is not supported", x.getClass()));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Date</code> value, using the given
   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
   * SQL <code>DATE</code> value, which the driver then sends to the database. With a <code>Calendar
   * </code> object, the driver can calculate the date taking into account a custom timezone. If no
   * <code>Calendar</code> object is specified, the driver uses the default timezone, which is that
   * of the virtual machine running the application.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @param cal the <code>Calendar</code> object the driver will use to construct the date
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.2
   */
  @Override
  public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new ParameterWithCal<>(DateCodec.INSTANCE, x, cal));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Time</code> value, using the given
   * <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to construct an
   * SQL <code>TIME</code> value, which the driver then sends to the database. With a <code>Calendar
   * </code> object, the driver can calculate the time taking into account a custom timezone. If no
   * <code>Calendar</code> object is specified, the driver uses the default timezone, which is that
   * of the virtual machine running the application.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @param cal the <code>Calendar</code> object the driver will use to construct the time
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.2
   */
  @Override
  public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new ParameterWithCal<>(TimeCodec.INSTANCE, x, cal));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.Timestamp</code> value, using the
   * given <code>Calendar</code> object. The driver uses the <code>Calendar</code> object to
   * construct an SQL <code>TIMESTAMP</code> value, which the driver then sends to the database.
   * With a <code>Calendar</code> object, the driver can calculate the timestamp taking into account
   * a custom timezone. If no <code>Calendar</code> object is specified, the driver uses the default
   * timezone, which is that of the virtual machine running the application.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @param cal the <code>Calendar</code> object the driver will use to construct the timestamp
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.2
   */
  @Override
  public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new ParameterWithCal<>(TimestampCodec.INSTANCE, x, cal));
  }

  /**
   * Sets the designated parameter to SQL <code>NULL</code>. This version of the method <code>
   * setNull</code> should be used for user-defined types and REF type parameters. Examples of
   * user-defined types include: STRUCT, DISTINCT, JAVA_OBJECT, and named array types.
   *
   * <p><B>Note:</B> To be portable, applications must give the SQL type code and the
   * fully-qualified SQL type name when specifying a NULL user-defined or REF parameter. In the case
   * of a user-defined type the name is the type name of the parameter itself. For a REF parameter,
   * the name is the type name of the referenced type. If a JDBC driver does not need the type code
   * or type name information, it may ignore it.
   *
   * <p>Although it is intended for user-defined and Ref parameters, this method may be used to set
   * a null parameter of any JDBC type. If the parameter does not have a user-defined or REF type,
   * the given typeName is ignored.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param sqlType a value from <code>java.sql.Types</code>
   * @param typeName the fully-qualified name of an SQL user-defined type; ignored if the parameter
   *     is not a user-defined type or REF
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if <code>sqlType</code> is a <code>ARRAY</code>, <code>
   *     BLOB</code>, <code>CLOB</code>, <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>
   *     NCHAR</code>, <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>, <code>
   *     REF</code>, <code>ROWID</code>, <code>SQLXML</code> or <code>STRUCT</code> data type and
   *     the JDBC driver does not support this data type or if the JDBC driver does not support this
   *     method
   * @since 1.2
   */
  @Override
  public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
  }

  /**
   * Sets the designated parameter to the given <code>java.net.URL</code> value. The driver converts
   * this to an SQL <code>DATALINK</code> value when it sends it to the database.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the <code>java.net.URL</code> object to be set
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.4
   */
  @Override
  public void setURL(int parameterIndex, URL x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(
        parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, x == null ? null : x.toString()));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.RowId</code> object. The driver
   * converts this to an SQL <code>ROWID</code> value when it sends it to the database
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setRowId(int parameterIndex, RowId x) throws SQLException {
    throw exceptionFactory().notSupported("RowId parameter are not supported");
  }

  /**
   * Sets the designated parameter to the given <code>String</code> object. The driver converts this
   * to an SQL <code>NCHAR</code> or <code>NVARCHAR</code> or <code>LONGNVARCHAR</code> value
   * (depending on the argument's size relative to the driver's limits on <code>NVARCHAR</code>
   * values) when it sends it to the database.
   *
   * @param parameterIndex of the first parameter is 1, the second is 2, ...
   * @param value the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the driver does not support national character sets; if the driver can detect
   *     that a data conversion error could occur; if a database access error occurs; or this method
   *     is called on a closed <code>PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNString(int parameterIndex, String value) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StringCodec.INSTANCE, value));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. The <code>Reader</code> reads
   * the data till end-of-file is reached. The driver does the necessary conversion from Java
   * character format to the national character set in the database.
   *
   * @param parameterIndex of the first parameter is 1, the second is 2, ...
   * @param value the parameter value
   * @param length the number of characters in the parameter data.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the driver does not support national character sets; if the driver can detect
   *     that a data conversion error could occur; if a database access error occurs; or this method
   *     is called on a closed <code>PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNCharacterStream(int parameterIndex, Reader value, long length)
      throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value, length));
  }

  /**
   * Sets the designated parameter to a <code>java.sql.NClob</code> object. The driver converts this
   * to an SQL <code>NCLOB</code> value when it sends it to the database.
   *
   * @param parameterIndex of the first parameter is 1, the second is 2, ...
   * @param value the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the driver does not support national character sets; if the driver can detect
   *     that a data conversion error could occur; if a database access error occurs; or this method
   *     is called on a closed <code>PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNClob(int parameterIndex, NClob value) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ClobCodec.INSTANCE, value));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. The reader must contain the
   * number of characters specified by length otherwise a <code>SQLException</code> will be
   * generated when the <code>PreparedStatement</code> is executed. This method differs from the
   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
   * the parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
   * setCharacterStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGVARCHAR</code> or a
   * <code>CLOB</code>
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param reader An object that contains the data to set the parameter value to.
   * @param length the number of characters in the parameter data.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code> or if the length specified is less than zero.
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
  }

  /**
   * Sets the designated parameter to a <code>InputStream</code> object. The inputstream must
   * contain the number of characters specified by length otherwise a <code>SQLException</code> will
   * be generated when the <code>PreparedStatement</code> is executed. This method differs from the
   * <code>setBinaryStream (int, InputStream, int)</code> method because it informs the driver that
   * the parameter value should be sent to the server as a <code>BLOB</code>. When the <code>
   * setBinaryStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGVARBINARY</code> or a
   * <code>BLOB</code>
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param inputStream An object that contains the data to set the parameter value to.
   * @param length the number of bytes in the parameter data.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code>; if the length specified is less than zero or if the number of
   *     bytes in the inputstream does not match the specified length.
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setBlob(int parameterIndex, InputStream inputStream, long length)
      throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream, length));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. The reader must contain the
   * number of characters specified by length otherwise a <code>SQLException</code> will be
   * generated when the <code>PreparedStatement</code> is executed. This method differs from the
   * <code>setCharacterStream (int, Reader, int)</code> method because it informs the driver that
   * the parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
   * setCharacterStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGNVARCHAR</code> or a
   * <code>NCLOB</code>
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param reader An object that contains the data to set the parameter value to.
   * @param length the number of characters in the parameter data.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the length specified is less than zero; if the driver does not support
   *     national character sets; if the driver can detect that a data conversion error could occur;
   *     if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
  }

  /**
   * Sets the designated parameter to the given <code>java.sql.SQLXML</code> object. The driver
   * converts this to an SQL <code>XML</code> value when it sends it to the database.
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param xmlObject a <code>SQLXML</code> object that maps an SQL <code>XML</code> value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code> or the <code>java.xml.transform.Result</code>, <code>Writer</code>
   *     or <code>OutputStream</code> has not been closed for the <code>SQLXML</code> object
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
    throw exceptionFactory().notSupported("SQLXML parameter are not supported");
  }

  private ExceptionFactory exceptionFactory() {
    return con.getExceptionFactory().of(this);
  }

  /**
   * Sets the value of the designated parameter with the given object.
   *
   * <p>If the second argument is an <code>InputStream</code> then the stream must contain the
   * number of bytes specified by scaleOrLength. If the second argument is a <code>Reader</code>
   * then the reader must contain the number of characters specified by scaleOrLength. If these
   * conditions are not true the driver will generate a <code>SQLException</code> when the prepared
   * statement is executed.
   *
   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
   * database.
   *
   * <p>If the object has a custom mapping (is of a class implementing the interface <code>SQLData
   * </code>), the JDBC driver should call the method <code>SQLData.writeSQL</code> to write it to
   * the SQL data stream. If, on the other hand, the object is of a class implementing <code>Ref
   * </code>, <code>Blob</code>, <code>Clob</code>, <code>NClob</code>, <code>Struct</code>, <code>
   * java.net.URL</code>, or <code>Array</code>, the driver should pass it to the database as a
   * value of the corresponding SQL type.
   *
   * <p>Note that this method may be used to pass database-specific abstract data types.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the object containing the input parameter value
   * @param targetSqlType the SQL type (as defined in java.sql.Types) to be sent to the database.
   *     The scale argument may further qualify this type.
   * @param scaleOrLength for <code>java.sql.Types.DECIMAL</code> or <code>
   *     java.sql.Types.NUMERIC types</code>, this is the number of digits after the decimal point.
   *     For Java Object types <code>InputStream</code> and <code>Reader</code>, this is the length
   *     of the data in the stream or reader. For all other types, this value will be ignored.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code> or if the Java Object specified by x is an InputStream or Reader
   *     object and the value of the scale parameter is less than zero
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified
   *     targetSqlType
   * @see Types
   */
  @Override
  public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength)
      throws SQLException {
    setInternalObject(parameterIndex, x, targetSqlType, (long) scaleOrLength);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private void setInternalObject(
      int parameterIndex, Object obj, Integer targetSqlType, Long scaleOrLength)
      throws SQLException {
    checkIndex(parameterIndex);
    if (obj == null) {
      parameters.set(parameterIndex - 1, Parameter.NULL_PARAMETER);
      return;
    }

    if (targetSqlType != null) {
      if (trySetArrayType(parameterIndex, obj, targetSqlType)) return;
      checkUnsupportedTypes(targetSqlType);
      if (trySetStringOrCharacter(parameterIndex, obj, targetSqlType)) return;
      if (trySetNumber(parameterIndex, obj, targetSqlType)) return;
      if (trySetByteArray(parameterIndex, obj, targetSqlType, scaleOrLength)) return;
    }

    // in case parameter still not set, defaulting to object type
    trySetWithCodec(parameterIndex, obj, scaleOrLength);
  }

  private boolean trySetArrayType(int parameterIndex, Object obj, Integer targetSqlType)
      throws SQLException {
    if (targetSqlType != Types.ARRAY) return false;

    if (obj instanceof float[]) {
      parameters.set(parameterIndex - 1, new Parameter<>(FloatArrayCodec.INSTANCE, (float[]) obj));
      return true;
    }
    if (obj instanceof Float[]) {
      parameters.set(
          parameterIndex - 1, new Parameter<>(FloatObjectArrayCodec.INSTANCE, (Float[]) obj));
      return true;
    }
    if (obj instanceof FloatArray) {
      parameters.set(
          parameterIndex - 1,
          new Parameter<>(FloatArrayCodec.INSTANCE, (float[]) ((FloatArray) obj).getArray()));
      return true;
    }

    throw exceptionFactory()
        .notSupported(String.format("ARRAY Type not supported for %s", obj.getClass().getName()));
  }

  private void checkUnsupportedTypes(Integer targetSqlType) throws SQLException {
    switch (targetSqlType) {
      case Types.DATALINK:
      case Types.JAVA_OBJECT:
      case Types.REF:
      case Types.ROWID:
      case Types.SQLXML:
      case Types.STRUCT:
        throw exceptionFactory().notSupported("Type not supported");
    }
  }

  private boolean trySetStringOrCharacter(int parameterIndex, Object obj, Integer targetSqlType)
      throws SQLException {
    if (!(obj instanceof String || obj instanceof Character)) return false;

    if (targetSqlType == Types.BLOB) {
      throw exceptionFactory()
          .create(
              String.format(
                  "Cannot convert a %s to a Blob", obj instanceof String ? "string" : "character"));
    }

    String str = obj instanceof String ? (String) obj : ((Character) obj).toString();
    return handleStringConversion(parameterIndex, str, targetSqlType);
  }

  private boolean handleStringConversion(int parameterIndex, String str, Integer targetSqlType)
      throws SQLException {
    try {
      switch (targetSqlType) {
        case Types.BIT:
        case Types.BOOLEAN:
          setBoolean(parameterIndex, !("false".equalsIgnoreCase(str) || "0".equals(str)));
          return true;
        case Types.TINYINT:
          setByte(parameterIndex, Byte.parseByte(str));
          return true;
        case Types.SMALLINT:
          setShort(parameterIndex, Short.parseShort(str));
          return true;
        case Types.INTEGER:
          setInt(parameterIndex, Integer.parseInt(str));
          return true;
        case Types.DOUBLE:
        case Types.FLOAT:
          setDouble(parameterIndex, Double.parseDouble(str));
          return true;
        case Types.REAL:
          setFloat(parameterIndex, Float.parseFloat(str));
          return true;
        case Types.BIGINT:
          setLong(parameterIndex, Long.parseLong(str));
          return true;
        case Types.DECIMAL:
        case Types.NUMERIC:
          setBigDecimal(parameterIndex, new BigDecimal(str));
          return true;
        case Types.CLOB:
        case Types.NCLOB:
        case Types.CHAR:
        case Types.VARCHAR:
        case Types.LONGVARCHAR:
        case Types.NCHAR:
        case Types.NVARCHAR:
        case Types.LONGNVARCHAR:
          setString(parameterIndex, str);
          return true;
        case Types.TIMESTAMP:
          handleTimestampString(parameterIndex, str);
          return true;
        case Types.TIME:
          setTime(parameterIndex, Time.valueOf(str));
          return true;
      }
    } catch (IllegalArgumentException e) {
      throw exceptionFactory()
          .create(
              String.format("Could not convert [%s] to java.sql.Type %s", str, targetSqlType),
              "HY000",
              e);
    }
    throw exceptionFactory()
        .create(String.format("Could not convert [%s] to %s", str, targetSqlType));
  }

  private void handleTimestampString(int parameterIndex, String str) throws SQLException {
    if (str.startsWith("0000-00-00")) {
      setTimestamp(parameterIndex, null);
    } else {
      setTimestamp(parameterIndex, Timestamp.valueOf(str));
    }
  }

  private boolean trySetNumber(int parameterIndex, Object obj, Integer targetSqlType)
      throws SQLException {
    if (!(obj instanceof Number)) return false;

    Number bd = (Number) obj;
    switch (targetSqlType) {
      case Types.TINYINT:
        setByte(parameterIndex, bd.byteValue());
        return true;
      case Types.SMALLINT:
        setShort(parameterIndex, bd.shortValue());
        return true;
      case Types.INTEGER:
        setInt(parameterIndex, bd.intValue());
        return true;
      case Types.BIGINT:
        setLong(parameterIndex, bd.longValue());
        return true;
      case Types.FLOAT:
      case Types.DOUBLE:
        setDouble(parameterIndex, bd.doubleValue());
        return true;
      case Types.REAL:
        setFloat(parameterIndex, bd.floatValue());
        return true;
      case Types.DECIMAL:
      case Types.NUMERIC:
        handleNumericType(parameterIndex, obj, bd);
        return true;
      case Types.BIT:
        setBoolean(parameterIndex, bd.shortValue() != 0);
        return true;
      case Types.CHAR:
      case Types.VARCHAR:
        setString(parameterIndex, bd.toString());
        return true;
    }
    throw exceptionFactory()
        .create(String.format("Could not convert [%s] to %s", bd, targetSqlType));
  }

  private void handleNumericType(int parameterIndex, Object obj, Number bd) throws SQLException {
    if (obj instanceof BigDecimal) {
      setBigDecimal(parameterIndex, (BigDecimal) obj);
    } else if (obj instanceof Double || obj instanceof Float) {
      setDouble(parameterIndex, bd.doubleValue());
    } else {
      setLong(parameterIndex, bd.longValue());
    }
  }

  private boolean trySetByteArray(
      int parameterIndex, Object obj, Integer targetSqlType, Long scaleOrLength)
      throws SQLException {
    if (!(obj instanceof byte[])) return false;

    byte[] bytes = (byte[]) obj;
    switch (targetSqlType) {
      case Types.BINARY:
      case Types.VARBINARY:
      case Types.LONGVARBINARY:
        if (scaleOrLength != null) {
          setBytes(parameterIndex, Arrays.copyOfRange(bytes, 0, scaleOrLength.intValue()));
        } else {
          setBytes(parameterIndex, bytes);
        }
        return true;
      case Types.BLOB:
        if (scaleOrLength != null) {
          setBlob(parameterIndex, new MariaDbBlob(bytes, 0, scaleOrLength.intValue()));
        } else {
          setBlob(parameterIndex, new MariaDbBlob(bytes));
        }
        return true;
      default:
        throw exceptionFactory()
            .create("Can only convert a byte[] to BINARY, VARBINARY, LONGVARBINARY or BLOB type");
    }
  }

  private void trySetWithCodec(int parameterIndex, Object obj, Long scaleOrLength)
      throws SQLException {
    for (Codec<?> codec : con.getContext().getConf().codecs()) {
      if (codec.canEncode(obj)) {
        Parameter p = new Parameter(codec, obj, scaleOrLength);
        parameters.set(parameterIndex - 1, p);
        return;
      }
    }

    throw new SQLException(String.format("Type %s not supported type", obj.getClass().getName()));
  }

  /**
   * Sets the designated parameter to the given input stream, which will have the specified number
   * of bytes. When a very large ASCII value is input to a <code>LONGVARCHAR</code> parameter, it
   * may be more practical to send it via a <code>java.io.InputStream</code>. Data will be read from
   * the stream as needed until end-of-file is reached. The JDBC driver will do any necessary
   * conversion from ASCII to the database char format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the Java input stream that contains the ASCII parameter value
   * @param length the number of bytes in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.6
   */
  @Override
  public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
  }

  /**
   * Sets the designated parameter to the given input stream, which will have the specified number
   * of bytes. When a very large binary value is input to a <code>LONGVARBINARY</code> parameter, it
   * may be more practical to send it via a <code>java.io.InputStream</code> object. The data will
   * be read from the stream as needed until end-of-file is reached.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the java input stream which contains the binary parameter value
   * @param length the number of bytes in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.6
   */
  @Override
  public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
  }

  /**
   * Sets the designated parameter to the given <code>Reader</code> object, which is the given
   * number of characters long. When a very large UNICODE value is input to a <code>LONGVARCHAR
   * </code> parameter, it may be more practical to send it via a <code>java.io.Reader</code>
   * object. The data will be read from the stream as needed until end-of-file is reached. The JDBC
   * driver will do any necessary conversion from UNICODE to the database char format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param reader the <code>java.io.Reader</code> object that contains the Unicode data
   * @param length the number of characters in the stream
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @since 1.6
   */
  @Override
  public void setCharacterStream(int parameterIndex, Reader reader, long length)
      throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader, length));
  }

  /**
   * Sets the designated parameter to the given input stream. When a very large ASCII value is input
   * to a <code>LONGVARCHAR</code> parameter, it may be more practical to send it via a <code>
   * java.io.InputStream</code>. Data will be read from the stream as needed until end-of-file is
   * reached. The JDBC driver will do any necessary conversion from ASCII to the database char
   * format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setAsciiStream</code> which takes a length parameter.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the Java input stream that contains the ASCII parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given input stream. When a very large binary value is
   * input to a <code>LONGVARBINARY</code> parameter, it may be more practical to send it via a
   * <code>java.io.InputStream</code> object. The data will be read from the stream as needed until
   * end-of-file is reached.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setBinaryStream</code> which takes a length parameter.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the java input stream which contains the binary parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
  }

  /**
   * Sets the designated parameter to the given <code>Reader</code> object. When a very large
   * UNICODE value is input to a <code>LONGVARCHAR</code> parameter, it may be more practical to
   * send it via a <code>java.io.Reader</code> object. The data will be read from the stream as
   * needed until end-of-file is reached. The JDBC driver will do any necessary conversion from
   * UNICODE to the database char format.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setCharacterStream</code> which takes a length parameter.
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param reader the <code>java.io.Reader</code> object that contains the Unicode data
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed <code>
   *     PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. The <code>Reader</code> reads
   * the data till end-of-file is reached. The driver does the necessary conversion from Java
   * character format to the national character set in the database.
   *
   * <p><B>Note:</B> This stream object can either be a standard Java stream object or your own
   * subclass that implements the standard interface.
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setNCharacterStream</code> which takes a length parameter.
   *
   * @param parameterIndex of the first parameter is 1, the second is 2, ...
   * @param value the parameter value
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the driver does not support national character sets; if the driver can detect
   *     that a data conversion error could occur; if a database access error occurs; or this method
   *     is called on a closed <code>PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, value));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. This method differs from the
   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
   * parameter value should be sent to the server as a <code>CLOB</code>. When the <code>
   * setCharacterStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGVARCHAR</code> or a
   * <code>CLOB</code>
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setClob</code> which takes a length parameter.
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param reader An object that contains the data to set the parameter value to.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code>or if parameterIndex does not correspond to a parameter marker in
   *     the SQL statement
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setClob(int parameterIndex, Reader reader) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
  }

  /**
   * Sets the designated parameter to a <code>InputStream</code> object. This method differs from
   * the <code>setBinaryStream (int, InputStream)</code> method because it informs the driver that
   * the parameter value should be sent to the server as a <code>BLOB</code>. When the <code>
   * setBinaryStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGVARBINARY</code> or a
   * <code>BLOB</code>
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setBlob</code> which takes a length parameter.
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param inputStream An object that contains the data to set the parameter value to.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs; this method is called on a closed <code>
   *     PreparedStatement</code> or if parameterIndex does not correspond to a parameter marker in
   *     the SQL statement,
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(StreamCodec.INSTANCE, inputStream));
  }

  /**
   * Sets the designated parameter to a <code>Reader</code> object. This method differs from the
   * <code>setCharacterStream (int, Reader)</code> method because it informs the driver that the
   * parameter value should be sent to the server as a <code>NCLOB</code>. When the <code>
   * setCharacterStream</code> method is used, the driver may have to do extra work to determine
   * whether the parameter data should be sent to the server as a <code>LONGNVARCHAR</code> or a
   * <code>NCLOB</code>
   *
   * <p><B>Note:</B> Consult your JDBC driver documentation to determine if it might be more
   * efficient to use a version of <code>setNClob</code> which takes a length parameter.
   *
   * @param parameterIndex index of the first parameter is 1, the second is 2, ...
   * @param reader An object that contains the data to set the parameter value to.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if the driver does not support national character sets; if the driver can detect
   *     that a data conversion error could occur; if a database access error occurs or this method
   *     is called on a closed <code>PreparedStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.6
   */
  @Override
  public void setNClob(int parameterIndex, Reader reader) throws SQLException {
    checkIndex(parameterIndex);
    parameters.set(parameterIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, reader));
  }

  /**
   * Sets the value of the designated parameter with the given object.
   *
   * <p>If the second argument is an {@code InputStream} then the stream must contain the number of
   * bytes specified by scaleOrLength. If the second argument is a {@code Reader} then the reader
   * must contain the number of characters specified by scaleOrLength. If these conditions are not
   * true the driver will generate a {@code SQLException} when the prepared statement is executed.
   *
   * <p>The given Java object will be converted to the given targetSqlType before being sent to the
   * database.
   *
   * <p>If the object has a custom mapping (is of a class implementing the interface {@code
   * SQLData}), the JDBC driver should call the method {@code SQLData.writeSQL} to write it to the
   * SQL data stream. If, on the other hand, the object is of a class implementing {@code Ref},
   * {@code Blob}, {@code Clob}, {@code NClob}, {@code Struct}, {@code java.net.URL}, or {@code
   * Array}, the driver should pass it to the database as a value of the corresponding SQL type.
   *
   * <p>Note that this method may be used to pass database-specific abstract data types.
   *
   * <p>The default implementation will throw {@code SQLFeatureNotSupportedException}
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the object containing the input parameter value
   * @param targetSqlType the SQL type to be sent to the database. The scale argument may further
   *     qualify this type.
   * @param scaleOrLength for {@code java.sql.JDBCType.DECIMAL} or {@code java.sql.JDBCType.NUMERIC
   *     types}, this is the number of digits after the decimal point. For Java Object types {@code
   *     InputStream} and {@code Reader}, this is the length of the data in the stream or reader.
   *     For all other types, this value will be ignored.
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed {@code
   *     PreparedStatement} or if the Java Object specified by x is an InputStream or Reader object
   *     and the value of the scale parameter is less than zero
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified
   *     targetSqlType
   * @see JDBCType
   * @see SQLType
   * @since 1.8
   */
  @Override
  public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength)
      throws SQLException {
    setInternalObject(
        parameterIndex,
        x,
        targetSqlType == null ? null : targetSqlType.getVendorTypeNumber(),
        (long) scaleOrLength);
  }

  /**
   * Sets the value of the designated parameter with the given object.
   *
   * <p>This method is similar to {@link #setObject(int parameterIndex, Object x, SQLType
   * targetSqlType, int scaleOrLength)}, except that it assumes a scale of zero.
   *
   * <p>The default implementation will throw {@code SQLFeatureNotSupportedException}
   *
   * @param parameterIndex the first parameter is 1, the second is 2, ...
   * @param x the object containing the input parameter value
   * @param targetSqlType the SQL type to be sent to the database
   * @throws SQLException if parameterIndex does not correspond to a parameter marker in the SQL
   *     statement; if a database access error occurs or this method is called on a closed {@code
   *     PreparedStatement}
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support the specified
   *     targetSqlType
   * @see JDBCType
   * @see SQLType
   * @since 1.8
   */
  @Override
  public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException {
    setInternalObject(
        parameterIndex,
        x,
        targetSqlType == null ? null : targetSqlType.getVendorTypeNumber(),
        null);
  }

  protected abstract BatchTypeState executeInternalPreparedBatch() throws SQLException;

  @Override
  @SuppressWarnings("try")
  public int[] executeBatch() throws SQLException {
    checkNotClosed();
    if (isBatchEmpty()) {
      return new int[0];
    }

    try (ClosableLock ignore = lock.closeableLock();
        QueryTimeoutHandler ignore2 = con.handleTimeout(queryTimeout)) {
      return executeBatchInternal();
    } catch (SQLException e) {
      handleExecutionError(e);
      throw e;
    } finally {
      cleanupResources();
    }
  }

  private boolean isBatchEmpty() {
    return batchParameters == null || batchParameters.isEmpty();
  }

  protected enum BatchTypeState {
    Normal,
    Bulk,
    Rewrite
  }

  private int[] executeBatchInternal() throws SQLException {
    BatchTypeState batchTypeState = executeInternalPreparedBatch();
    int[] updates = new int[batchParameters.size()];

    if (shouldHandleBulkUnitResults(batchTypeState == BatchTypeState.Bulk)) {
      return handleBulkUnitResults(updates);
    }

    if (shouldHandleBulkInsert(
        batchTypeState == BatchTypeState.Bulk || batchTypeState == BatchTypeState.Rewrite)) {
      int[] bulkInsertUpdates = handleBulkInsert(updates);
      if (bulkInsertUpdates != null) {
        return bulkInsertUpdates;
      }
    }

    return handleStandardResults(updates);
  }

  private boolean shouldHandleBulkUnitResults(boolean wasBulk) {
    return wasBulk && con.getContext().hasClientCapability(BULK_UNIT_RESULTS);
  }

  private int[] handleBulkUnitResults(int[] updates) throws SQLException {
    int updateIdx = 0;
    Arrays.fill(updates, Statement.SUCCESS_NO_INFO);

    for (Completion completion : results) {
      if (!(completion instanceof CompleteResult)) {
        continue;
      }

      CompleteResult completeResult = (CompleteResult) completion;
      if (!completeResult.isBulkResult()) {
        continue;
      }

      updateIdx = processBulkResult(updates, updateIdx, completeResult);
    }

    currResult = results.remove(0);
    return updates;
  }

  private long[] handleLongBulkUnitResults(long[] updates) throws SQLException {
    int updateIdx = 0;
    Arrays.fill(updates, Statement.SUCCESS_NO_INFO);

    for (Completion completion : results) {
      if (!(completion instanceof CompleteResult)) {
        continue;
      }

      CompleteResult completeResult = (CompleteResult) completion;
      if (!completeResult.isBulkResult()) {
        continue;
      }

      updateIdx = processLongBulkResult(updates, updateIdx, completeResult);
    }

    currResult = results.remove(0);
    return updates;
  }

  private int processBulkResult(int[] updates, int updateIdx, Result unitaryResults)
      throws SQLException {
    if (!unitaryResults.isBulkResult()) {
      return updateIdx;
    }

    unitaryResults.beforeFirst();
    while (unitaryResults.next()) {
      updates[updateIdx++] = unitaryResults.getInt(2);
    }
    return updateIdx;
  }

  private int processLongBulkResult(long[] updates, int updateIdx, Result unitaryResults)
      throws SQLException {
    if (!unitaryResults.isBulkResult()) {
      return updateIdx;
    }

    unitaryResults.beforeFirst();
    while (unitaryResults.next()) {
      updates[updateIdx++] = unitaryResults.getInt(2);
    }
    return updateIdx;
  }

  private boolean shouldHandleBulkInsert(boolean wasBulk) {
    return wasBulk && clientParser.isInsert() && !clientParser.isInsertDuplicate();
  }

  private int[] handleBulkInsert(int[] updates) {
    int totalAffectedRows = calculateTotalAffectedRows();

    if (totalAffectedRows == updates.length) {
      Arrays.fill(updates, 1);
      currResult = results.remove(0);
      return updates;
    }

    return null;
  }

  private long[] handleLongBulkInsert(long[] updates) {
    int totalAffectedRows = calculateTotalAffectedRows();

    if (totalAffectedRows == updates.length) {
      Arrays.fill(updates, 1);
      currResult = results.remove(0);
      return updates;
    }

    return null;
  }

  private int calculateTotalAffectedRows() {
    return results.stream().mapToInt(result -> (int) ((OkPacket) result).getAffectedRows()).sum();
  }

  private int[] handleStandardResults(int[] updates) {
    if (results.size() != updates.length) {
      Arrays.fill(updates, Statement.SUCCESS_NO_INFO);
    } else {
      processIndividualResults(updates);
    }

    currResult = results.remove(0);
    return updates;
  }

  private long[] handleStandardLongResults(long[] updates) {
    if (results.size() != updates.length) {
      Arrays.fill(updates, Statement.SUCCESS_NO_INFO);
    } else {
      processIndividualLongResults(updates);
    }

    currResult = results.remove(0);
    return updates;
  }

  private void processIndividualResults(int[] updates) {
    for (int i = 0; i < updates.length; i++) {
      updates[i] =
          results.get(i) instanceof OkPacket
              ? (int) ((OkPacket) results.get(i)).getAffectedRows()
              : Statement.SUCCESS_NO_INFO;
    }
  }

  private void processIndividualLongResults(long[] updates) {
    for (int i = 0; i < updates.length; i++) {
      updates[i] =
          results.get(i) instanceof OkPacket
              ? (int) ((OkPacket) results.get(i)).getAffectedRows()
              : Statement.SUCCESS_NO_INFO;
    }
  }

  private void handleExecutionError(SQLException e) {
    results = null;
    currResult = null;
  }

  private void cleanupResources() {
    localInfileInputStream = null;
    batchParameters.clear();
  }

  private long[] executeLongBatchInternal() throws SQLException {
    BatchTypeState batchTypeState = executeInternalPreparedBatch();
    long[] updates = new long[batchParameters.size()];

    if (shouldHandleBulkUnitResults(batchTypeState == BatchTypeState.Bulk)) {
      return handleLongBulkUnitResults(updates);
    }

    if (shouldHandleBulkInsert(
        batchTypeState == BatchTypeState.Bulk || batchTypeState == BatchTypeState.Rewrite)) {
      long[] bulkInsertUpdates = handleLongBulkInsert(updates);
      if (bulkInsertUpdates != null) {
        return bulkInsertUpdates;
      }
    }

    return handleStandardLongResults(updates);
  }

  @Override
  @SuppressWarnings("try")
  public long[] executeLargeBatch() throws SQLException {
    checkNotClosed();
    if (isBatchEmpty()) {
      return new long[0];
    }

    try (ClosableLock ignore = lock.closeableLock();
        QueryTimeoutHandler timeoutHandler = con.handleTimeout(queryTimeout)) {

      return executeLongBatchInternal();

    } catch (SQLException e) {
      handleExecutionError(e);
      throw e;
    } finally {
      cleanupResources();
    }
  }

  /**
   * Send COM_STMT_PREPARE + X * COM_STMT_BULK_EXECUTE, then read for the all answers
   *
   * @param cmd command
   * @throws SQLException if IOException / Command error
   */
  protected void executeBatchBulk(String cmd) throws SQLException {
    List<Completion> res;
    if (prepareResult == null && con.cachePrepStmts())
      prepareResult = con.getContext().getPrepareCacheCmd(cmd, this);
    try {
      if (prepareResult == null) {
        ClientMessage[] packets;
        packets =
            new ClientMessage[] {
              new PreparePacket(cmd), new BulkExecutePacket(null, batchParameters, cmd, this)
            };
        res =
            con.getClient()
                .executePipeline(
                    packets,
                    this,
                    0,
                    maxRows,
                    ResultSet.CONCUR_READ_ONLY,
                    ResultSet.TYPE_FORWARD_ONLY,
                    closeOnCompletion,
                    false);

        // in case of failover, prepare is done in failover, skipping prepare result
        if (res.get(0) instanceof PrepareResultPacket) {
          results = res.subList(1, res.size());
        } else {
          results = res;
        }
      } else {
        results =
            con.getClient()
                .execute(
                    new BulkExecutePacket(prepareResult, batchParameters, cmd, this),
                    this,
                    0,
                    maxRows,
                    ResultSet.CONCUR_READ_ONLY,
                    ResultSet.TYPE_FORWARD_ONLY,
                    closeOnCompletion,
                    false);
      }

    } catch (SQLException bue) {
      results = null;
      throw exceptionFactory()
          .createBatchUpdate(Collections.emptyList(), batchParameters.size(), bue);
    }
  }

  /**
   * reset prepare statement in case of a failover. (Command need then to be re-prepared on server)
   */
  public void reset() {
    lock.lock();
    try {
      prepareResult = null;
    } finally {
      lock.unlock();
    }
  }

  @Override
  public ResultSet getGeneratedKeys() throws SQLException {
    checkNotClosed();
    validateGeneratedKeysSupport();

    List<String[]> insertIds = extractInsertIds();

    if (!insertIds.isEmpty()) {
      return createGeneratedKeysResultSet(insertIds);
    }

    return super.getGeneratedKeys();
  }

  private void validateGeneratedKeysSupport() throws SQLException {
    if (autoGeneratedKeys != java.sql.Statement.RETURN_GENERATED_KEYS) {
      throw new SQLException(
          "Cannot return generated keys: query was not set with Statement.RETURN_GENERATED_KEYS");
    }
  }

  private List<String[]> extractInsertIds() throws SQLException {
    if (currResult == null) {
      return Collections.emptyList();
    }

    List<String[]> insertIds = new ArrayList<>(results.size() + 1); // results + currResult
    List<Completion> allResults = getAllResults();

    for (Completion completion : allResults) {
      if (isValidBulkResult(completion)) {
        processUnitaryResults((CompleteResult) completion, insertIds);
      }
    }

    return insertIds;
  }

  private List<Completion> getAllResults() {
    List<Completion> allResults = new ArrayList<>(results);
    allResults.add(currResult);
    return allResults;
  }

  private boolean isValidBulkResult(Completion completion) {
    return completion instanceof CompleteResult && ((CompleteResult) completion).isBulkResult();
  }

  private void processUnitaryResults(CompleteResult unitaryResults, List<String[]> insertIds)
      throws SQLException {
    unitaryResults.beforeFirst();
    while (unitaryResults.next()) {
      addAutoGeneratedIdIfPresent(unitaryResults, insertIds);
    }
  }

  private void addAutoGeneratedIdIfPresent(Result unitaryResults, List<String[]> insertIds)
      throws SQLException {
    int autoGeneratedId = unitaryResults.getInt(1);
    if (autoGeneratedId != 0) {
      insertIds.add(new String[] {String.valueOf(autoGeneratedId)});
    }
  }

  private ResultSet createGeneratedKeysResultSet(List<String[]> insertIds) {
    String[][] ids = insertIds.toArray(new String[0][]);
    return CompleteResult.createResultSet(
        "insert_id",
        DataType.BIGINT,
        ids,
        con.getContext(),
        ColumnFlags.AUTO_INCREMENT | ColumnFlags.UNSIGNED,
        resultSetType);
  }
}
