org.firebirdsql.gds.ng.wire.version10.V10Statement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaybird Show documentation
Show all versions of jaybird Show documentation
JDBC Driver for the Firebird RDBMS
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.gds.ng.wire.version10;
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.impl.wire.WireProtocolConstants;
import org.firebirdsql.gds.impl.wire.XdrInputStream;
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
import org.firebirdsql.gds.ng.*;
import org.firebirdsql.gds.ng.fields.*;
import org.firebirdsql.gds.ng.wire.*;
import java.io.IOException;
import java.sql.SQLException;
import java.sql.SQLNonTransientException;
import java.sql.SQLWarning;
import static org.firebirdsql.gds.ng.TransactionHelper.checkTransactionActive;
/**
* {@link org.firebirdsql.gds.ng.wire.FbWireStatement} implementation for the version 10 wire protocol.
*
* @author Mark Rotteveel
* @since 3.0
*/
public class V10Statement extends AbstractFbWireStatement implements FbWireStatement {
// TODO Handle error state in a consistent way (eg when does an exception lead to the error state, or when is it 'just' valid feedback)
// TODO Fix state transitions
private static final int NULL_INDICATOR_NOT_NULL = 0;
private static final int NULL_INDICATOR_NULL = -1;
private static final System.Logger log = System.getLogger(V10Statement.class.getName());
/**
* Creates a new instance of V10Statement for the specified database.
*
* @param database
* FbWireDatabase implementation
*/
public V10Statement(FbWireDatabase database) {
super(database);
}
@Override
protected void free(final int option) throws SQLException {
try (LockCloseable ignored = withLock()) {
try {
doFreePacket(option);
getXdrOut().flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
try {
processFreeResponse(getDatabase().readResponse(getStatementWarningCallback()));
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
}
}
}
/**
* Handles sending the free statement packet and associated state changes on this statement
*
* @param option
* free statement option
*/
protected void doFreePacket(int option) throws SQLException, IOException {
sendFree(option);
// Reset statement information
reset(option == ISCConstants.DSQL_drop);
}
/**
* Sends the free statement to the database
*
* @param option
* Free statement option
*/
protected void sendFree(int option) throws IOException, SQLException {
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(WireProtocolConstants.op_free_statement);
xdrOut.writeInt(getHandle());
xdrOut.writeInt(option);
}
/**
* Processes the response to the free statement.
*
* @param response
* Response object
*/
protected void processFreeResponse(@SuppressWarnings("UnusedParameters") Response response) {
// No processing needed
}
@Override
public void prepare(final String statementText) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkTransactionActive(getTransaction());
final StatementState initialState = getState();
if (!isPrepareAllowed(initialState)) {
throw new SQLNonTransientException(String.format("Current statement state (%s) does not allow call to prepare", initialState));
}
resetAll();
if (initialState == StatementState.NEW) {
sendAllocate0();
receiveAllocate0Response();
} else {
checkStatementValid();
}
sendPrepare0(statementText);
receivePrepare0Response();
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
private void sendAllocate0() throws SQLException {
try {
sendAllocate();
getXdrOut().flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
}
private void receiveAllocate0Response() throws SQLException {
try {
processAllocateResponse(getDatabase().readGenericResponse(getStatementWarningCallback()));
switchState(StatementState.ALLOCATED);
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
} catch (SQLException e) {
forceState(StatementState.NEW);
throw e;
}
}
private void sendPrepare0(String statementText) throws SQLException {
try {
sendPrepare(statementText);
getXdrOut().flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
}
private void receivePrepare0Response() throws SQLException {
try {
processPrepareResponse(getDatabase().readGenericResponse(getStatementWarningCallback()));
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
} catch (SQLException e) {
switchState(StatementState.ALLOCATED);
throw e;
}
}
/**
* Sends the statement prepare to the connection.
*
* @param statementText
* Statement
*/
protected void sendPrepare(final String statementText) throws SQLException, IOException {
switchState(StatementState.PREPARING);
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(WireProtocolConstants.op_prepare_statement);
xdrOut.writeInt(getTransaction().getHandle());
xdrOut.writeInt(getHandle());
xdrOut.writeInt(getDatabase().getConnectionDialect());
xdrOut.writeString(statementText, getDatabase().getEncoding());
xdrOut.writeBuffer(getStatementInfoRequestItems());
xdrOut.writeInt(getDefaultSqlInfoSize());
}
/**
* Processes the prepare response from the server.
*
* @param genericResponse
* GenericResponse
*/
protected void processPrepareResponse(final GenericResponse genericResponse) throws SQLException {
parseStatementInfo(genericResponse.data());
switchState(StatementState.PREPARED);
}
@Override
protected void setCursorNameImpl(String cursorName)throws SQLException {
try {
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(WireProtocolConstants.op_set_cursor);
xdrOut.writeInt(getHandle());
// Null termination is needed due to a quirk of the protocol
xdrOut.writeString(cursorName + '\0', getDatabase().getEncoding());
xdrOut.writeInt(0); // Cursor type
xdrOut.flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
try {
getDatabase().readGenericResponse(getStatementWarningCallback());
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
}
}
@Override
public void execute(final RowValue parameters) throws SQLException {
final StatementState initialState = getState();
try (LockCloseable ignored = withLock()) {
checkStatementValid();
checkTransactionActive(getTransaction());
validateParameters(parameters);
reset(false);
switchState(StatementState.EXECUTING);
final StatementType statementType = getType();
final boolean hasSingletonResult = hasSingletonResult();
int expectedResponseCount = 0;
try (OperationCloseHandle operationCloseHandle = signalExecute()) {
if (operationCloseHandle.isCancelled()) {
// operation was synchronously cancelled from an OperationAware implementation
throw FbExceptionBuilder.forException(ISCConstants.isc_cancelled).toSQLException();
}
try {
if (hasSingletonResult) {
expectedResponseCount++;
}
sendExecute(hasSingletonResult
? WireProtocolConstants.op_execute2
: WireProtocolConstants.op_execute,
parameters);
expectedResponseCount++;
getXdrOut().flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
final WarningMessageCallback statementWarningCallback = getStatementWarningCallback();
try {
final FbWireDatabase db = getDatabase();
try {
expectedResponseCount--;
Response response = db.readResponse(statementWarningCallback);
if (hasSingletonResult) {
/* A type with a singleton result (ie an execute procedure with return fields), doesn't actually
* have a result set that will be fetched, instead we have a singleton result if we have fields
*/
statementListenerDispatcher.statementExecuted(this, false, true);
if (response instanceof SqlResponse sqlResponse) {
processExecuteSingletonResponse(sqlResponse);
expectedResponseCount--;
response = db.readResponse(statementWarningCallback);
} else {
// We didn't get an op_sql_response first, something is iffy, maybe cancellation or very low level problem?
// We don't expect any more responses after this
expectedResponseCount = 0;
SQLWarning sqlWarning = new SQLWarning(
"Expected an SqlResponse, instead received a " + response.getClass().getName());
log.log(System.Logger.Level.WARNING, "Unexpected response; see debug level for stacktrace");
log.log(System.Logger.Level.DEBUG, "Unexpected response", sqlWarning);
statementWarningCallback.processWarning(sqlWarning);
}
setAfterLast();
} else {
// A normal execute is never a singleton result (even if it only produces a single result)
statementListenerDispatcher.statementExecuted(this, hasFields(), false);
}
// This should always be a GenericResponse, otherwise something went fundamentally wrong anyway
processExecuteResponse((GenericResponse) response);
} catch (SQLException e) {
if (e.getErrorCode() == ISCConstants.isc_cancelled) {
expectedResponseCount = 0;
}
throw e;
} finally {
db.consumePackets(expectedResponseCount, getStatementWarningCallback());
}
if (getState() != StatementState.ERROR) {
switchState(statementType.isTypeWithCursor() ? StatementState.CURSOR_OPEN : StatementState.PREPARED);
}
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
}
}
} catch (SQLException e) {
if (getState() != StatementState.ERROR) {
switchState(initialState);
}
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
/**
* Sends the execute (for {@code op_execute} or {@code op_execute2}) to the database.
*
* @param operation
* Operation ({@code op_execute} or {@code op_execute2})
* @param parameters
* Parameters
*/
protected void sendExecute(final int operation, final RowValue parameters) throws IOException, SQLException {
assert operation == WireProtocolConstants.op_execute || operation == WireProtocolConstants.op_execute2 : "Needs to be called with operation op_execute or op_execute2";
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(operation);
xdrOut.writeInt(getHandle());
xdrOut.writeInt(getTransaction().getHandle());
if (parameters != null && parameters.getCount() > 0) {
final RowDescriptor parameterDescriptor = getParameterDescriptor();
xdrOut.writeBuffer(calculateBlr(parameterDescriptor, parameters));
xdrOut.writeInt(0); // message number = in_message_type
xdrOut.writeInt(1); // Number of messages
writeSqlData(parameterDescriptor, parameters, true);
} else {
xdrOut.writeBuffer(null);
xdrOut.writeInt(0); // message number = in_message_type
xdrOut.writeInt(0); // Number of messages
}
if (operation == WireProtocolConstants.op_execute2) {
final RowDescriptor fieldDescriptor = getRowDescriptor();
xdrOut.writeBuffer(fieldDescriptor != null && fieldDescriptor.getCount() > 0 ? calculateBlr(fieldDescriptor) : null);
xdrOut.writeInt(0); // out_message_number = out_message_type
}
}
/**
* Process the execute response for statements with a singleton response ({@code op_execute2}; stored
* procedures).
*
* @param sqlResponse
* SQL response object
*/
protected void processExecuteSingletonResponse(SqlResponse sqlResponse) throws SQLException, IOException {
if (sqlResponse.count() > 0) {
queueRowData(readSqlData());
}
}
/**
* Process the execute response.
*
* @param genericResponse
* Generic response object
*/
@SuppressWarnings("unused")
protected void processExecuteResponse(GenericResponse genericResponse) {
// Nothing to do here
}
@Override
public void fetchRows(int fetchSize) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkStatementHasOpenCursor();
checkFetchSize(fetchSize);
if (isAfterLast()) return;
try (OperationCloseHandle operationCloseHandle = signalFetch()) {
if (operationCloseHandle.isCancelled()) {
// operation was synchronously cancelled from an OperationAware implementation
throw FbExceptionBuilder.forException(ISCConstants.isc_cancelled).toSQLException();
}
sendFetch0(fetchSize);
receiveFetch0Response();
}
} catch (SQLException e) {
exceptionListenerDispatcher.errorOccurred(e);
throw e;
}
}
private void sendFetch0(int fetchSize) throws SQLException {
try {
sendFetch(fetchSize);
getXdrOut().flush();
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioWriteError(e);
}
}
private void receiveFetch0Response() throws SQLException {
try {
processFetchResponse(FetchDirection.FORWARD);
} catch (IOException e) {
switchState(StatementState.ERROR);
throw FbExceptionBuilder.ioReadError(e);
}
}
/**
* Process the fetch response by reading the returned rows and queuing them.
*
* @param direction
* fetch direction
*/
protected void processFetchResponse(FetchDirection direction) throws IOException, SQLException {
processFetchResponse(direction, null);
}
/**
* Process the fetch response by reading the returned rows and queuing them.
*
* @param direction
* fetch direction
* @param response initial response, or {@code null} to retrieve initial response
*/
protected void processFetchResponse(FetchDirection direction, Response response) throws IOException, SQLException {
int rowsFetched = 0;
FbWireDatabase database = getDatabase();
if (response == null) {
response = getDatabase().readResponse(getStatementWarningCallback());
}
do {
if (!(response instanceof FetchResponse fetchResponse)) break;
if (fetchResponse.count() > 0 && fetchResponse.status() == ISCConstants.FETCH_OK) {
queueRowData(readSqlData());
rowsFetched++;
} else if (fetchResponse.status() == ISCConstants.FETCH_NO_MORE_ROWS) {
switch (direction) {
case IN_PLACE -> {
if (isBeforeFirst()) {
setBeforeFirst();
} else {
setAfterLast();
}
}
case FORWARD,
// Not generally applicable, but handling same as FORWARD (as after-last)
UNKNOWN -> setAfterLast();
case REVERSE -> setBeforeFirst();
}
// Note: we are not explicitly 'closing' the cursor here as we might be scrolling
// Exit loop
break;
} else {
log.log(System.Logger.Level.DEBUG, "Received unexpected fetch response {0}, ignored", fetchResponse);
break;
}
} while ((response = database.readResponse(getStatementWarningCallback())) instanceof FetchResponse);
statementListenerDispatcher.fetchComplete(this, direction, rowsFetched);
// TODO Handle other response type?
}
/**
* Sends the fetch request to the database.
*
* @param fetchSize Number of rows to fetch.
*/
protected void sendFetch(int fetchSize) throws SQLException, IOException {
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(WireProtocolConstants.op_fetch);
xdrOut.writeInt(getHandle());
xdrOut.writeBuffer(hasFetched() ? null : calculateBlr(getRowDescriptor()));
xdrOut.writeInt(0); // out_message_number = out_message_type
xdrOut.writeInt(fetchSize); // fetch size
}
/**
* Reads a single row from the database.
*
* @return Row as a {@link RowValue}
*/
protected RowValue readSqlData() throws SQLException, IOException {
final RowDescriptor rowDescriptor = getRowDescriptor();
final RowValue rowValue = rowDescriptor.createDefaultFieldValues();
final BlrCalculator blrCalculator = getBlrCalculator();
final XdrInputStream xdrIn = getXdrIn();
for (int idx = 0; idx < rowDescriptor.getCount(); idx++) {
final FieldDescriptor fieldDescriptor = rowDescriptor.getFieldDescriptor(idx);
final int len = blrCalculator.calculateIoLength(fieldDescriptor);
byte[] buffer = readColumnData(xdrIn, len);
if (xdrIn.readInt() == NULL_INDICATOR_NULL) {
buffer = null;
}
rowValue.setFieldData(idx, buffer);
}
return rowValue;
}
protected byte[] readColumnData(XdrInputStream xdrIn, int len) throws IOException {
if (len == 0) {
// Length specified in response
return xdrIn.readBuffer();
} else if (len < 0) {
// Buffer is not padded
return xdrIn.readRawBuffer(-len);
} else {
// len is incremented in calculateIoLength to avoid value 0, so it must be decremented
return xdrIn.readBuffer(len - 1);
}
}
/**
* Write a set of SQL data from a {@link RowValue}.
*
* @param rowDescriptor
* The row descriptor
* @param fieldValues
* The List containing the SQL data to be written
* @param useActualLength
* Should actual field length be used (applies to CHAR)
* @throws IOException
* if an error occurs while writing to the underlying output stream
*/
protected void writeSqlData(RowDescriptor rowDescriptor, RowValue fieldValues, boolean useActualLength)
throws IOException, SQLException {
final XdrOutputStream xdrOut = getXdrOut();
final BlrCalculator blrCalculator = getBlrCalculator();
for (int idx = 0; idx < fieldValues.getCount(); idx++) {
final FieldDescriptor fieldDescriptor = rowDescriptor.getFieldDescriptor(idx);
final byte[] buffer = fieldValues.getFieldData(idx);
final int len = useActualLength
? blrCalculator.calculateIoLength(fieldDescriptor, buffer)
: blrCalculator.calculateIoLength(fieldDescriptor);
writeColumnData(xdrOut, len, buffer, fieldDescriptor);
// sqlind (null indicator)
xdrOut.writeInt(buffer != null ? NULL_INDICATOR_NOT_NULL : NULL_INDICATOR_NULL);
}
}
protected void writeColumnData(XdrOutputStream xdrOut, int len, byte[] buffer, FieldDescriptor fieldDescriptor)
throws IOException {
// Nothing to write for SQL_NULL (except null indicator in v10 - v12, which happens at end in writeSqlData)
if (fieldDescriptor.isFbType(ISCConstants.SQL_NULL)) return;
if (len == 0) {
writeLengthPrefixedBuffer(xdrOut, buffer, fieldDescriptor);
} else if (len < 0) {
writeFixedLengthBuffer(xdrOut, -len, buffer);
} else {
// decrement length because it was incremented before; increment happens in BlrCalculator.calculateIoLength
writePaddedBuffer(xdrOut, len - 1, buffer, fieldDescriptor);
}
}
/**
* Writes the entire buffer prefixed with length and suffixed with padding using the padding byte of the descriptor.
*/
private static void writeLengthPrefixedBuffer(XdrOutputStream xdrOut, byte[] buffer,
FieldDescriptor fieldDescriptor) throws IOException {
if (buffer != null) {
int len = buffer.length;
xdrOut.writeInt(len);
xdrOut.write(buffer, 0, len);
xdrOut.writePadding((4 - len) & 3, fieldDescriptor.getPaddingByte());
} else {
xdrOut.writeInt(0);
}
}
/**
* Writes {@code len} bytes of the buffer, or if {@code buffer == null}, {@code len} bytes of zero padding.
*/
private static void writeFixedLengthBuffer(XdrOutputStream xdrOut, int len, byte[] buffer) throws IOException {
if (buffer != null) {
xdrOut.write(buffer, 0, len);
} else {
xdrOut.writeZeroPadding(len);
}
}
/**
* Writes at most {@code len} bytes from {@code buffer}, padding upto {@code len} if the buffer is shorter or
* {@code null}, suffixed with the normal buffer padding. Padding is done with the padding byte from the descriptor.
*/
private static void writePaddedBuffer(XdrOutputStream xdrOut, int len, byte[] buffer,
FieldDescriptor fieldDescriptor) throws IOException {
if (buffer != null) {
final int buflen = buffer.length;
if (buflen >= len) {
xdrOut.write(buffer, 0, len);
xdrOut.writePadding((4 - len) & 3, fieldDescriptor.getPaddingByte());
} else {
xdrOut.write(buffer, 0, buflen);
xdrOut.writePadding(len - buflen + ((4 - len) & 3), fieldDescriptor.getPaddingByte());
}
} else {
xdrOut.writePadding(len + ((4 - len) & 3), fieldDescriptor.getPaddingByte());
}
}
/**
* Sends the allocate request to the server.
*/
protected void sendAllocate() throws SQLException, IOException {
final XdrOutputStream xdrOut = getXdrOut();
xdrOut.writeInt(WireProtocolConstants.op_allocate_statement);
xdrOut.writeInt(0);
}
/**
* Processes the allocate response from the server.
*
* @param response
* GenericResponse
*/
@SuppressWarnings("java:S1130")
protected void processAllocateResponse(GenericResponse response) throws SQLException {
try (LockCloseable ignored = withLock()) {
setHandle(response.objectHandle());
reset();
setType(StatementType.NONE);
}
}
@Override
public int getDefaultSqlInfoSize() {
// TODO Test for an optimal buffer size
return getMaxSqlInfoSize();
}
@Override
public int getMaxSqlInfoSize() {
// TODO Is this the actual max for the v10 protocol, or is it 65535?
return 32767;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy