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

org.mariadb.jdbc.ServerPreparedStatement Maven / Gradle / Ivy

There is a newer version: 3.5.0
Show newest version
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2023 MariaDB Corporation Ab

package org.mariadb.jdbc;

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

import java.sql.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import org.mariadb.jdbc.client.ColumnDecoder;
import org.mariadb.jdbc.client.Completion;
import org.mariadb.jdbc.client.result.CompleteResult;
import org.mariadb.jdbc.client.result.Result;
import org.mariadb.jdbc.client.util.Parameters;
import org.mariadb.jdbc.export.ExceptionFactory;
import org.mariadb.jdbc.message.ClientMessage;
import org.mariadb.jdbc.message.client.BulkExecutePacket;
import org.mariadb.jdbc.message.client.ExecutePacket;
import org.mariadb.jdbc.message.client.PrepareExecutePacket;
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.util.ParameterList;

/**
 * Server prepare statement. command will generate COM_STMT_PREPARE + COM_STMT_EXECUTE (+
 * COM_STMT_CLOSE)
 */
public class ServerPreparedStatement extends BasePreparedStatement {
  private static final Pattern PREPARABLE_STATEMENT_PATTERN =
      Pattern.compile("^(SELECT|UPDATE|INSERT|DELETE|REPLACE|DO|CALL)", Pattern.CASE_INSENSITIVE);
  private final boolean canCachePrepStmts;

  /**
   * Server prepare statement constructor
   *
   * @param sql command
   * @param con connection
   * @param lock thread safe lock
   * @param canUseServerTimeout can server use timeout
   * @param canUseServerMaxRows can server use max rows
   * @param canCachePrepStmts can server cache prepared statement
   * @param autoGeneratedKeys must command return automatically generated keys
   * @param resultSetType resultset type
   * @param resultSetConcurrency resultset concurrency
   * @param defaultFetchSize default fetch size
   * @throws SQLException if prepare fails
   */
  public ServerPreparedStatement(
      String sql,
      Connection con,
      ReentrantLock lock,
      boolean canUseServerTimeout,
      boolean canUseServerMaxRows,
      boolean canCachePrepStmts,
      int autoGeneratedKeys,
      int resultSetType,
      int resultSetConcurrency,
      int defaultFetchSize)
      throws SQLException {
    super(
        sql,
        con,
        lock,
        canUseServerTimeout,
        canUseServerMaxRows,
        autoGeneratedKeys,
        resultSetType,
        resultSetConcurrency,
        defaultFetchSize);
    this.canCachePrepStmts = canCachePrepStmts;
    prepareResult = canCachePrepStmts ? con.getContext().getPrepareCache().get(sql, this) : null;
    if (prepareResult == null && !PREPARABLE_STATEMENT_PATTERN.matcher(sql).find()) {
      con.getClient().execute(new PreparePacket(sql), this, true);
    }
    parameters = new ParameterList();
  }

  /**
   * Execute command with parameters
   *
   * @throws SQLException if any error occurs
   */
  protected void executeInternal() throws SQLException {
    checkNotClosed();
    validParameters();
    lock.lock();
    String cmd = escapeTimeout(sql);
    if (prepareResult == null)
      if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
    try {
      if (prepareResult == null && con.getContext().permitPipeline()) {
        executePipeline(cmd);
      } else {
        executeStandard(cmd);
      }
    } finally {
      localInfileInputStream = null;
      lock.unlock();
    }
  }

  /**
   * Send COM_STMT_PREPARE + COM_STMT_EXECUTE, then read for the 2 answers
   *
   * @param cmd command
   * @throws SQLException if IOException / Command error
   */
  private void executePipeline(String cmd) throws SQLException {
    // server is 10.2+, permitting to execute last prepare with (-1) statement id.
    // Server send prepare, followed by execute, in one exchange.
    try {
      List res =
          con.getClient()
              .execute(
                  new PrepareExecutePacket(cmd, parameters, this, localInfileInputStream),
                  this,
                  fetchSize,
                  maxRows,
                  resultSetConcurrency,
                  resultSetType,
                  closeOnCompletion,
                  false);
      results = res.subList(1, res.size());
    } catch (SQLException ex) {
      results = null;
      throw ex;
    }
  }

  private void executeStandard(String cmd) throws SQLException {
    // send COM_STMT_PREPARE
    if (prepareResult == null) {
      if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
      if (prepareResult == null) {
        con.getClient().execute(new PreparePacket(cmd), this, true);
      }
    }
    validParameters();
    // send COM_STMT_EXECUTE
    ExecutePacket execute =
        new ExecutePacket(prepareResult, parameters, cmd, this, localInfileInputStream);
    results =
        con.getClient()
            .execute(
                execute,
                this,
                fetchSize,
                maxRows,
                resultSetConcurrency,
                resultSetType,
                closeOnCompletion,
                false);
  }

  private boolean executeInternalPreparedBatch() throws SQLException {
    checkNotClosed();
    String cmd = escapeTimeout(sql);
    if (batchParameters.size() > 1 && con.getContext().hasServerCapability(STMT_BULK_OPERATIONS)) {

      // ensure pipelining is possible (no LOAD DATA/XML INFILE commands)
      boolean possibleLoadLocal = con.getContext().hasClientCapability(LOCAL_FILES);
      if (possibleLoadLocal) {
        String sqlUpper = sql.toUpperCase(Locale.ROOT);
        possibleLoadLocal =
            sqlUpper.contains(" LOCAL ")
                && sqlUpper.contains("LOAD")
                && sqlUpper.contains(" INFILE");
      }

      if (!possibleLoadLocal) {
        Configuration conf = con.getContext().getConf();
        checkIfInsertCommand();
        if (((isCommandInsert && (conf.useBulkStmts() || conf.useBulkStmtsForInserts()))
                || (!isCommandInsert && conf.useBulkStmts()))
            && autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS) {
          executeBatchBulk(cmd);
          return isCommandInsert;
        } else {
          executeBatchPipeline(cmd);
          return false;
        }
      }
    }
    executeBatchStandard(cmd);
    return false;
  }

  /**
   * Send COM_STMT_PREPARE + X * COM_STMT_BULK_EXECUTE, then read for the all answers
   *
   * @param cmd command
   * @throws SQLException if IOException / Command error
   */
  private void executeBatchBulk(String cmd) throws SQLException {
    List res;
    if (prepareResult == null && canCachePrepStmts)
      prepareResult = con.getContext().getPrepareCache().get(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);
    }
  }

  /**
   * Send COM_STMT_PREPARE + X * COM_STMT_EXECUTE, then read for the all answers
   *
   * @param cmd command
   * @throws SQLException if Command error
   */
  private void executeBatchPipeline(String cmd) throws SQLException {
    if (prepareResult == null && canCachePrepStmts)
      prepareResult = con.getContext().getPrepareCache().get(cmd, this);
    // server is 10.2+, permitting to execute last prepare with (-1) statement id.
    // Server send prepare, followed by execute, in one exchange.
    int maxCmd = 250;
    List res = new ArrayList<>();
    try {
      int index = 0;
      if (prepareResult == null) {
        res.addAll(executeBunchPrepare(cmd, index, maxCmd));
        index += maxCmd;
      }
      while (index < batchParameters.size()) {
        res.addAll(executeBunch(cmd, index, maxCmd));
        index += maxCmd;
      }
      results = res;

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

  private List executeBunch(String cmd, int index, int maxCmd) throws SQLException {
    int maxCmdToSend = Math.min(batchParameters.size() - index, maxCmd);
    ClientMessage[] packets = new ClientMessage[maxCmdToSend];
    for (int i = index; i < index + maxCmdToSend; i++) {
      packets[i - index] =
          new ExecutePacket(
              prepareResult, batchParameters.get(i), cmd, this, localInfileInputStream);
    }
    return con.getClient()
        .executePipeline(
            packets,
            this,
            0,
            maxRows,
            ResultSet.CONCUR_READ_ONLY,
            ResultSet.TYPE_FORWARD_ONLY,
            closeOnCompletion,
            false);
  }

  private List executeBunchPrepare(String cmd, int index, int maxCmd)
      throws SQLException {
    int maxCmdToSend = Math.min(batchParameters.size() - index, maxCmd);
    ClientMessage[] packets = new ClientMessage[maxCmdToSend + 1];
    packets[0] = new PreparePacket(cmd);
    for (int i = index; i < index + maxCmdToSend; i++) {
      packets[i + 1 - index] =
          new ExecutePacket(null, batchParameters.get(i), cmd, this, localInfileInputStream);
    }
    List 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) {
      return res.subList(1, res.size());
    } else {
      return res;
    }
  }

  /**
   * Send COM_STMT_PREPARE + read answer, then Send a COM_STMT_EXECUTE + read answer * n time
   *
   * @param cmd command
   * @throws SQLException if IOException / Command error
   */
  private void executeBatchStandard(String cmd) throws SQLException {
    // send COM_STMT_PREPARE
    List tmpResults = new ArrayList<>();
    SQLException error = null;
    for (Parameters batchParameter : batchParameters) {
      // prepare is in loop, because if connection fail, prepare is reset, and need to be re
      // prepared
      if (prepareResult == null) {
        if (canCachePrepStmts) prepareResult = con.getContext().getPrepareCache().get(cmd, this);
        if (prepareResult == null) {
          con.getClient().execute(new PreparePacket(cmd), this, false);
        }
      }
      try {
        ExecutePacket execute =
            new ExecutePacket(prepareResult, batchParameter, cmd, this, localInfileInputStream);
        tmpResults.addAll(con.getClient().execute(execute, this, false));
      } catch (SQLException e) {
        if (error == null) error = e;
      }
    }

    if (error != null) {
      throw exceptionFactory().createBatchUpdate(tmpResults, batchParameters.size(), error);
    }
    this.results = tmpResults;
  }

  /**
   * Executes the SQL statement in this PreparedStatement object, which may be any kind
   * of SQL statement. Some prepared statements return multiple results; the execute
   * method handles these complex statements as well as the simpler form of statements handled by
   * the methods executeQuery and executeUpdate.
   *
   * 

The execute method returns a boolean to indicate the form of the * first result. You must call either the method getResultSet or getUpdateCount * to retrieve the result; you must call getMoreResults to move to any * subsequent result(s). * * @return true if the first result is a ResultSet object; false * if the first result is an update count or there is no result * @throws SQLException if a database access error occurs; this method is called on a closed * PreparedStatement or an argument is supplied to 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 Statement#execute * @see Statement#getResultSet * @see Statement#getUpdateCount * @see Statement#getMoreResults */ @Override public boolean execute() throws SQLException { executeInternal(); handleParameterOutput(); if (results.size() > 0) { currResult = results.remove(0); return currResult instanceof Result; } return false; } @Override public void setMaxRows(int max) throws SQLException { super.setMaxRows(max); if (canUseServerMaxRows && prepareResult != null) { prepareResult.decrementUse(con.getClient(), this); prepareResult = null; } } @Override public void setLargeMaxRows(long max) throws SQLException { super.setLargeMaxRows(max); if (canUseServerMaxRows && prepareResult != null) { prepareResult.decrementUse(con.getClient(), this); prepareResult = null; } } @Override public void setQueryTimeout(int seconds) throws SQLException { super.setQueryTimeout(seconds); if (canUseServerTimeout && prepareResult != null) { prepareResult.decrementUse(con.getClient(), this); prepareResult = null; } } /** * Executes the SQL query in this PreparedStatement object and returns the * ResultSet object generated by the query. * * @return a ResultSet object that contains the data produced by the query; never * null * @throws SQLException if a database access error occurs; this method is called on a closed * PreparedStatement or the SQL statement does not return a ResultSet * object * @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 ResultSet executeQuery() throws SQLException { executeInternal(); handleParameterOutput(); if (results.size() > 0) { currResult = results.remove(0); if (currResult instanceof Result) return (Result) currResult; } return new CompleteResult(new ColumnDecoder[0], new byte[0][], con.getContext()); } /** * Executes the SQL statement in this PreparedStatement object, which must be an SQL * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or * DELETE; 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 * PreparedStatement or the SQL statement returns a ResultSet object * @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() throws SQLException { return (int) executeLargeUpdate(); } /** * Executes the SQL statement in this PreparedStatement object, which must be an SQL * Data Manipulation Language (DML) statement, such as INSERT, UPDATE or * DELETE; or an SQL statement that returns nothing, such as a DDL statement. * *

This method should be used when the returned row count may exceed {@link Integer#MAX_VALUE}. * *

The default implementation will throw {@code UnsupportedOperationException} * * @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 * PreparedStatement or the SQL statement returns a ResultSet object * @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.8 */ @Override public long executeLargeUpdate() throws SQLException { executeInternal(); handleParameterOutput(); 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(); } /** * Handle output parameter result-set (only for CallableStatement) * * @throws SQLException if any error occurs */ protected void handleParameterOutput() throws SQLException {} /** * Adds a set of parameters to this PreparedStatement object's batch of commands. * * @throws SQLException if a database access error occurs or this method is called on a closed * PreparedStatement * @see Statement#addBatch * @since 1.2 */ @Override public void addBatch() throws SQLException { validParameters(); if (batchParameters == null) batchParameters = new ArrayList<>(); batchParameters.add(parameters); parameters = parameters.clone(); } /** * Validated that all parameters have been set. * * @throws SQLException if number of parameters doesn't correspond to expected number */ protected void validParameters() throws SQLException { if (prepareResult != null) { for (int i = 0; i < prepareResult.getParameters().length; i++) { if (!parameters.containsKey(i)) { throw exceptionFactory() .create("Parameter at position " + (i + 1) + " is not set", "07004"); } } } else { if (batchParameters != null && !batchParameters.isEmpty() && parameters.size() < batchParameters.get(0).size()) { // ensure batch parameters set same number throw exceptionFactory() .create( "batch set of parameters differ from previous set. All parameters must be set", "07004"); } // ensure all parameters are set for (int i = 0; i < parameters.size(); i++) { if (!parameters.containsKey(i)) { throw exceptionFactory() .create("Parameter at position " + (i + 1) + " is not set", "07004"); } } } } /** * Retrieves a ResultSetMetaData object that contains information about the columns * of the ResultSet object that will be returned when this PreparedStatement * object is executed. * *

Because a PreparedStatement object is precompiled, it is possible to know about * the ResultSet object that it will return without having to execute it. * Consequently, it is possible to invoke the method getMetaData on a * PreparedStatement object rather than waiting to execute it and then invoking the * ResultSet.getMetaData method on the ResultSet object that is returned. * *

NOTE: Using this method may be expensive for some drivers due to the lack of * underlying DBMS support. * * @return the description of a ResultSet object's columns or null if * the driver cannot return a ResultSetMetaData object * @throws SQLException if a database access error occurs or this method is called on a closed * PreparedStatement * @throws SQLFeatureNotSupportedException if the JDBC driver does not support this method * @since 1.2 */ @Override public ResultSetMetaData getMetaData() throws SQLException { // send COM_STMT_PREPARE if (prepareResult == null) { con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true); } return new org.mariadb.jdbc.client.result.ResultSetMetaData( exceptionFactory(), prepareResult.getColumns(), con.getContext().getConf(), false); } /** * Retrieves the number, types and properties of this PreparedStatement object's * parameters. * * @return a ParameterMetaData object that contains information about the number, * types and properties for each parameter marker of this PreparedStatement * object * @throws SQLException if a database access error occurs or this method is called on a closed * PreparedStatement * @see ParameterMetaData * @since 1.4 */ @Override public java.sql.ParameterMetaData getParameterMetaData() throws SQLException { // send COM_STMT_PREPARE if (prepareResult == null) { con.getClient().execute(new PreparePacket(escapeTimeout(sql)), this, true); } return new ParameterMetaData(exceptionFactory(), prepareResult.getParameters()); } @Override public int[] executeBatch() throws SQLException { checkNotClosed(); if (batchParameters == null || batchParameters.isEmpty()) return new int[0]; lock.lock(); try { boolean wasBulkInsert = executeInternalPreparedBatch(); int[] updates = new int[batchParameters.size()]; // specific case for BULK INSERT // return not Statement.SUCCESS_NO_INFO, but 1 if (wasBulkInsert) { int numberOfResult = 0; for (int i = 0; i < results.size(); i++) { numberOfResult += (int) ((OkPacket) results.get(i)).getAffectedRows(); } if (numberOfResult == updates.length) { Arrays.fill(updates, 1); currResult = results.remove(0); return updates; } } if (results.size() != updates.length) { for (int i = 0; i < updates.length; i++) { updates[i] = Statement.SUCCESS_NO_INFO; } } else { for (int i = 0; i < updates.length; i++) { if (results.get(i) instanceof OkPacket) { updates[i] = (int) ((OkPacket) results.get(i)).getAffectedRows(); } else { updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO; } } } currResult = results.remove(0); return updates; } finally { localInfileInputStream = null; batchParameters.clear(); lock.unlock(); } } @Override public long[] executeLargeBatch() throws SQLException { checkNotClosed(); if (batchParameters == null || batchParameters.isEmpty()) return new long[0]; lock.lock(); try { boolean wasBulkInsert = executeInternalPreparedBatch(); long[] updates = new long[batchParameters.size()]; // specific case for BULK INSERT // return not Statement.SUCCESS_NO_INFO, but 1 if (wasBulkInsert) { int numberOfResult = 0; for (int i = 0; i < results.size(); i++) { numberOfResult += (int) ((OkPacket) results.get(i)).getAffectedRows(); } if (numberOfResult == updates.length) { Arrays.fill(updates, 1L); currResult = results.remove(0); return updates; } } if (results.size() != updates.length) { for (int i = 0; i < updates.length; i++) { updates[i] = Statement.SUCCESS_NO_INFO; } } else { for (int i = 0; i < updates.length; i++) { if (results.get(i) instanceof OkPacket) { updates[i] = ((OkPacket) results.get(i)).getAffectedRows(); } else { updates[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO; } } } currResult = results.remove(0); return updates; } finally { batchParameters.clear(); lock.unlock(); } } private ExceptionFactory exceptionFactory() { return con.getExceptionFactory().of(this); } @Override public void close() throws SQLException { if (prepareResult != null) { prepareResult.decrementUse(con.getClient(), this); prepareResult = null; } con.fireStatementClosed(this); super.close(); } /** * 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 String toString() { return "ServerPreparedStatement{" + super.toString() + '}'; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy