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

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

import java.io.InputStream;
import java.sql.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.export.ExceptionFactory;
import org.mariadb.jdbc.message.client.QueryPacket;
import org.mariadb.jdbc.message.server.OkPacket;
import org.mariadb.jdbc.util.ClientParser;
import org.mariadb.jdbc.util.NativeSql;
import org.mariadb.jdbc.util.constants.ColumnFlags;
import org.mariadb.jdbc.util.constants.ServerStatus;

/** Statement implementation */
public class Statement implements java.sql.Statement {
  private static final Pattern identifierPattern =
      Pattern.compile("[0-9a-zA-Z$_\\u0080-\\uFFFF]*", Pattern.UNICODE_CASE | Pattern.CANON_EQ);
  private static final Pattern escapePattern = Pattern.compile("[\u0000'\"\b\n\r\t\u001A\\\\]");
  private static final Map<String, String> mapper = new HashMap<>();

  static {
    mapper.put("\u0000", "\\0");
    mapper.put("'", "\\\\'");
    mapper.put("\"", "\\\\\"");
    mapper.put("\b", "\\\\b");
    mapper.put("\n", "\\\\n");
    mapper.put("\r", "\\\\r");
    mapper.put("\t", "\\\\t");
    mapper.put("\u001A", "\\\\Z");
    mapper.put("\\", "\\\\");
  }

  /** result-set type */
  protected final int resultSetType;

  /** concurrency */
  protected final int resultSetConcurrency;

  /** thread safe locker */
  protected final ReentrantLock lock;

  /** can use server query timeout */
  protected final boolean canUseServerTimeout;

  /** can use server row limitation */
  protected final boolean canUseServerMaxRows;

  /** connection */
  protected final Connection con;

  protected Boolean isInsertDuplicate = null;

  /** required query timeout */
  protected int queryTimeout;

  /** maximum row number */
  protected long maxRows;

  protected String lastSql;

  /** fetch size */
  protected int fetchSize;

  /** automatic generated keys result required */
  protected int autoGeneratedKeys;

  /** close statement on resultset completion */
  protected boolean closeOnCompletion;

  /** closed flag */
  protected boolean closed;

  /** escape processing */
  protected boolean escape;

  /** last execution results */
  protected List<Completion> results;

  /** current results */
  protected Completion currResult;

  /** streaming load data infile data */
  protected InputStream localInfileInputStream;

  private List<String> batchQueries;

  /**
   * Constructor
   *
   * @param con connection
   * @param lock thread safe locker
   * @param canUseServerTimeout can use server timeout
   * @param canUseServerMaxRows can use server row number limitation
   * @param autoGeneratedKeys automatic generated keys result required
   * @param resultSetType result-set type
   * @param resultSetConcurrency concurrency
   * @param defaultFetchSize fetch size
   */
  public Statement(
      Connection con,
      ReentrantLock lock,
      boolean canUseServerTimeout,
      boolean canUseServerMaxRows,
      int autoGeneratedKeys,
      int resultSetType,
      int resultSetConcurrency,
      int defaultFetchSize) {
    this.con = con;
    this.lock = lock;
    this.resultSetConcurrency = resultSetConcurrency;
    this.resultSetType = resultSetType;
    this.autoGeneratedKeys = autoGeneratedKeys;
    this.canUseServerTimeout = canUseServerTimeout;
    this.canUseServerMaxRows = canUseServerMaxRows;
    this.fetchSize = defaultFetchSize;
  }

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

  /**
   * Set current local infile stream
   *
   * @param inputStream stream
   * @throws SQLException if statement is already closed
   */
  public void setLocalInfileInputStream(InputStream inputStream) throws SQLException {
    checkNotClosed();
    localInfileInputStream = inputStream;
  }

  /**
   * Executes the given SQL statement, which returns a single <code>ResultSet</code> object.
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql an SQL statement to be sent to the database, typically a static SQL <code>SELECT
   *     </code> statement
   * @return a <code>ResultSet</code> object that contains the data produced by the given query;
   *     never <code>null</code>
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the given SQL statement produces anything other than a single
   *     <code>ResultSet</code> object, the method is called on a <code>PreparedStatement</code> or
   *     <code>CallableStatement</code>
   * @throws java.sql.SQLTimeoutException when the driver has determined that the timeout value that
   *     was specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   */
  @Override
  public ResultSet executeQuery(String sql) throws SQLException {
    executeInternal(sql, Statement.NO_GENERATED_KEYS);
    currResult = results.remove(0);
    if (currResult instanceof Result) return (Result) currResult;
    return new CompleteResult(new ColumnDecoder[0], new byte[0][], con.getContext(), resultSetType);
  }

  /**
   * Executes the given SQL statement, which may be an <code>INSERT</code>, <code>UPDATE</code>, or
   * <code>DELETE</code> statement or an SQL statement that returns nothing, such as an SQL DDL
   * statement.
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
   *     <code>UPDATE</code> or <code>DELETE</code>; or an SQL statement that returns nothing, such
   *     as a DDL statement.
   * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0
   *     for SQL statements that return nothing
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the given SQL statement produces a <code>ResultSet</code> object,
   *     the method is called on a <code>PreparedStatement</code> or <code>CallableStatement</code>
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   */
  @Override
  public int executeUpdate(String sql) throws SQLException {
    return executeUpdate(sql, Statement.NO_GENERATED_KEYS);
  }

  /**
   * Releases this <code>Statement</code> object's database and JDBC resources immediately instead
   * of waiting for this to happen when it is automatically closed. It is generally good practice to
   * release resources as soon as you are finished with them to avoid tying up database resources.
   *
   * <p>Calling the method <code>close</code> on a <code>Statement</code> object that is already
   * closed has no effect.
   *
   * <p><B>Note:</B>When a <code>Statement</code> object is closed, its current <code>ResultSet
   * </code> object, if one exists, is also closed.
   *
   * @throws SQLException if a database access error occurs
   */
  @Override
  public void close() throws SQLException {
    if (!closed) {
      closed = true;

      if (currResult != null && currResult instanceof Result) {
        ((Result) currResult).closeFromStmtClose(lock);
      }

      // close result-set
      if (results != null && !results.isEmpty()) {
        for (Completion completion : results) {
          if (completion instanceof Result) {
            ((Result) completion).closeFromStmtClose(lock);
          }
        }
      }
    }
  }

  /**
   * Abort current command result if streaming. result-set will be incomplete and closed, but ensure
   * connection state
   */
  public void abort() {
    lock.lock();
    try {
      if (!closed) {
        closed = true;

        if (currResult != null && currResult instanceof Result) {
          ((Result) currResult).abort();
        }

        // close result-set
        if (results != null) {
          for (Completion completion : results) {
            if (completion instanceof Result) {
              ((Result) completion).abort();
            }
          }
        }
      }
    } finally {
      lock.unlock();
    }
  }

  /**
   * Retrieves the maximum number of bytes that can be returned for character and binary column
   * values in a <code>ResultSet</code> object produced by this <code>Statement</code> object. This
   * limit applies only to <code>BINARY</code>, <code>VARBINARY</code>, <code>LONGVARBINARY</code>,
   * <code>CHAR</code>, <code>VARCHAR</code>, <code>NCHAR</code>, <code>NVARCHAR</code>, <code>
   * LONGNVARCHAR</code> and <code>LONGVARCHAR</code> columns. If the limit is exceeded, the excess
   * data is silently discarded.
   *
   * @return the current column size limit for columns storing character and binary values; zero
   *     means there is no limit
   * @see #setMaxFieldSize
   */
  @Override
  public int getMaxFieldSize() {
    return 0;
  }

  /**
   * NOT SUPPORTED.
   *
   * @see #getMaxFieldSize
   */
  @Override
  public void setMaxFieldSize(int max) {}

  /**
   * Retrieves the maximum number of rows that a <code>ResultSet</code> object produced by this
   * <code>Statement</code> object can contain. If this limit is exceeded, the excess rows are
   * silently dropped.
   *
   * @return the current maximum number of rows for a <code>ResultSet</code> object produced by this
   *     <code>Statement</code> object; zero means there is no limit
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #setMaxRows
   */
  @Override
  public int getMaxRows() throws SQLException {
    checkNotClosed();
    return (int) maxRows;
  }

  /**
   * Sets the limit for the maximum number of rows that any <code>ResultSet</code> object generated
   * by this <code>Statement</code> object can contain to the given number. If the limit is
   * exceeded, the excess rows are silently dropped.
   *
   * @param max the new max rows limit; zero means there is no limit
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the condition {@code max >= 0} is not satisfied
   * @see #getMaxRows
   */
  @Override
  public void setMaxRows(int max) throws SQLException {
    checkNotClosed();
    if (max < 0) {
      throw exceptionFactory().create("max rows cannot be negative : asked for " + max, "42000");
    }
    maxRows = max;
  }

  /**
   * Sets escape processing on or off. If escape scanning is on (the default), the driver will do
   * escape substitution before sending the SQL statement to the database.
   *
   * <p>The {@code Connection} and {@code DataSource} property {@code escapeProcessing} may be used
   * to change the default escape processing behavior. A value of true (the default) enables escape
   * Processing for all {@code Statement} objects. A value of false disables escape processing for
   * all {@code Statement} objects. The {@code setEscapeProcessing} method may be used to specify
   * the escape processing behavior for an individual {@code Statement} object.
   *
   * <p>Note: Since prepared statements have usually been parsed prior to making this call,
   * disabling escape processing for <code>PreparedStatements</code> objects will have no effect.
   *
   * @param enable <code>true</code> to enable escape processing; <code>false</code> to disable it
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   */
  @Override
  public void setEscapeProcessing(boolean enable) throws SQLException {
    checkNotClosed();
    this.escape = enable;
  }

  /**
   * Retrieves the number of seconds the driver will wait for a <code>Statement</code> object to
   * execute. If the limit is exceeded, a <code>SQLException</code> is thrown.
   *
   * @return the current query timeout limit in seconds; zero means there is no limit
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #setQueryTimeout
   */
  @Override
  public int getQueryTimeout() throws SQLException {
    checkNotClosed();
    return queryTimeout;
  }

  /**
   * Sets the number of seconds the driver will wait for a <code>Statement</code> object to execute
   * to the given number of seconds. By default, there is no limit on the amount of time allowed for
   * a running statement to complete. If the limit is exceeded, an <code>SQLTimeoutException</code>
   * is thrown. A JDBC driver must apply this limit to the <code>execute</code>, <code>executeQuery
   * </code> and <code>executeUpdate</code> methods.
   *
   * <p><strong>Note:</strong> JDBC driver implementations may also apply this limit to {@code
   * ResultSet} methods (consult your driver vendor documentation for details).
   *
   * <p><strong>Note:</strong> In the case of {@code Statement} batching, it is implementation
   * defined whether the time-out is applied to individual SQL commands added via the {@code
   * addBatch} method or to the entire batch of SQL commands invoked by the {@code executeBatch}
   * method (consult your driver vendor documentation for details).
   *
   * @param seconds the new query timeout limit in seconds; zero means there is no limit
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the condition {@code seconds >= 0} is not satisfied
   * @see #getQueryTimeout
   */
  @Override
  public void setQueryTimeout(int seconds) throws SQLException {
    if (seconds < 0) {
      throw exceptionFactory()
          .create("Query timeout cannot be negative : asked for " + seconds, "42000");
    }
    this.queryTimeout = seconds;
  }

  /**
   * Cancels this <code>Statement</code> object if both the DBMS and driver support aborting an SQL
   * statement. This method can be used by one thread to cancel a statement that is being executed
   * by another thread.
   *
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   */
  @Override
  public void cancel() throws SQLException {
    checkNotClosed();
    boolean locked = lock.tryLock();
    // if any query is active, lock is set.
    // this avoids trying to execute a KILL QUERY if no query is running.
    if (!locked) {
      con.cancelCurrentQuery();
    } else {
      lock.unlock();
    }
  }

  /**
   * Retrieves the first warning reported by calls on this <code>Statement</code> object. Subsequent
   * <code>Statement</code> object warnings will be chained to this <code>SQLWarning</code> object.
   *
   * <p>The warning chain is automatically cleared each time a statement is (re)executed. This
   * method may not be called on a closed <code>Statement</code> object; doing so will cause an
   * <code>SQLException</code> to be thrown.
   *
   * <p><B>Note:</B> If you are processing a <code>ResultSet</code> object, any warnings associated
   * with reads on that <code>ResultSet</code> object will be chained on it rather than on the
   * <code>Statement</code> object that produced it.
   *
   * @return the first <code>SQLWarning</code> object or <code>null</code> if there are no warnings
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   */
  @Override
  public SQLWarning getWarnings() throws SQLException {
    return con.getWarnings();
  }

  /**
   * Clears all the warnings reported on this <code>Statement</code> object. After a call to this
   * method, the method <code>getWarnings</code> will return <code>null</code> until a new warning
   * is reported for this <code>Statement</code> object.
   */
  @Override
  public void clearWarnings() {
    con.getContext().setWarning(0);
  }

  /**
   * Sets the SQL cursor name to the given <code>String</code>, which will be used by subsequent
   * <code>Statement</code> object <code>execute</code> methods. This name can then be used in SQL
   * positioned update or delete statements to identify the current row in the <code>ResultSet
   * </code> object generated by this statement. If the database does not support positioned
   * update/delete, this method is a noop. To ensure that a cursor has the proper isolation level to
   * support updates, the cursor's <code>SELECT</code> statement should have the form <code>
   * SELECT FOR UPDATE</code>. If <code>FOR UPDATE</code> is not present, positioned updates may
   * fail.
   *
   * <p><B>Note:</B> By definition, the execution of positioned updates and deletes must be done by
   * a different <code>Statement</code> object than the one that generated the <code>ResultSet
   * </code> object being used for positioning. Also, cursor names must be unique within a
   * connection.
   *
   * @param name the new cursor name, which must be unique within a connection
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   */
  @Override
  public void setCursorName(String name) throws SQLException {
    throw exceptionFactory().notSupported("Cursors are not supported");
  }

  /**
   * Executes the given SQL statement, which may return multiple results. In some (uncommon)
   * situations, a single SQL statement may return multiple result sets and/or update counts.
   * Normally you can ignore this unless you are (1) executing a stored procedure that you know may
   * return multiple results or (2) you are dynamically executing an unknown SQL string.
   *
   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
   * result(s).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql any SQL statement
   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no results
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the method is called on a <code>PreparedStatement</code> or <code>
   *     CallableStatement</code>
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @see #getResultSet
   * @see #getUpdateCount
   * @see #getMoreResults
   */
  @Override
  public boolean execute(String sql) throws SQLException {
    return execute(sql, Statement.NO_GENERATED_KEYS);
  }

  /**
   * Retrieves the current result as a <code>ResultSet</code> object. This method should be called
   * only once per result.
   *
   * @return the current result as a <code>ResultSet</code> object or <code>null</code> if the
   *     result is an update count or there are no more results
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #execute
   */
  @Override
  public ResultSet getResultSet() throws SQLException {
    checkNotClosed();
    if (currResult instanceof Result) {
      return (Result) currResult;
    }
    return null;
  }

  /**
   * Retrieves the current result as an update count; if the result is a <code>ResultSet</code>
   * object or there are no more results, -1 is returned. This method should be called only once per
   * result.
   *
   * @return the current result as an update count; -1 if the current result is a <code>ResultSet
   *     </code> object or there are no more results
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #execute
   */
  @Override
  public int getUpdateCount() throws SQLException {
    checkNotClosed();
    if (currResult instanceof OkPacket) {
      return (int) ((OkPacket) currResult).getAffectedRows();
    }
    return -1;
  }

  /**
   * Moves to this <code>Statement</code> object's next result, returns <code>true</code> if it is a
   * <code>ResultSet</code> object, and implicitly closes any current <code>ResultSet</code>
   * object(s) obtained with the method <code>getResultSet</code>.
   *
   * <p>There are no more results when the following is true:
   *
   * <PRE>{@code
   * // stmt is a Statement object
   * ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1))
   * }</PRE>
   *
   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no more results
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #execute
   */
  @Override
  public boolean getMoreResults() throws SQLException {
    return getMoreResults(Statement.CLOSE_CURRENT_RESULT);
  }

  /**
   * Retrieves the direction for fetching rows from database tables that is the default for result
   * sets generated from this <code>Statement</code> object. If this <code>Statement</code> object
   * has not set a fetch direction by calling the method <code>setFetchDirection</code>, the return
   * value is implementation-specific.
   *
   * @return the default fetch direction for result sets generated from this <code>Statement</code>
   *     object
   * @see #setFetchDirection
   * @since 1.2
   */
  @Override
  public int getFetchDirection() {
    return ResultSet.FETCH_FORWARD;
  }

  /**
   * Gives the driver a hint as to the direction in which rows will be processed in <code>ResultSet
   * </code> objects created using this <code>Statement</code> object. The default value is <code>
   * ResultSet.FETCH_FORWARD</code>.
   *
   * <p>Note that this method sets the default fetch direction for result sets generated by this
   * <code>Statement</code> object. Each result set has its own methods for getting and setting its
   * own fetch direction.
   *
   * @param direction the initial direction for processing rows
   */
  @Override
  public void setFetchDirection(int direction) {
    // not supported
  }

  /**
   * Retrieves the number of result set rows that is the default fetch size for <code>ResultSet
   * </code> objects generated from this <code>Statement</code> object. If this <code>Statement
   * </code> object has not set a fetch size by calling the method <code>setFetchSize</code>, the
   * return value is implementation-specific.
   *
   * @return the default fetch size for result sets generated from this <code>Statement</code>
   *     object
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @see #setFetchSize
   */
  @Override
  public int getFetchSize() throws SQLException {
    checkNotClosed();
    return this.fetchSize;
  }

  /**
   * Gives the JDBC driver a hint as to the number of rows that should be fetched from the database
   * when more rows are needed for <code>ResultSet</code> objects generated by this <code>Statement
   * </code>. If the value specified is zero, then the hint is ignored. The default value is zero.
   *
   * @param rows the number of rows to fetch
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the condition {@code rows >= 0} is not satisfied.
   * @see #getFetchSize
   * @since 1.2
   */
  @Override
  public void setFetchSize(int rows) throws SQLException {
    if (rows < 0) {
      throw exceptionFactory().create("invalid fetch size");
    }
    this.fetchSize = rows;
  }

  /**
   * Retrieves the result set concurrency for <code>ResultSet</code> objects generated by this
   * <code>Statement</code> object.
   *
   * @return either <code>ResultSet.CONCUR_READ_ONLY</code> or <code>ResultSet.CONCUR_UPDATABLE
   *     </code>
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   */
  @Override
  public int getResultSetConcurrency() throws SQLException {
    checkNotClosed();
    return this.resultSetConcurrency;
  }

  /**
   * Retrieves the result set type for <code>ResultSet</code> objects generated by this <code>
   * Statement</code> object.
   *
   * @return one of <code>ResultSet.TYPE_FORWARD_ONLY</code>, <code>
   *     ResultSet.TYPE_SCROLL_INSENSITIVE</code>, or <code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
   * @since 1.2
   */
  @Override
  public int getResultSetType() {
    return this.resultSetType;
  }

  /**
   * Adds the given SQL command to the current list of commands for this <code>Statement</code>
   * object. The commands in this list can be executed as a batch by calling the method <code>
   * executeBatch</code>.
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql typically this is an SQL <code>INSERT</code> or <code>UPDATE</code> statement
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the driver does not support batch updates, the method is called on
   *     a <code>PreparedStatement</code> or <code>CallableStatement</code>
   * @see #executeBatch
   * @see DatabaseMetaData#supportsBatchUpdates
   */
  @Override
  public void addBatch(String sql) throws SQLException {
    if (sql == null) {
      throw exceptionFactory().create("null cannot be set to addBatch(String sql)");
    }
    if (batchQueries == null) batchQueries = new ArrayList<>();
    batchQueries.add(escape ? NativeSql.parse(sql, con.getContext()) : sql);
  }

  /**
   * Empties this <code>Statement</code> object's current list of SQL commands.
   *
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the driver does not support batch updates
   * @see #addBatch
   * @see DatabaseMetaData#supportsBatchUpdates
   * @since 1.2
   */
  @Override
  public void clearBatch() throws SQLException {
    checkNotClosed();
    if (batchQueries == null) {
      batchQueries = new ArrayList<>();
    } else {
      batchQueries.clear();
    }
  }

  /**
   * Submits a batch of commands to the database for execution and if all commands execute
   * successfully, returns an array of update counts. The <code>int</code> elements of the array
   * that is returned are ordered to correspond to the commands in the batch, which are ordered
   * according to the order in which they were added to the batch. The elements in the array
   * returned by the method <code>executeBatch</code> may be one of the following:
   *
   * <OL>
   *   <LI>A number greater than or equal to zero -- indicates that the command was processed
   *       successfully and is an update count giving the number of rows in the database that were
   *       affected by the command's execution
   *   <LI>A value of <code>SUCCESS_NO_INFO</code> -- indicates that the command was processed
   *       successfully but that the number of rows affected is unknown
   *       <p>If one of the commands in a batch update fails to execute properly, this method throws
   *       a <code>BatchUpdateException</code>, and a JDBC driver may or may not continue to process
   *       the remaining commands in the batch. However, the driver's behavior must be consistent
   *       with a particular DBMS, either always continuing to process commands or never continuing
   *       to process commands. If the driver continues processing after a failure, the array
   *       returned by the method <code>BatchUpdateException.getUpdateCounts</code> will contain as
   *       many elements as there are commands in the batch, and at least one of the elements will
   *       be the following:
   *   <LI>A value of <code>EXECUTE_FAILED</code> -- indicates that the command failed to execute
   *       successfully and occurs only if a driver continues to process commands after a command
   *       fails
   * </OL>
   *
   * <p>The possible implementations and return values have been modified in the Java 2 SDK,
   * Standard Edition, version 1.3 to accommodate the option of continuing to process commands in a
   * batch update after a <code>BatchUpdateException</code> object has been thrown.
   *
   * @return an array of update counts containing one element for each command in the batch. The
   *     elements of the array are ordered according to the order in which commands were added to
   *     the batch.
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the driver does not support batch statements. Throws {@link
   *     BatchUpdateException} (a subclass of <code>SQLException</code>) if one of the commands sent
   *     to the database fails to execute properly or attempts to return a result set.
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @see #addBatch
   * @see DatabaseMetaData#supportsBatchUpdates
   * @since 1.2
   */
  @Override
  public int[] executeBatch() throws SQLException {
    checkNotClosed();
    if (batchQueries == null || batchQueries.isEmpty()) return new int[0];
    lock.lock();
    try {
      this.lastSql = batchQueries.get(0);
      this.autoGeneratedKeys = java.sql.Statement.RETURN_GENERATED_KEYS;
      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
      if (possibleLoadLocal) {
        possibleLoadLocal = false;
        for (String batchQuery : batchQueries) {
          String sql = batchQuery.toUpperCase(Locale.ROOT);
          if (sql.contains(" LOCAL ") && sql.contains("LOAD") && sql.contains(" INFILE")) {
            possibleLoadLocal = true;
            break;
          }
        }
      }

      List<Completion> res =
          possibleLoadLocal ? executeInternalBatchStandard() : executeInternalBatchPipeline();

      results = res;

      int[] updates = new int[res.size()];
      for (int i = 0; i < res.size(); i++) {
        if (res.get(i) instanceof OkPacket) {
          updates[i] = (int) ((OkPacket) res.get(i)).getAffectedRows();
        } else {
          updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO;
        }
      }
      currResult = results.remove(0);
      return updates;

    } catch (SQLException e) {
      results = null;
      currResult = null;
      throw e;
    } finally {
      batchQueries.clear();
      lock.unlock();
    }
  }

  /**
   * Retrieves the <code>Connection</code> object that produced this <code>Statement</code> object.
   *
   * @return the connection that produced this statement
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @since 1.2
   */
  @Override
  public Connection getConnection() throws SQLException {
    checkNotClosed();
    return con;
  }

  /**
   * Moves to this <code>Statement</code> object's next result, deals with any current <code>
   * ResultSet</code> object(s) according to the instructions specified by the given flag, and
   * returns <code>true</code> if the next result is a <code>ResultSet</code> object.
   *
   * <p>There are no more results when the following is true:
   *
   * <PRE>{@code
   * // stmt is a Statement object
   * ((stmt.getMoreResults(current) == false) && (stmt.getUpdateCount() == -1))
   * }</PRE>
   *
   * @param current one of the following <code>Statement</code> constants indicating what should
   *     happen to current <code>ResultSet</code> objects obtained using the method <code>
   *     getResultSet</code>: <code>Statement.CLOSE_CURRENT_RESULT</code>, <code>
   *     Statement.KEEP_CURRENT_RESULT</code>, or <code>Statement.CLOSE_ALL_RESULTS</code>
   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no more results
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code> or the argument supplied is not one of the following: <code>
   *     Statement.CLOSE_CURRENT_RESULT</code>, <code>Statement.KEEP_CURRENT_RESULT</code> or <code>
   *     Statement.CLOSE_ALL_RESULTS</code>
   * @throws SQLFeatureNotSupportedException if <code>DatabaseMetaData.supportsMultipleOpenResults
   *     </code> returns <code>false</code> and either <code>Statement.KEEP_CURRENT_RESULT</code> or
   *     <code>Statement.CLOSE_ALL_RESULTS</code> are supplied as the argument.
   * @see #execute
   * @since 1.4
   */
  @Override
  public boolean getMoreResults(int current) throws SQLException {
    checkNotClosed();
    if (currResult instanceof ResultSet) {
      lock.lock();
      try {
        Result result = (Result) currResult;
        if (current == java.sql.Statement.CLOSE_CURRENT_RESULT) {
          result.close();
        } else {
          result.fetchRemaining();
        }
        if (result.streaming()
            && (con.getContext().getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) {
          con.getClient()
              .readStreamingResults(
                  results,
                  fetchSize,
                  maxRows,
                  resultSetConcurrency,
                  resultSetType,
                  closeOnCompletion);
        }
      } finally {
        lock.unlock();
      }
    }

    if (results.size() > 0) {
      currResult = results.remove(0);
      return (currResult instanceof Result);
    }
    currResult = null;
    return false;
  }

  /**
   * Permit to streaming result to fetch remaining results.
   *
   * @throws SQLException if socket error occurs.
   */
  public void fetchRemaining() throws SQLException {
    if (currResult != null && currResult instanceof ResultSet) {
      Result result = (Result) currResult;
      result.fetchRemaining();
      if (result.streaming()
          && (con.getContext().getServerStatus() & ServerStatus.MORE_RESULTS_EXISTS) > 0) {
        con.getClient()
            .readStreamingResults(
                results, 0, 0L, resultSetConcurrency, resultSetType, closeOnCompletion);
      }
    }
  }

  /**
   * Retrieves any auto-generated keys created as a result of executing this <code>Statement</code>
   * object. If this <code>Statement</code> object did not generate any keys, an empty <code>
   * ResultSet</code> object is returned.
   *
   * <p><B>Note:</B>If the columns which represent the auto-generated keys were not specified, the
   * JDBC driver implementation will determine the columns which best represent the auto-generated
   * keys.
   *
   * @return a <code>ResultSet</code> object containing the auto-generated key(s) generated by the
   *     execution of this <code>Statement</code> object
   * @throws SQLException if a database access error occurs or this method is called on a closed
   *     <code>Statement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @since 1.4
   */
  @Override
  public ResultSet getGeneratedKeys() throws SQLException {
    checkNotClosed();
    if (autoGeneratedKeys != java.sql.Statement.RETURN_GENERATED_KEYS) {
      throw new SQLException(
          "Cannot return generated keys: query was not set with Statement.RETURN_GENERATED_KEYS");
    }

    List<String[]> insertIds = new ArrayList<>();
    if (con.getContext().getConf().returnMultiValuesGeneratedIds()
        && !checkIfInsertDuplicateCommand(getLastSql())) {
      // For compatibility with 2.x connector
      long autoIncrement = con.getContext().getAutoIncrement();
      if (currResult instanceof OkPacket) {
        OkPacket ok = ((OkPacket) currResult);
        if (ok.getLastInsertId() != 0) {
          insertIds.add(new String[] {String.valueOf(ok.getLastInsertId())});
          if (ok.getAffectedRows() > 1) {
            for (int i = 1; i < ok.getAffectedRows(); i++) {
              insertIds.add(
                  new String[] {String.valueOf(ok.getLastInsertId() + i * autoIncrement)});
            }
          }
        }
      }
      if (results != null) {
        for (Completion result : results) {
          if (result instanceof OkPacket) {
            OkPacket ok = ((OkPacket) result);
            if (ok.getLastInsertId() != 0) {
              insertIds.add(new String[] {String.valueOf(ok.getLastInsertId())});
              if (ok.getAffectedRows() > 1) {
                for (int i = 1; i < ok.getAffectedRows(); i++) {
                  insertIds.add(
                      new String[] {String.valueOf(ok.getLastInsertId() + i * autoIncrement)});
                }
              }
            }
          }
        }
      }
    } else {
      // standard behavior
      if (currResult instanceof OkPacket && ((OkPacket) currResult).getLastInsertId() != 0) {
        insertIds.add(new String[] {String.valueOf(((OkPacket) currResult).getLastInsertId())});
      }
      if (results != null) {
        for (Completion result : results) {
          if (result instanceof OkPacket && ((OkPacket) result).getLastInsertId() != 0) {
            insertIds.add(new String[] {String.valueOf(((OkPacket) result).getLastInsertId())});
          }
        }
      }
    }

    if (insertIds.isEmpty()) {
      return new CompleteResult(
          new ColumnDecoder[0], new byte[0][], con.getContext(), resultSetType);
    }

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

  protected boolean checkIfInsertDuplicateCommand(String sql) {
    if (isInsertDuplicate == null) {
      if (sql == null) {
        isInsertDuplicate = false;
      } else {
        ClientParser parser =
            ClientParser.parameterParts(
                sql, (con.getContext().getServerStatus() & ServerStatus.NO_BACKSLASH_ESCAPES) > 0);
        isInsertDuplicate = parser.isInsertDuplicate();
      }
    }
    return isInsertDuplicate;
  }

  /**
   * Executes the given SQL statement and signals the driver with the given flag about whether the
   * auto-generated keys produced by this <code>Statement</code> object should be made available for
   * retrieval. The driver will ignore the flag if the SQL statement is not an <code>INSERT</code>
   * statement, or an SQL statement able to return auto-generated keys (the list of such statements
   * is vendor-specific).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
   *     <code>UPDATE</code> or <code>DELETE</code>; or an SQL statement that returns nothing, such
   *     as a DDL statement.
   * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available
   *     for retrieval; one of the following constants: <code>Statement.RETURN_GENERATED_KEYS</code>
   *     <code>Statement.NO_GENERATED_KEYS</code>
   * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0
   *     for SQL statements that return nothing
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the given SQL statement returns a <code>ResultSet</code> object,
   *     the given constant is not one of those allowed, the method is called on a <code>
   *     PreparedStatement</code> or <code>CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method with a
   *     constant of Statement.RETURN_GENERATED_KEYS
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @since 1.4
   */
  @Override
  public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
    executeInternal(sql, autoGeneratedKeys);

    currResult = results.remove(0);
    if (currResult instanceof Result) {
      throw exceptionFactory()
          .create("the given SQL statement produces an unexpected ResultSet object", "HY000");
    }
    return (int) ((OkPacket) currResult).getAffectedRows();
  }

  private void executeInternal(String sql, int autoGeneratedKeys) throws SQLException {
    checkNotClosed();
    lock.lock();
    try {
      this.lastSql = sql;
      this.autoGeneratedKeys = autoGeneratedKeys;
      String cmd = escapeTimeout(sql);
      results =
          con.getClient()
              .execute(
                  new QueryPacket(cmd, localInfileInputStream),
                  this,
                  fetchSize,
                  maxRows,
                  resultSetConcurrency,
                  resultSetType,
                  closeOnCompletion,
                  false);
    } catch (SQLException e) {
      results = null;
      currResult = null;
      throw e;
    } finally {
      localInfileInputStream = null;
      lock.unlock();
    }
  }

  /**
   * Executes the given SQL statement and signals the driver that the auto-generated keys indicated
   * in the given array should be made available for retrieval. This array contains the indexes of
   * the columns in the target table that contain the auto-generated keys that should be made
   * available. The driver will ignore the array if the SQL statement is not an <code>INSERT</code>
   * statement, or an SQL statement able to return auto-generated keys (the list of such statements
   * is vendor-specific).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
   *     <code>UPDATE</code> or <code>DELETE</code>; or an SQL statement that returns nothing, such
   *     as a DDL statement.
   * @param columnIndexes an array of column indexes indicating the columns that should be returned
   *     from the inserted row
   * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) 0
   *     for SQL statements that return nothing
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the SQL statement returns a <code>ResultSet</code> object,the
   *     second argument supplied to this method is not an <code>int</code> array whose elements are
   *     valid column indexes, the method is called on a <code>PreparedStatement</code> or <code>
   *     CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @since 1.4
   */
  @Override
  public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
    return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Executes the given SQL statement and signals the driver that the auto-generated keys indicated
   * in the given array should be made available for retrieval. This array contains the names of the
   * columns in the target table that contain the auto-generated keys that should be made available.
   * The driver will ignore the array if the SQL statement is not an <code>INSERT</code> statement,
   * or an SQL statement able to return auto-generated keys (the list of such statements is
   * vendor-specific).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>,
   *     <code>UPDATE</code> or <code>DELETE</code>; or an SQL statement that returns nothing, such
   *     as a DDL statement.
   * @param columnNames an array of the names of the columns that should be returned from the
   *     inserted row
   * @return either the row count for <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE
   *     </code> statements, or 0 for SQL statements that return nothing
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the SQL statement returns a <code>ResultSet</code> object, the
   *     second argument supplied to this method is not a <code>String</code> array whose elements
   *     are valid column names, the method is called on a <code>PreparedStatement</code> or <code>
   *     CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @since 1.4
   */
  @Override
  public int executeUpdate(String sql, String[] columnNames) throws SQLException {
    return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Executes the given SQL statement, which may return multiple results, and signals the driver
   * that any auto-generated keys should be made available for retrieval. The driver will ignore
   * this signal if the SQL statement is not an <code>INSERT</code> statement, or an SQL statement
   * able to return auto-generated keys (the list of such statements is vendor-specific).
   *
   * <p>In some (uncommon) situations, a single SQL statement may return multiple result sets and/or
   * update counts. Normally you can ignore this unless you are (1) executing a stored procedure
   * that you know may return multiple results or (2) you are dynamically executing an unknown SQL
   * string.
   *
   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
   * result(s).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql any SQL statement
   * @param autoGeneratedKeys a constant indicating whether auto-generated keys should be made
   *     available for retrieval using the method <code>getGeneratedKeys</code>; one of the
   *     following constants: <code>Statement.RETURN_GENERATED_KEYS</code> or <code>
   *     Statement.NO_GENERATED_KEYS</code>
   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no results
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the second parameter supplied to this method is not <code>
   *     Statement.RETURN_GENERATED_KEYS</code> or <code>Statement.NO_GENERATED_KEYS</code>, the
   *     method is called on a <code>PreparedStatement</code> or <code>CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method with a
   *     constant of Statement.RETURN_GENERATED_KEYS
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @see #getResultSet
   * @see #getUpdateCount
   * @see #getMoreResults
   * @see #getGeneratedKeys
   * @since 1.4
   */
  @Override
  public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
    executeInternal(sql, autoGeneratedKeys);

    currResult = results.remove(0);
    return currResult instanceof Result;
  }

  /**
   * Build sql command to execute :
   *
   * <ul>
   *   <li>Execute escape substitution if needed
   *   <li>add query timeout prefix if server permits it
   *   <li>add max row limit prefix if server permits it
   * </ul>
   *
   * @param sql sql command
   * @return sql command to execute
   * @throws SQLException if fails to escape sql
   */
  protected String escapeTimeout(final String sql) throws SQLException {
    String escapedSql = escape ? NativeSql.parse(sql, con.getContext()) : sql;
    if (queryTimeout != 0 && canUseServerTimeout) {
      if (canUseServerMaxRows && maxRows > 0) {
        return "SET STATEMENT max_statement_time="
            + queryTimeout
            + ", SQL_SELECT_LIMIT="
            + maxRows
            + " FOR "
            + escapedSql;
      }
      return "SET STATEMENT max_statement_time=" + queryTimeout + " FOR " + escapedSql;
    }
    if (canUseServerMaxRows && maxRows > 0) {
      return "SET STATEMENT SQL_SELECT_LIMIT=" + maxRows + " FOR " + escapedSql;
    }
    return escapedSql;
  }

  /**
   * Executes the given SQL statement, which may return multiple results, and signals the driver
   * that the auto-generated keys indicated in the given array should be made available for
   * retrieval. This array contains the indexes of the columns in the target table that contain the
   * auto-generated keys that should be made available. The driver will ignore the array if the SQL
   * statement is not an <code>INSERT</code> statement, or an SQL statement able to return
   * auto-generated keys (the list of such statements is vendor-specific).
   *
   * <p>Under some (uncommon) situations, a single SQL statement may return multiple result sets
   * and/or update counts. Normally you can ignore this unless you are (1) executing a stored
   * procedure that you know may return multiple results or (2) you are dynamically executing an
   * unknown SQL string.
   *
   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
   * result(s).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql any SQL statement
   * @param columnIndexes an array of the indexes of the columns in the inserted row that should be
   *     made available for retrieval by a call to the method <code>getGeneratedKeys</code>
   * @return <code>true</code> if the first result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no results
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>, the elements in the <code>int</code> array passed to this method
   *     are not valid column indexes, the method is called on a <code>PreparedStatement</code> or
   *     <code>CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @see #getResultSet
   * @see #getUpdateCount
   * @see #getMoreResults
   * @since 1.4
   */
  @Override
  public boolean execute(String sql, int[] columnIndexes) throws SQLException {
    return execute(sql, Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Executes the given SQL statement, which may return multiple results, and signals the driver
   * that the auto-generated keys indicated in the given array should be made available for
   * retrieval. This array contains the names of the columns in the target table that contain the
   * auto-generated keys that should be made available. The driver will ignore the array if the SQL
   * statement is not an <code>INSERT</code> statement, or an SQL statement able to return
   * auto-generated keys (the list of such statements is vendor-specific).
   *
   * <p>In some (uncommon) situations, a single SQL statement may return multiple result sets and/or
   * update counts. Normally you can ignore this unless you are (1) executing a stored procedure
   * that you know may return multiple results or (2) you are dynamically executing an unknown SQL
   * string.
   *
   * <p>The <code>execute</code> method executes an SQL statement and indicates the form of the
   * first result. You must then use the methods <code>getResultSet</code> or <code>getUpdateCount
   * </code> to retrieve the result, and <code>getMoreResults</code> to move to any subsequent
   * result(s).
   *
   * <p><strong>Note:</strong>This method cannot be called on a <code>PreparedStatement</code> or
   * <code>CallableStatement</code>.
   *
   * @param sql any SQL statement
   * @param columnNames an array of the names of the columns in the inserted row that should be made
   *     available for retrieval by a call to the method <code>getGeneratedKeys</code>
   * @return <code>true</code> if the next result is a <code>ResultSet</code> object; <code>false
   *     </code> if it is an update count or there are no more results
   * @throws SQLException if a database access error occurs, this method is called on a closed
   *     <code>Statement</code>,the elements of the <code>String</code> array passed to this method
   *     are not valid column names, the method is called on a <code>PreparedStatement</code> or
   *     <code>CallableStatement</code>
   * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method
   * @throws SQLTimeoutException when the driver has determined that the timeout value that was
   *     specified by the {@code setQueryTimeout} method has been exceeded and has at least
   *     attempted to cancel the currently running {@code Statement}
   * @see #getResultSet
   * @see #getUpdateCount
   * @see #getMoreResults
   * @see #getGeneratedKeys
   * @since 1.4
   */
  @Override
  public boolean execute(String sql, String[] columnNames) throws SQLException {
    return execute(sql, Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Retrieves the result set holdability for <code>ResultSet</code> objects generated by this
   * <code>Statement</code> object.
   *
   * @return either <code>ResultSet.HOLD_CURSORS_OVER_COMMIT</code> or <code>
   *     ResultSet.CLOSE_CURSORS_AT_COMMIT</code>
   * @since 1.4
   */
  @Override
  public int getResultSetHoldability() {
    return ResultSet.HOLD_CURSORS_OVER_COMMIT;
  }

  /**
   * Retrieves whether this <code>Statement</code> object has been closed. A <code>Statement</code>
   * is closed if the method close has been called on it, or if it is automatically closed.
   *
   * @return true if this <code>Statement</code> object is closed; false if it is still open
   */
  @Override
  public boolean isClosed() {
    return closed;
  }

  /**
   * Returns a value indicating whether the <code>Statement</code> is poolable or not.
   *
   * @return <code>true</code> if the <code>Statement</code> is poolable; <code>false</code>
   *     otherwise
   * @throws SQLException if this method is called on a closed <code>Statement</code>
   * @see java.sql.Statement#setPoolable(boolean) setPoolable(boolean)
   */
  @Override
  public boolean isPoolable() throws SQLException {
    checkNotClosed();
    return false;
  }

  /**
   * Requests that a <code>Statement</code> be pooled or not pooled. The value specified is a hint
   * to the statement pool implementation indicating whether the application wants the statement to
   * be pooled. It is up to the statement pool manager whether the hint is used.
   *
   * <p>The poolable value of a statement is applicable to both internal statement caches
   * implemented by the driver and external statement caches implemented by application servers and
   * other applications.
   *
   * <p>By default, a <code>Statement</code> is not poolable when created, and a <code>
   * PreparedStatement</code> and <code>CallableStatement</code> are poolable when created.
   *
   * @param poolable requests that the statement be pooled if true and that the statement not be
   *     pooled if false
   * @throws SQLException if this method is called on a closed <code>Statement</code>
   */
  @Override
  public void setPoolable(boolean poolable) throws SQLException {
    checkNotClosed();
  }

  /**
   * Specifies that this {@code Statement} will be closed when all its dependent result sets are
   * closed. If execution of the {@code Statement} does not produce any result sets, this method has
   * no effect.
   *
   * <p><strong>Note:</strong> Multiple calls to {@code closeOnCompletion} do not toggle the effect
   * on this {@code Statement}. However, a call to {@code closeOnCompletion} does affect both the
   * subsequent execution of statements, and statements that currently have open, dependent, result
   * sets.
   *
   * @throws SQLException if this method is called on a closed {@code Statement}
   */
  @Override
  public void closeOnCompletion() throws SQLException {
    checkNotClosed();
    this.closeOnCompletion = true;

    // set closeOnCompletion on last result-set
    if (results != null && results.size() > 0) {
      for (int i = results.size(); i > 0; i--) {
        Completion completion = results.get(i - 1);
        if (completion instanceof Result) {
          ((Result) completion).closeOnCompletion();
          return;
        }
      }
    } else {
      if (currResult != null && currResult instanceof ResultSet) {
        Result res = (Result) currResult;
        if (res.streaming() || res.loaded()) {
          res.closeOnCompletion();
        }
      }
    }
  }

  /**
   * Returns a value indicating whether this {@code Statement} will be closed when all its dependent
   * result sets are closed.
   *
   * @return {@code true} if the {@code Statement} will be closed when all of its dependent result
   *     sets are closed; {@code false} otherwise
   * @throws SQLException if this method is called on a closed {@code Statement}
   */
  @Override
  public boolean isCloseOnCompletion() throws SQLException {
    checkNotClosed();
    return closeOnCompletion;
  }

  /**
   * Returns an object that implements the given interface to allow access to non-standard methods,
   * or standard methods not exposed by the proxy.
   *
   * <p>If the receiver implements the interface then the result is the receiver or a proxy for the
   * receiver. If the receiver is a wrapper and the wrapped object implements the interface then the
   * result is the wrapped object or a proxy for the wrapped object. Otherwise, return the result of
   * calling <code>unwrap</code> recursively on the wrapped object or a proxy for that result. If
   * the receiver is not a wrapper and does not implement the interface, then an <code>SQLException
   * </code> is thrown.
   *
   * @param iface A Class defining an interface that the result must implement.
   * @return an object that implements the interface. Maybe a proxy for the actual implementing
   *     object.
   * @throws SQLException If no object found that implements the interface
   */
  @Override
  @SuppressWarnings("unchecked")
  public <T> T unwrap(Class<T> iface) throws SQLException {
    if (isWrapperFor(iface)) {
      return (T) this;
    }
    throw exceptionFactory()
        .create("The receiver is not a wrapper and does not implement the interface", "42000");
  }

  /**
   * Returns true if this either implements the interface argument or is directly or indirectly a
   * wrapper for an object that does. Returns false otherwise. If this implements the interface then
   * return true, else if this is a wrapper then return the result of recursively calling <code>
   * isWrapperFor</code> on the wrapped object. If this does not implement the interface and is not
   * a wrapper, return false. This method should be implemented as a low-cost operation compared to
   * <code>unwrap</code> so that callers can use this method to avoid expensive <code>unwrap</code>
   * calls that may fail. If this method returns true then calling <code>unwrap</code> with the same
   * argument should succeed.
   *
   * @param iface a Class defining an interface.
   * @return true if this implements the interface or directly or indirectly wraps an object that
   *     does.
   */
  @Override
  public boolean isWrapperFor(Class<?> iface) {
    if (iface == null) return false;
    return iface.isInstance(this);
  }

  /**
   * Check if statement is closed, and throw exception if so.
   *
   * @throws SQLException if statement close
   */
  protected void checkNotClosed() throws SQLException {
    if (closed) {
      throw exceptionFactory().create("Cannot do an operation on a closed statement");
    }
  }

  /**
   * Executes the given SQL statement, which may be an INSERT, UPDATE, or DELETE statement or an SQL
   * statement that returns nothing, such as an SQL DDL statement. This method should be used when
   * the returned row count may exceed Integer.MAX_VALUE.
   *
   * @param sql sql command
   * @return update counts
   * @throws SQLException if any error occur during execution
   */
  @Override
  public long executeLargeUpdate(String sql) throws SQLException {
    return executeLargeUpdate(sql, Statement.NO_GENERATED_KEYS);
  }

  /**
   * Identical to executeLargeUpdate(String sql), with a flag that indicate that autoGeneratedKeys
   * (primary key fields with "auto_increment") generated id's must be retrieved.
   *
   * <p>Those id's will be available using getGeneratedKeys() method.
   *
   * @param sql sql command
   * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be made available
   *     for retrieval; one of the following constants: Statement.RETURN_GENERATED_KEYS
   *     Statement.NO_GENERATED_KEYS
   * @return update counts
   * @throws SQLException if any error occur during execution
   */
  @Override
  public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
    executeInternal(sql, autoGeneratedKeys);
    currResult = results.remove(0);
    if (currResult instanceof Result) {
      throw exceptionFactory()
          .create("the given SQL statement produces an unexpected ResultSet object", "HY000");
    }
    return ((OkPacket) currResult).getAffectedRows();
  }

  /**
   * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys =
   * Statement.RETURN_GENERATED_KEYS set.
   *
   * @param sql sql command
   * @param columnIndexes column Indexes
   * @return update counts
   * @throws SQLException if any error occur during execution
   */
  @Override
  public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
    return executeLargeUpdate(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Identical to executeLargeUpdate(String sql, int autoGeneratedKeys) with autoGeneratedKeys =
   * Statement.RETURN_GENERATED_KEYS set.
   *
   * @param sql sql command
   * @param columnNames columns names
   * @return update counts
   * @throws SQLException if any error occur during execution
   */
  @Override
  public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
    return executeLargeUpdate(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
  }

  /**
   * Retrieves the maximum number of rows that a ResultSet object produced by this Statement object
   * can contain. If this limit is exceeded, the excess rows are silently dropped.
   *
   * @throws SQLException if this method is called on a closed Statement
   * @return the current maximum number of rows for a ResultSet object produced by this Statement
   *     object; zero means there is no limit
   */
  @Override
  public long getLargeMaxRows() throws SQLException {
    checkNotClosed();
    return maxRows;
  }

  /**
   * Sets the limit for the maximum number of rows that any ResultSet object generated by this
   * Statement object can contain to the given number. If the limit is exceeded, the excess rows are
   * silently dropped.
   *
   * @param max the new max rows limit; zero means there is no limit
   * @throws SQLException if the condition max &gt;= 0 is not satisfied
   */
  @Override
  public void setLargeMaxRows(long max) throws SQLException {
    checkNotClosed();
    if (max < 0) {
      throw exceptionFactory().create("max rows cannot be negative : asked for " + max, "42000");
    }
    maxRows = max;
  }

  /**
   * Retrieves the current result as an update count; if the result is a ResultSet object or there
   * are no more results, -1 is returned.
   *
   * @throws SQLException if this method is called on a closed Statement
   * @return last update count
   */
  @Override
  public long getLargeUpdateCount() throws SQLException {
    checkNotClosed();
    if (currResult instanceof OkPacket) {
      return (int) ((OkPacket) currResult).getAffectedRows();
    }
    return -1;
  }

  /**
   * Execute batch, like executeBatch(), with returning results with long[]. For when row count may
   * exceed Integer.MAX_VALUE.
   *
   * @return an array of update counts (one element for each command in the batch)
   * @throws SQLException if a database error occur.
   */
  @Override
  public long[] executeLargeBatch() throws SQLException {
    checkNotClosed();
    if (batchQueries == null || batchQueries.isEmpty()) return new long[0];
    lock.lock();
    try {
      this.lastSql = batchQueries.get(0);
      this.autoGeneratedKeys = java.sql.Statement.RETURN_GENERATED_KEYS;
      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
      if (possibleLoadLocal) {
        possibleLoadLocal = false;
        for (String batchQuery : batchQueries) {
          String sql = batchQuery.toUpperCase(Locale.ROOT);
          if (sql.contains(" LOCAL ") && sql.contains("LOAD") && sql.contains(" INFILE")) {
            possibleLoadLocal = true;
            break;
          }
        }
      }

      List<Completion> res =
          possibleLoadLocal ? executeInternalBatchStandard() : executeInternalBatchPipeline();

      results = res;
      long[] updates = new long[res.size()];
      for (int i = 0; i < res.size(); i++) {
        updates[i] = ((OkPacket) res.get(i)).getAffectedRows();
      }
      currResult = results.remove(0);
      return updates;

    } catch (SQLException e) {
      results = null;
      currResult = null;
      throw e;
    } finally {
      batchQueries.clear();
      lock.unlock();
    }
  }

  /**
   * Execute batch pipelining commands (sending all client message, then reading results) (batches
   * cannot contain results-set, so cannot fill receiving socket buffer while sending buffer is
   * full)
   *
   * @return results
   * @throws SQLException if any error occurs
   */
  public List<Completion> executeInternalBatchPipeline() throws SQLException {
    QueryPacket[] packets = new QueryPacket[batchQueries.size()];
    for (int i = 0; i < batchQueries.size(); i++) {
      String sql = batchQueries.get(i);
      packets[i] = new QueryPacket(sql);
    }
    return con.getClient()
        .executePipeline(
            packets,
            this,
            0,
            0L,
            ResultSet.CONCUR_READ_ONLY,
            ResultSet.TYPE_FORWARD_ONLY,
            closeOnCompletion,
            false);
  }

  /**
   * basic implementation Send batch query per query.
   *
   * @return results
   * @throws SQLException if any error occurs
   */
  public List<Completion> executeInternalBatchStandard() throws SQLException {
    List<Completion> results = new ArrayList<>();
    try {
      for (String batchQuery : batchQueries) {
        results.addAll(
            con.getClient()
                .execute(
                    new QueryPacket(batchQuery, localInfileInputStream),
                    this,
                    0,
                    0L,
                    ResultSet.CONCUR_READ_ONLY,
                    ResultSet.TYPE_FORWARD_ONLY,
                    closeOnCompletion,
                    false));
      }
      return results;
    } catch (SQLException sqle) {
      int[] updateCounts = new int[batchQueries.size()];
      for (int i = 0; i < Math.min(results.size(), updateCounts.length); i++) {
        Completion completion = results.get(i);
        updateCounts[i] =
            completion instanceof OkPacket ? (int) ((OkPacket) completion).getAffectedRows() : 0;
      }
      throw new BatchUpdateException(
          sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle);
    } finally {
      localInfileInputStream = null;
    }
  }

  /**
   * Enquote String value.
   *
   * @param val string value to enquote
   * @return enquoted string value
   */
  // @Override when not supporting java 8
  public String enquoteLiteral(String val) {
    Matcher matcher = escapePattern.matcher(val);
    StringBuffer escapedVal = new StringBuffer("'");

    while (matcher.find()) {
      matcher.appendReplacement(escapedVal, mapper.get(matcher.group()));
    }
    matcher.appendTail(escapedVal);
    escapedVal.append("'");
    return escapedVal.toString();
  }

  /**
   * Escaped identifier according to MariaDB requirement.
   *
   * @param identifier identifier
   * @param alwaysQuote indicate if identifier must be enquoted even if not necessary.
   * @return return escaped identifier, quoted when necessary or indicated with alwaysQuote.
   * @see <a href="https://mariadb.com/kb/en/library/identifier-names/">mariadb identifier name</a>
   * @throws SQLException if containing u0000 character
   */
  // @Override when not supporting java 8
  public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException {
    if (isSimpleIdentifier(identifier)) {
      return alwaysQuote ? "`" + identifier + "`" : identifier;
    } else {
      if (identifier.contains("\u0000")) {
        throw exceptionFactory().create("Invalid name - containing u0000 character", "42000");
      }

      if (identifier.matches("^`.+`$")) {
        identifier = identifier.substring(1, identifier.length() - 1);
      }
      return "`" + identifier.replace("`", "``") + "`";
    }
  }

  /**
   * Retrieves whether identifier is a simple SQL identifier. The first character is an alphabetic
   * character from a through z, or from A through Z The string only contains alphanumeric
   * characters or the characters "_" and "$"
   *
   * @param identifier identifier
   * @return true if identifier doesn't have to be quoted
   * @see <a href="https://mariadb.com/kb/en/library/identifier-names/">mariadb identifier name</a>
   */
  // @Override when not supporting java 8
  public boolean isSimpleIdentifier(String identifier) {
    return identifier != null
        && !identifier.isEmpty()
        && identifierPattern.matcher(identifier).matches();
  }

  public String getLastSql() {
    return lastSql;
  }

  /**
   * Enquote utf8 value.
   *
   * @param val value to enquote
   * @return enquoted String value
   */
  // @Override when not supporting java 8
  public String enquoteNCharLiteral(String val) {
    return "N'" + val.replace("'", "''") + "'";
  }
}
