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

org.firebirdsql.jdbc.ServerBatch Maven / Gradle / Ivy

The newest version!
/*
 * Firebird Open Source JDBC Driver
 *
 * Distributable under LGPL license.
 * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * LGPL License for more details.
 *
 * This file was created by members of the firebird development team.
 * All individual contributions remain the Copyright (C) of those
 * individuals.  Contributors to this file are either listed here or
 * can be obtained from a source control history command.
 *
 * All rights reserved.
 */
package org.firebirdsql.jdbc;

import org.firebirdsql.gds.ng.BatchCompletion;
import org.firebirdsql.gds.ng.DeferredResponse;
import org.firebirdsql.gds.ng.FbBatchConfig;
import org.firebirdsql.gds.ng.FbStatement;
import org.firebirdsql.gds.ng.StatementState;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.StatementListener;
import org.firebirdsql.jaybird.fb.constants.BatchItems;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import org.firebirdsql.util.Primitives;
import org.firebirdsql.util.SQLExceptionChainBuilder;

import java.sql.BatchUpdateException;
import java.sql.SQLException;
import java.sql.SQLNonTransientException;
import java.sql.Statement;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.List;

import static java.lang.String.format;
import static java.util.Collections.emptyList;

/**
 * Batch implementation using server-side batch updates.
 * 

* This implementation itself is not thread-safe, and expects the caller to lock appropriately using * {@link FbStatement#withLock()} of the executing statement. *

* * @author Mark Rotteveel * @since 5 */ final class ServerBatch implements Batch, StatementListener { private static final Logger log = LoggerFactory.getLogger(ServerBatch.class); private volatile BatchState state = BatchState.INITIAL; private final FbBatchConfig batchConfig; private Deque batchRowValues = new ArrayDeque<>(); private FbStatement statement; ServerBatch(FbBatchConfig batchConfig, FbStatement statement) throws SQLException { if (!statement.supportBatchUpdates()) { throw new FBDriverNotCapableException( format("FbStatement implementation %s does not support server-side batch updates", statement.getClass().getName())); } this.batchConfig = batchConfig.immutable(); this.statement = statement; statement.addStatementListener(this); } @Override public void statementStateChanged(FbStatement sender, StatementState newState, StatementState previousState) { if (isClosed() || sender != statement) { sender.removeStatementListener(this); return; } switch (newState) { case ALLOCATED: case PREPARING: // NOTE: These state transition shouldn't occur for usage in FBPreparedStatement; included for robustness // Server-side batch is deallocated when unprepared or when preparing a new statement text if (state != BatchState.INITIAL) { // NOTE: Changing state before clearing batch to avoid server-side cancel (which would fail) state = BatchState.INITIAL; try { clearBatch(); } catch (SQLException e) { // path when state is INITIAL should not result in SQLException log.debug("Unexpected exception clearing batch, this might indicate a bug in Jaybird", e); } } break; case CLOSED: // Normal usage from FBPreparedStatement will have already closed it; included for robustness close(); break; default: // do nothing break; } } @Override public void addBatch(Batch.BatchRowValue rowValue) throws SQLException { checkOpen(); batchRowValues.addLast(rowValue); } private boolean isEmpty() throws SQLException { checkOpen(); return batchRowValues.isEmpty(); } @Override public List execute() throws SQLException { try { checkOpen(); if (isEmpty()) { return emptyList(); } Collection rowValues = toRowValues(); SQLExceptionChainBuilder chain = new SQLExceptionChainBuilder<>(); // Create server-side batch if (!state.isOpenOnServer()) { createBatch(chain); } // Send batch to server sendBatch(rowValues, chain); BatchCompletion batchCompletion = executeBatch(chain); if (batchCompletion.hasErrors()) { throw chain.addFirst(toBatchUpdateException(batchCompletion)).getException(); } else if (chain.hasException()) { // We had an earlier exception on sending the batch, causing us to execute an empty batch throw chain.getException(); } else { int[] updateCounts = toJdbcUpdateCounts(batchCompletion); return Primitives.toLongList(updateCounts); } } finally { clearBatch(); } } /** * Sends batch create with deferred response processing. * * @param chain * Chain to append error returned from server, if any, or to throw previously chained exceptions * @throws SQLException * For database access errors (I/O errors) */ @SuppressWarnings("NonAtomicOperationOnVolatileField") private void createBatch(SQLExceptionChainBuilder chain) throws SQLException { try { statement.deferredBatchCreate(batchConfig, new BatchDeferredAction(chain, "exception creating batch") { @Override public void onException(Exception exception) { super.onException(exception); state = BatchState.INITIAL; } }); // We assume success on opening, otherwise it's reset in onException of the deferred action state = state.onServerOpen(); } catch (SQLException e) { chain.append(e); throw chain.getException(); } } /** * Sends batch data with deferred response processing. * * @param chain * Chain to append error returned from server, if any, or to throw previously chained exceptions * @throws SQLException * For database access errors (I/O errors) */ @SuppressWarnings("NonAtomicOperationOnVolatileField") private void sendBatch(Collection rowValues, SQLExceptionChainBuilder chain) throws SQLException { try { statement.deferredBatchSend(rowValues, new BatchDeferredAction(chain, "exception sending batch message") { @Override public void onException(Exception exception) { super.onException(exception); state = BatchState.SERVER_OPEN; } }); // We assume success on opening, otherwise it's reset in onException of the deferred action state = state.onSend(); } catch (SQLException e) { chain.append(e); throw chain.getException(); } } /** * Execute the batch on the server. * * @param chain * Chain to append error returned from server, if any, or to throw previously chained exceptions * @return batch completion response * @throws SQLException * for database access errors */ @SuppressWarnings("NonAtomicOperationOnVolatileField") private BatchCompletion executeBatch(SQLExceptionChainBuilder chain) throws SQLException { try { state = state.onExecute(); BatchCompletion batchCompletion = statement.batchExecute(); state = state.onBatchComplete(); return batchCompletion; } catch (SQLException e) { chain.append(e); throw chain.getException(); } } private Collection toRowValues() throws SQLException { Deque batchRowValues = this.batchRowValues; List rowValues = new ArrayList<>(batchRowValues.size()); Batch.BatchRowValue batchRowValue; while ((batchRowValue = batchRowValues.pollFirst()) != null) { rowValues.add(batchRowValue.toRowValue()); } return rowValues; } @SuppressWarnings("NonAtomicOperationOnVolatileField") @Override public void clearBatch() throws SQLException { checkOpen(); try { if (state.isBatchOnServer()) { statement.batchCancel(); state = state.onServerCancel(); } } finally { batchRowValues.clear(); } } private void checkOpen() throws SQLException { if (isClosed()) { throw new SQLException("batch has been closed"); } } boolean isClosed() { return state == BatchState.CLOSED; } @Override public void close() { if (isClosed()) return; state = BatchState.CLOSED; batchRowValues = null; FbStatement copyStmt = statement; if (copyStmt != null) { statement = null; copyStmt.removeStatementListener(this); } } /** * Produces update counts as expected by JDBC. *

* When no update counts ({@code TAG_RECORD_COUNTS}) were requested, this method will generate appropriate * update counts (populated with {@link Statement#SUCCESS_NO_INFO}, and - if multi-error - * {@link Statement#EXECUTE_FAILED} for errors). *

* * @param batchCompletion * Batch completion data * @return update counts as expected by JDBC * @see #toBatchUpdateException(BatchCompletion) */ int[] toJdbcUpdateCounts(BatchCompletion batchCompletion) { int elementCount = batchCompletion.elementCount(); int[] jdbcUpdateCounts = batchCompletion.updateCounts(); List detailedErrors = batchCompletion.detailedErrors(); int[] simplifiedErrors = batchCompletion.simplifiedErrors(); if (!batchCompletion.hasErrors()) { if (jdbcUpdateCounts.length == 0 && elementCount > 0) { // FbBatchConfig.updateCounts() was false, report as SUCCESS_NO_INFO for each element jdbcUpdateCounts = new int[elementCount]; Arrays.fill(jdbcUpdateCounts, Statement.SUCCESS_NO_INFO); } } else if (jdbcUpdateCounts.length == 0 && elementCount > 0) { // FbBatchConfig.updateCounts() was false, report as SUCCESS_NO_INFO if (batchConfig.multiError()) { jdbcUpdateCounts = new int[elementCount]; Arrays.fill(jdbcUpdateCounts, Statement.SUCCESS_NO_INFO); // Populate EXECUTE_FAILED for errors for (BatchCompletion.DetailedError error : detailedErrors) { jdbcUpdateCounts[error.element()] = Statement.EXECUTE_FAILED; } for (int element : simplifiedErrors) { jdbcUpdateCounts[element] = Statement.EXECUTE_FAILED; } } else { int firstErrorPosition = detailedErrors.isEmpty() ? simplifiedErrors[0] : detailedErrors.get(0).element(); if (firstErrorPosition != 0) { jdbcUpdateCounts = new int[firstErrorPosition]; Arrays.fill(jdbcUpdateCounts, Statement.SUCCESS_NO_INFO); } } } else if (batchConfig.multiError()) { for (int i = 0; i < jdbcUpdateCounts.length; i++) { // Replace Firebird failure code (-1) with JDBC failure code (-3) if (jdbcUpdateCounts[i] == BatchItems.BATCH_EXECUTE_FAILED) { jdbcUpdateCounts[i] = Statement.EXECUTE_FAILED; } } } else if (jdbcUpdateCounts[jdbcUpdateCounts.length - 1] == BatchItems.BATCH_EXECUTE_FAILED) { // Exclude update count for failed row for non-multi-error case (see JDBC requirements) jdbcUpdateCounts = Arrays.copyOf(jdbcUpdateCounts, jdbcUpdateCounts.length - 1); } return jdbcUpdateCounts; } /** * Produces a {@code BatchUpdateException} with exception information and update counts. *

* If available, the message, SQLstate and error code of the first detailed error is used for * the {@code BatchUpdateException}. The chain of detailed errors is added as the next exception * this {@code BatchUpdateException}, and can be accessed through {@link SQLException#getNextException()}. *

* * @param batchCompletion * Batch completion data * @return populated {@code BatchUpdateException} * @throws IllegalStateException * When this response has no errors ({@link BatchCompletion#hasErrors()} returns {@code false} * @see #toJdbcUpdateCounts(BatchCompletion) */ BatchUpdateException toBatchUpdateException(BatchCompletion batchCompletion) { if (!batchCompletion.hasErrors()) { throw new IllegalStateException("toBatchUpdateException called while BatchCompletion has no errors"); } int[] updateCounts = toJdbcUpdateCounts(batchCompletion); List detailedErrors = batchCompletion.detailedErrors(); if (detailedErrors.isEmpty()) { // NOTE: Currently not configurable, we're always using the default of 64 errors return new BatchUpdateException("Batch execution failed without detailed errors; " + "this can happen when detailedErrors batch config is set to zero", SQLStateConstants.SQL_STATE_GENERAL_ERROR, 0, updateCounts); } SQLException exception = detailedErrors.get(0).error(); BatchUpdateException bue = new BatchUpdateException(exception.getMessage(), exception.getSQLState(), exception.getErrorCode(), updateCounts); detailedErrors.stream() .map(BatchCompletion.DetailedError::error) .forEach(bue::setNextException); return bue; } private static class BatchDeferredAction implements DeferredResponse { private final SQLExceptionChainBuilder chain; private final String genericExceptionMessage; BatchDeferredAction(SQLExceptionChainBuilder chain, String genericExceptionMessage) { this.chain = chain; this.genericExceptionMessage = genericExceptionMessage; } @Override public void onException(Exception exception) { if (exception instanceof SQLException) { chain.append((SQLException) exception); } else { chain.append(new SQLNonTransientException(genericExceptionMessage, exception)); } } } private enum BatchState { /** * Initial state when open, but not yet open on server, including after preparing a new statement on a handle */ INITIAL, /** * Batch is open on server */ SERVER_OPEN, /** * At least a part of the batch has been sent to the server */ PARTIAL_SEND, /** * The batch is being executed. */ EXECUTING, /** * Batch has been closed */ CLOSED; /** * @return {@code true} if the batch is open on the server for this state */ boolean isOpenOnServer() { return !(this == INITIAL || this == CLOSED); } /** * @return {@code true} if there is batch data on the server for this state */ boolean isBatchOnServer() { return this == PARTIAL_SEND || this == EXECUTING; } /** * @return new state on server-open. */ BatchState onServerOpen() throws SQLException { if (this == INITIAL) { return SERVER_OPEN; } else { throw new SQLNonTransientException("Cannot server-open in state " + this); } } /** * @return new state on sending batch data. */ BatchState onSend() throws SQLException { switch (this) { case INITIAL: // Assume we open as part of the send operation case SERVER_OPEN: case PARTIAL_SEND: return PARTIAL_SEND; // Assume we send more data as part of executing a very large batch case EXECUTING: return EXECUTING; case CLOSED: throw new SQLNonTransientException("Cannot send in state CLOSED"); default: throw new SQLNonTransientException("Unexpected state " + this); } } /** * @return new state on execute */ BatchState onExecute() throws SQLException { switch (this) { case INITIAL: // Assume we open as part of the execute operation case SERVER_OPEN: case PARTIAL_SEND: // Assume we execute as part of executing a very large batch case EXECUTING: return EXECUTING; case CLOSED: throw new SQLNonTransientException("Cannot execute in state CLOSED"); default: throw new SQLNonTransientException("Unexpected state " + this); } } /** * @return new state on completing batch execute */ BatchState onBatchComplete() throws SQLException { switch (this) { case EXECUTING: return SERVER_OPEN; case CLOSED: throw new SQLNonTransientException("Cannot complete in state CLOSED"); default: throw new SQLNonTransientException("Unexpected state " + this); } } BatchState onServerCancel() throws SQLException { if (this == CLOSED || this == INITIAL) { return this; } return SERVER_OPEN; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy