org.firebirdsql.jdbc.FBConnection Maven / Gradle / Ivy
Show all versions of jaybird Show documentation
/*
* 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.ISCConstants;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.TransactionParameterBuffer;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.ng.FbDatabase;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.gds.ng.IConnectionProperties;
import org.firebirdsql.gds.ng.LockCloseable;
import org.firebirdsql.jaybird.parser.LocalStatementClass;
import org.firebirdsql.jaybird.parser.LocalStatementType;
import org.firebirdsql.jaybird.parser.StatementDetector;
import org.firebirdsql.jaybird.props.DatabaseConnectionProperties;
import org.firebirdsql.jaybird.props.PropertyConstants;
import org.firebirdsql.jaybird.xca.FBLocalTransaction;
import org.firebirdsql.jaybird.xca.FBManagedConnection;
import org.firebirdsql.jaybird.util.SQLExceptionChainBuilder;
import org.firebirdsql.jdbc.InternalTransactionCoordinator.MetaDataTransactionCoordinator;
import org.firebirdsql.jdbc.escape.FBEscapedParser;
import org.firebirdsql.util.InternalApi;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.function.Function;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.lang.System.Logger.Level.WARNING;
import static java.util.stream.Collectors.toMap;
import static org.firebirdsql.gds.ISCConstants.fb_cancel_abort;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_CONNECTION_CLOSED;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_GENERAL_ERROR;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_INVALID_TX_STATE;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_TX_ACTIVE;
/**
* The class {@code FBConnection} is a handle to a {@link FBManagedConnection} and implements {@link Connection}.
*
* This class is internal API of Jaybird. Future versions may radically change, move, or make inaccessible this type.
* For the public API, refer to the {@link Connection} and {@link FirebirdConnection} interfaces.
*
*
* @author David Jencks
* @author Mark Rotteveel
*/
@SuppressWarnings({ "RedundantThrows", "SqlSourceToSinkFlow" })
@InternalApi
public class FBConnection implements FirebirdConnection {
private static final System.Logger log = System.getLogger(FBConnection.class.getName());
private static final SQLPermission PERMISSION_SET_NETWORK_TIMEOUT = new SQLPermission("setNetworkTimeout");
private static final SQLPermission PERMISSION_CALL_ABORT = new SQLPermission("callAbort");
@SuppressWarnings("java:S3077")
protected volatile FBManagedConnection mc;
private FBLocalTransaction localTransaction;
private FBDatabaseMetaData metaData;
protected final InternalTransactionCoordinator txCoordinator;
private SQLWarning firstWarning;
// This set contains all allocated but not closed statements
// It is used to close them before the connection is closed
protected final Set activeStatements = Collections.synchronizedSet(new HashSet<>());
private int resultSetHoldability;
private StoredProcedureMetaData storedProcedureMetaData;
private GeneratedKeysSupport generatedKeysSupport;
private ClientInfoProvider clientInfoProvider;
private boolean readOnly;
/**
* Create a new FBConnection instance based on a {@link FBManagedConnection}.
*
* @param mc
* A FBManagedConnection around which this connection is based
*/
public FBConnection(FBManagedConnection mc) {
this.mc = mc;
txCoordinator = new InternalTransactionCoordinator(this);
IConnectionProperties props = mc.getConnectionRequestInfo().asIConnectionProperties();
resultSetHoldability = props.isDefaultResultSetHoldable()
? ResultSet.HOLD_CURSORS_OVER_COMMIT
: ResultSet.CLOSE_CURSORS_AT_COMMIT;
// Inherit read-only state from the initial TPB
readOnly = mc.isTpbReadOnly();
}
@Override
public int getHoldability() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return resultSetHoldability;
}
}
@Override
public void setHoldability(int holdability) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
this.resultSetHoldability = holdability;
}
}
/**
* Check if this connection is valid. This method should be invoked before
* executing any action in this class.
*
* @throws SQLException
* if this connection has been closed and cannot be used anymore
*/
protected void checkValidity() throws SQLException {
if (isClosed()) {
throw new SQLNonTransientConnectionException("This connection is closed and cannot be used now",
SQL_STATE_CONNECTION_CLOSED);
}
}
/**
* This method should be invoked by each of the statements in the {@link Statement#close()} method. Here we remove
* statement from the {@code activeStatements} set, so we do not need to close it later.
*
* @param stmt
* statement that was closed.
*/
void notifyStatementClosed(FirebirdStatement stmt) {
if (!activeStatements.remove(stmt)) {
if (stmt instanceof FBPreparedStatement pstmt && !pstmt.isInitialized()) {
// Close was likely triggered by finalizer of a prepared statement that failed on prepare in
// the constructor: Do not log warning
return;
}
// NOTE: Can also mean notifyStatementClosed was called multiple times for a statement
log.log(WARNING, "Specified statement was not created by this connection: {0}", stmt);
}
}
/**
* This method closes all active statements and cleans resources.
*
* @throws SQLException
* if at least one of the active statements failed to close gracefully.
*/
protected void freeStatements() throws SQLException {
// copy statements to avoid concurrent modification exception
List statements = new ArrayList<>(activeStatements);
// iterate through the set, close statements and collect exceptions
var chain = new SQLExceptionChainBuilder();
for (Statement stmt : statements) {
try {
stmt.close();
} catch (SQLException ex) {
chain.append(ex);
}
}
chain.throwIfPresent();
}
/**
* Set the {@link FBManagedConnection} around which this connection is
* based.
*
* @param mc
* The FBManagedConnection around which this connection is based
*/
public void setManagedConnection(FBManagedConnection mc) {
if (mc == null && this.mc == null) return;
try (LockCloseable ignored = withLock()) {
//close any prepared statements we may have executed.
if (this.mc != mc && metaData != null) {
try {
metaData.close();
} finally {
metaData = null;
}
}
this.mc = mc;
}
}
public FBManagedConnection getManagedConnection() {
try (LockCloseable ignored = withLock()) {
return mc;
}
}
@Override
public FbDatabase getFbDatabase() throws SQLException {
return getGDSHelper().getCurrentDatabase();
}
/**
* Get database connection properties for this connection.
*
* @return immutable instance of {@link DatabaseConnectionProperties}.
*/
public DatabaseConnectionProperties connectionProperties() {
// TODO Obtain elsewhere than from getConnectionRequestInfo()
return mc != null ? mc.getConnectionRequestInfo().asIConnectionProperties().asImmutable() : null;
}
@Deprecated(since = "2")
@Override
public void setTransactionParameters(int isolationLevel, int[] parameters) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
TransactionParameterBuffer tpbParams = createTransactionParameterBuffer();
for (int parameter : parameters) {
tpbParams.addArgument(parameter);
}
setTransactionParameters(isolationLevel, tpbParams);
}
}
@Override
public TransactionParameterBuffer getTransactionParameters(int isolationLevel) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return mc.getTransactionParameters(isolationLevel);
}
}
@Override
public TransactionParameterBuffer createTransactionParameterBuffer() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return getFbDatabase().createTransactionParameterBuffer();
}
}
@Override
public void setTransactionParameters(int isolationLevel, TransactionParameterBuffer tpb) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (mc.isManagedEnvironment()) {
throw new SQLException("Cannot set transaction parameters in managed environment.",
SQL_STATE_GENERAL_ERROR);
}
mc.setTransactionParameters(isolationLevel, tpb);
}
}
@Override
public void setTransactionParameters(TransactionParameterBuffer tpb) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (getLocalTransaction().inTransaction()) {
// TODO More specific exception, jaybird error code
throw new SQLException("Cannot set transaction parameters when transaction is already started.",
SQL_STATE_TX_ACTIVE);
}
mc.setTransactionParameters(tpb);
}
}
@Override
public Statement createStatement() throws SQLException {
return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
}
@Override
public Blob createBlob() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return createBlob(FBBlob.createConfig(ISCConstants.BLOB_SUB_TYPE_BINARY, connectionProperties(),
getFbDatabase().getDatatypeCoder()));
}
}
private FBBlob createBlob(FBBlob.Config blobConfig) throws SQLException {
return new FBBlob(getGDSHelper(), txCoordinator, blobConfig);
}
@Override
public Clob createClob() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
FBBlob blob = createBlob(FBBlob.createConfig(ISCConstants.BLOB_SUB_TYPE_TEXT, connectionProperties(),
getFbDatabase().getDatatypeCoder()));
return new FBClob(blob);
}
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
checkValidity();
throw new FBDriverNotCapableException("Type STRUCT not supported");
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
checkValidity();
throw new FBDriverNotCapableException("Type ARRAY not yet supported");
}
@Override
public String nativeSQL(String sql) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return FBEscapedParser.toNativeSql(sql);
}
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (getAutoCommit() == autoCommit) {
return;
}
// FIXME : Behavior in switch might be wrong, see also setSavePoint
txCoordinator.switchTransactionCoordinator(autoCommit);
}
}
protected void setTransactionCoordinator(boolean managedConnection, boolean autoCommit) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
txCoordinator.setTransactionCoordinator(managedConnection, autoCommit);
}
}
public void setManagedEnvironment(boolean managedConnection) throws SQLException {
try (LockCloseable ignored = withLock()) {
setTransactionCoordinator(managedConnection, true);
}
}
@Override
public boolean getAutoCommit() throws SQLException {
try (LockCloseable ignored = withLock()) {
if (isClosed()) {
throw new SQLNonTransientConnectionException(
"You cannot getAutoCommit on an unassociated closed connection.", SQL_STATE_CONNECTION_CLOSED);
}
return txCoordinator.getAutoCommit();
}
}
@Override
public void commit() throws SQLException {
try (LockCloseable ignored = withLock()) {
if (isClosed()) {
throw new SQLNonTransientConnectionException("You cannot commit a closed connection.",
SQL_STATE_CONNECTION_CLOSED);
}
if (mc.inDistributedTransaction()) {
throw connectionEnlistedInDistributedTransaction();
}
txCoordinator.commit();
invalidateTransactionLifetimeObjects();
}
}
private static SQLException connectionEnlistedInDistributedTransaction() {
return new SQLException("Connection enlisted in distributed transaction", SQL_STATE_INVALID_TX_STATE);
}
@Override
public void rollback() throws SQLException {
try (LockCloseable ignored = withLock()) {
if (isClosed()) {
throw new SQLNonTransientConnectionException("You cannot rollback closed connection.",
SQL_STATE_CONNECTION_CLOSED);
}
if (mc.inDistributedTransaction()) {
throw connectionEnlistedInDistributedTransaction();
}
txCoordinator.rollback();
invalidateTransactionLifetimeObjects();
}
}
/**
* Invalidate everything that should only last for the lifetime of the current transaction.
*/
protected void invalidateTransactionLifetimeObjects() {
invalidateSavepoints();
storedProcedureMetaData = null;
}
boolean isAllowTxStmts() {
DatabaseConnectionProperties props = connectionProperties();
return props != null && props.isAllowTxStmts();
}
void handleHardCommitStatement() throws SQLException {
if (isAllowTxStmts()) {
commit();
} else {
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_commitStatementNotAllowed)
.toSQLException();
}
}
void handleHardRollbackStatement() throws SQLException {
if (isAllowTxStmts()) {
rollback();
} else {
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_rollbackStatementNotAllowed)
.toSQLException();
}
}
void handleSetTransactionStatement(String sql) throws SQLException {
if (isAllowTxStmts()) {
txCoordinator.startSqlTransaction(sql);
} else {
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_setTransactionStatementNotAllowed)
.toSQLException();
}
}
/**
* {@inheritDoc}
*
* Implementation note: Certain fatal errors also result in a closed Connection.
*
*/
@Override
public void close() throws SQLException {
if (isClosed()) return;
if (log.isLoggable(TRACE)) {
log.log(TRACE, "Connection closed requested at", new RuntimeException("Connection close logging"));
}
var chainBuilder = new SQLExceptionChainBuilder();
try (LockCloseable ignored = withLock()) {
try {
if (metaData != null) metaData.close();
freeStatements();
} catch (SQLException e) {
chainBuilder.append(e);
} finally {
metaData = null;
closeMc(chainBuilder);
}
}
chainBuilder.throwIfPresent();
}
private void closeMc(SQLExceptionChainBuilder chainBuilder) {
FBManagedConnection mc = this.mc;
if (mc == null) return;
// leave managed transactions alone, they are normally committed after the Connection handle is closed.
if (!mc.inDistributedTransaction()) {
try {
txCoordinator.handleConnectionClose();
} catch (SQLException e) {
chainBuilder.append(e);
} finally {
try {
setAutoCommit(true);
} catch (SQLException e) {
if (!SQL_STATE_CONNECTION_CLOSED.equals(e.getSQLState())) {
chainBuilder.append(e);
}
}
}
}
mc.close(this);
}
@Override
public boolean isClosed() {
return mc == null;
}
@Override
public boolean isValid(int timeout) throws SQLException {
if (timeout < 0) {
throw new SQLException("Timeout should be >= 0", SQLStateConstants.SQL_STATE_INVALID_ATTR_VALUE);
}
if (isLockedByCurrentThread()) {
// Trying to async check validity will not work (this shouldn't normally happen, except maybe when Jaybird
// internals call isValid(int) or user code locks on result of withLock())
return isValidImpl(timeout);
}
return isValidAsync(timeout);
}
private boolean isValidAsync(int timeout) {
Future isValidFuture = ForkJoinPool.commonPool().submit(() -> isValidImpl(timeout));
try {
return timeout != 0 ? isValidFuture.get(timeout, TimeUnit.SECONDS) : isValidFuture.get();
} catch (ExecutionException e) {
log.log(DEBUG, "isValidImpl produced an exception", e);
return false;
} catch (InterruptedException e) {
isValidFuture.cancel(true);
// restore interrupted state
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
isValidFuture.cancel(true);
return false;
}
}
@SuppressWarnings({ "ReturnInsideFinallyBlock", "java:S1141", "java:S1143" })
private boolean isValidImpl(int timeout) {
try (LockCloseable ignored = withLock()) {
if (isClosed()) {
return false;
}
int originalNetworkTimeout = -1;
boolean networkTimeoutChanged = false;
try {
FbDatabase db = getFbDatabase();
if (timeout != 0) {
try {
originalNetworkTimeout = db.getNetworkTimeout();
db.setNetworkTimeout((int) TimeUnit.SECONDS.toMillis(timeout));
networkTimeoutChanged = true;
} catch (SQLFeatureNotSupportedException ignored2) {
// Implementation doesn't support network timeout
}
}
db.getDatabaseInfo(new byte[] { ISCConstants.isc_info_ods_version, ISCConstants.isc_info_end }, 10);
return true;
} catch (SQLException ex) {
log.log(DEBUG, "Exception while checking connection validity", ex);
return false;
} finally {
if (networkTimeoutChanged) {
try {
getFbDatabase().setNetworkTimeout(originalNetworkTimeout);
} catch (SQLException e) {
log.log(DEBUG, "Exception while resetting connection network timeout", e);
// We're interpreting this as an indication the connection is no longer valid
return false;
}
}
}
}
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (metaData == null)
metaData = new FBDatabaseMetaData(this);
return metaData;
}
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (getLocalTransaction().inTransaction() && !mc.isManagedEnvironment()) {
// TODO More specific exception, jaybird error code
throw new SQLException(
"Calling setReadOnly(boolean) method is not allowed when transaction is already started.",
SQL_STATE_TX_ACTIVE);
}
this.readOnly = readOnly;
mc.setTpbReadOnly(readOnly);
}
}
@Override
public boolean isReadOnly() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return readOnly;
}
}
/**
* {@inheritDoc}
*
* Implementation ignores calls to this method as catalogs are not supported.
*
*/
@Override
public void setCatalog(String catalog) throws SQLException {
checkValidity();
}
/**
* {@inheritDoc}
*
* @return Always {@code null} as catalogs are not supported.
*/
@Override
@SuppressWarnings("java:S4144")
public String getCatalog() throws SQLException {
checkValidity();
return null;
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (!getAutoCommit() && !mc.isManagedEnvironment()) {
txCoordinator.commit();
}
mc.setTransactionIsolation(level);
}
}
@SuppressWarnings("MagicConstant")
@Override
public int getTransactionIsolation() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return mc.getTransactionIsolation();
}
}
/**
* {@inheritDoc}
*
* If connection property {@code reportSQLWarnings} is set to {@code NONE} (case-insensitive), this method will
* not report warnings and always return {@code null}.
*
*/
@Override
public SQLWarning getWarnings() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
return firstWarning;
}
}
@Override
public void clearWarnings() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
firstWarning = null;
}
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return createStatement(resultSetType, resultSetConcurrency, this.resultSetHoldability);
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
var stmt = new FBStatement(this,
toResultSetBehavior(resultSetType, resultSetConcurrency, resultSetHoldability), txCoordinator);
activeStatements.add(stmt);
return stmt;
}
}
private ResultSetBehavior toResultSetBehavior(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException {
return ResultSetBehavior.of(resultSetType, resultSetConcurrency, resultSetHoldability, this::addWarning);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
throws SQLException {
return prepareStatement(sql, resultSetType, resultSetConcurrency, this.resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
return prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability, false, false);
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
GeneratedKeysSupport.Query query = getGeneratedKeysSupport()
.buildQuery(sql, autoGeneratedKeys);
return prepareStatement(query);
}
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
GeneratedKeysSupport.Query query = getGeneratedKeysSupport()
.buildQuery(sql, columnIndexes);
return prepareStatement(query);
}
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
GeneratedKeysSupport.Query query = getGeneratedKeysSupport()
.buildQuery(sql, columnNames);
return prepareStatement(query);
}
}
/**
* Prepares a statement for generated keys.
*
* @param query
* AbstractGeneratedKeysQuery instance
* @return PreparedStatement object
* @throws SQLException
* if a database access error occurs or this method is called on a closed connection
*/
private PreparedStatement prepareStatement(GeneratedKeysSupport.Query query) throws SQLException {
if (query.generatesKeys()) {
return prepareStatement(query.getQueryString(),
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_AT_COMMIT,
false, true);
} else {
return prepareStatement(query.getQueryString());
}
}
protected PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability, boolean metaData, boolean generatedKeys) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
ResultSetBehavior rsBehavior =
toResultSetBehavior(resultSetType, resultSetConcurrency, resultSetHoldability);
Optional txStmt = prepareIfTransactionStatement(sql, rsBehavior);
if (txStmt.isPresent()) return txStmt.get();
FBObjectListener.StatementListener coordinator = txCoordinator;
FBObjectListener.BlobListener blobCoordinator = txCoordinator;
if (metaData) {
coordinator = new MetaDataTransactionCoordinator(txCoordinator);
blobCoordinator = null;
}
var stmt = new FBPreparedStatement(this, sql, rsBehavior, coordinator, blobCoordinator, metaData, false,
generatedKeys);
activeStatements.add(stmt);
return stmt;
}
}
/**
* Prepares the statement if {@code sql} is a transaction management.
*
* The result set type, concurrency and holdability are only used to let the {@link PreparedStatement} report
* the requested values. Given transaction management statements have no result set, they are otherwise unused.
*
*
* @param sql
* statement text
* @param rsBehavior
* result set behavior
* @return non-empty with a prepared statement which can execute the equivalent of {@code sql} if {@code sql}
* contains a transaction management statement, empty if the statement was not a transaction management statement
* @throws SQLException
* if {@code sql} was a transaction management statement, but connection configuration does not allow
* execution of transaction management statements
* @since 6
*/
private Optional prepareIfTransactionStatement(String sql, ResultSetBehavior rsBehavior)
throws SQLException {
LocalStatementType localStatementType = StatementDetector.determineLocalStatementType(sql);
if (localStatementType.statementClass() == LocalStatementClass.TRANSACTION_BOUNDARY) {
PreparedStatement stmt = prepareTxStatement(sql, localStatementType, rsBehavior);
activeStatements.add(stmt);
return Optional.of(stmt);
}
return Optional.empty();
}
/**
* If execution of transaction statements is allowed, this creates an appropriate prepared statement instance to
* perform the equivalent of the requested operation when executed.
*
* The result set type, concurrency and holdability are only used to let the {@link PreparedStatement} report
* the requested values. Given transaction management statements have no result set, they are otherwise unused.
*
*
* @param sql
* statement text
* @param statementType
* statement type for {@code sql} as previous determined
* @param rsBehavior
* result set behavior
* @return a prepared statement handle which can execute the equivalent of {@code sql} if {@code sql} contains
* a transaction management statement
* @throws SQLException
* if the connection configuration does not allow execution of transaction management statements
* @since 6
*/
private PreparedStatement prepareTxStatement(String sql, LocalStatementType statementType,
ResultSetBehavior rsBehavior) throws SQLException {
if (isAllowTxStmts()) {
return new FBTxPreparedStatement(this, statementType, sql, rsBehavior);
}
int errorCode = switch (statementType) {
case HARD_COMMIT -> JaybirdErrorCodes.jb_commitStatementNotAllowed;
case HARD_ROLLBACK -> JaybirdErrorCodes.jb_rollbackStatementNotAllowed;
case SET_TRANSACTION -> JaybirdErrorCodes.jb_setTransactionStatementNotAllowed;
default -> throw new IllegalArgumentException("Unexpected statementType: " + statementType);
};
throw FbExceptionBuilder.forNonTransientException(errorCode).toSQLException();
}
@Override
public CallableStatement prepareCall(String sql,
int resultSetType, int resultSetConcurrency) throws SQLException {
return prepareCall(sql, resultSetType, resultSetConcurrency, this.resultSetHoldability);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
// With the current implementation of FBCallableStatement, transaction statements would fail, but we
// explicitly don't allow them, even if FBCallableStatement were to change so execution could work.
FBStatement.rejectIfTxStmt(sql, JaybirdErrorCodes.jb_prepareCallWithTxStmt);
ResultSetBehavior rsBehavior =
toResultSetBehavior(resultSetType, resultSetConcurrency, resultSetHoldability);
if (rsBehavior.isUpdatable()) {
addWarning(FbExceptionBuilder
.forWarning(JaybirdErrorCodes.jb_concurrencyResetReadOnlyReasonStoredProcedure)
.toSQLException(SQLWarning.class));
rsBehavior = rsBehavior.withReadOnly();
}
if (storedProcedureMetaData == null) {
storedProcedureMetaData = StoredProcedureMetaDataFactory.getInstance(this);
}
var stmt = new FBCallableStatement(this, sql, rsBehavior, storedProcedureMetaData, txCoordinator,
txCoordinator);
activeStatements.add(stmt);
return stmt;
}
}
@Override
public Map> getTypeMap() throws SQLException {
return new HashMap<>();
}
@Override
public void setTypeMap(Map> map) throws SQLException {
throw new FBDriverNotCapableException();
}
private static final AtomicIntegerFieldUpdater SAVEPOINT_COUNTER_UPDATE =
AtomicIntegerFieldUpdater.newUpdater(FBConnection.class, "savepointCounter");
@SuppressWarnings("java:S1068")
private volatile int savepointCounter;
private final List savepoints = new ArrayList<>();
private int getNextSavepointCounter() {
return SAVEPOINT_COUNTER_UPDATE.getAndIncrement(this);
}
@Override
public Savepoint setSavepoint() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
FBSavepoint savepoint = new FBSavepoint(getNextSavepointCounter());
setSavepoint(savepoint);
return savepoint;
}
}
/**
* Set the savepoint on the server.
*
* @param savepoint
* savepoint to set
* @throws SQLException
* if something went wrong
*/
private void setSavepoint(FBSavepoint savepoint) throws SQLException {
if (getAutoCommit()) {
throw new SQLException("Connection.setSavepoint() method cannot be used in auto-commit mode",
SQL_STATE_INVALID_TX_STATE);
}
if (mc.inDistributedTransaction()) {
throw connectionEnlistedInDistributedTransaction();
}
txCoordinator.ensureTransaction();
getGDSHelper().executeImmediate(savepoint.toSavepointStatement(getQuoteStrategy()));
savepoints.add(savepoint);
}
/**
* Creates a named savepoint.
*
* Savepoint names need to be valid Firebird identifiers, and the maximum length is restricted to the maximum
* identifier length (see {@link DatabaseMetaData#getMaxColumnNameLength()}. The implementation will take care of
* quoting the savepoint name appropriately for the connection dialect. The {@code name} should be passed unquoted.
*
*
* With connection dialect 1, the name is restricted to the rules for unquoted identifier names, that is, its
* characters are restricted to {@code A-Za-z0-9$_} and handled case insensitive.
*
*
* For dialect 2 and 3, the name is restricted to the rules for Firebird quoted identifiers (essentially any
* printable character and space is valid), and the name is handled case sensitive.
*
*
* @param name
* Savepoint name
* @return Savepoint object
* @throws SQLException
* if a database access error occurs, this method is called while participating in a distributed
* transaction, this method is called on a closed connection or this {@code Connection} object is currently
* in auto-commit mode
*/
@Override
public Savepoint setSavepoint(String name) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
FBSavepoint savepoint = new FBSavepoint(name);
setSavepoint(savepoint);
return savepoint;
}
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (getAutoCommit()) {
throw new SQLException("Connection.rollback(Savepoint) method cannot be used in auto-commit mode",
SQL_STATE_INVALID_TX_STATE);
} else if (mc.inDistributedTransaction()) {
throw connectionEnlistedInDistributedTransaction();
}
FBSavepoint fbSavepoint = validateSavepoint(savepoint);
getGDSHelper().executeImmediate(fbSavepoint.toRollbackStatement(getQuoteStrategy()));
}
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (getAutoCommit()) {
throw new SQLException("Connection.releaseSavepoint() method cannot be used in auto-commit mode",
SQL_STATE_INVALID_TX_STATE);
}
FBSavepoint fbSavepoint = validateSavepoint(savepoint);
getGDSHelper().executeImmediate(fbSavepoint.toReleaseStatement(getQuoteStrategy()));
savepoints.remove(fbSavepoint);
}
}
/**
* Invalidate all savepoints.
*/
protected void invalidateSavepoints() {
try (LockCloseable ignored = withLock()) {
savepoints.clear();
}
}
FBSavepoint validateSavepoint(Savepoint savepoint) throws SQLException {
// TODO The error message and actual condition do not match
if (!(savepoint instanceof FBSavepoint fbSavepoint)) {
throw new SQLException("Specified savepoint was not obtained from this connection");
}
if (!savepoints.contains(fbSavepoint)) {
throw new SQLException("Savepoint is no longer valid");
}
return fbSavepoint;
}
/**
* Returns a FBLocalTransaction instance that enables a component to
* demarcate resource manager local transactions on this connection.
*/
public FBLocalTransaction getLocalTransaction() {
try (LockCloseable ignored = withLock()) {
if (localTransaction == null) {
localTransaction = mc.getLocalTransaction();
}
return localTransaction;
}
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return iface != null && iface.isAssignableFrom(FBConnection.class);
}
@Override
public T unwrap(Class iface) throws SQLException {
if (!isWrapperFor(iface))
throw new SQLException("Unable to unwrap to class " + iface.getName());
return iface.cast(this);
}
/**
* {@inheritDoc}
*
* Implementation ignores calls to this method as schemas are not supported.
*
*/
@Override
public void setSchema(String schema) throws SQLException {
// Ignore: no schema support
checkValidity();
}
/**
* {@inheritDoc}
*
* @return Always {@code null} as schemas ar not supported
*/
@Override
@SuppressWarnings("java:S4144")
public String getSchema() throws SQLException {
checkValidity();
return null;
}
public void addWarning(SQLWarning warning) {
try (LockCloseable ignored = withLock()) {
if (isIgnoreSQLWarnings()) return;
// TODO: Find way so this method can be protected (or less visible) again.
if (firstWarning == null)
firstWarning = warning;
else {
firstWarning.setNextWarning(warning);
}
}
}
/**
* {@inheritDoc}
*
* Implementation note: This method behaves exactly the same as {@link #createClob()}.
*
*/
@Override
public NClob createNClob() throws SQLException {
return (NClob) createClob();
}
@Override
public SQLXML createSQLXML() throws SQLException {
checkValidity();
throw new FBDriverNotCapableException("Type SQLXML not supported");
}
public GDSHelper getGDSHelper() throws SQLException {
if (mc == null)
// TODO Right error code?
throw FbExceptionBuilder.forException(ISCConstants.isc_req_no_trans).toSQLException();
return mc.getGDSHelper();
}
@Override
public boolean isUseFirebirdAutoCommit() {
DatabaseConnectionProperties props = connectionProperties();
return props != null && props.isUseFirebirdAutocommit();
}
/**
* Returns, and if necessary creates, an instance of ClientInfoProvider for this connection.
*
* @return client info provider instance
* @throws SQLFeatureNotSupportedException
* if {@code connection} is to a Firebird version which does not support RDB$GET/SET_CONTEXT
* @throws SQLException
* if {@code connection} is not valid (e.g. closed)
*/
ClientInfoProvider getClientInfoProvider() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
ClientInfoProvider clientInfoProvider = this.clientInfoProvider;
if (clientInfoProvider != null) return clientInfoProvider;
return this.clientInfoProvider = new ClientInfoProvider(this);
}
}
/**
* {@inheritDoc}
*
* Retrieves the known properties of this connection. The initial known properties are {@code ApplicationName} (with
* fallback to {@code CLIENT_PROCESS@SYSTEM}), {@code ClientUser} (no default value or fallback), and
* {@code ClientHostname} (no default value or fallback). Successful retrieval or storing of properties with
* {@link #getClientInfo(String)}, {@link #setClientInfo(String, String)} and {@link #setClientInfo(Properties)}
* will register that property as a known property for this connection only.
*
*
* When auto-commit is enabled, known properties in context {@code USER_TRANSACTION} are skipped, and not included
* in the returned {@code Properties} object.
*
*
* Properties which were registered with suffix {@code @USER_SESSION} are included in the returned
* {@code Properties} object without that suffix. Known properties with value {@code null} are not
* included in the returned {@code Properties} object.
*
*
* @see #getClientInfo(String)
* @see #setClientInfo(Properties)
*/
@Override
public Properties getClientInfo() throws SQLException {
try (LockCloseable ignored = withLock()) {
return getClientInfoProvider().getClientInfo();
}
}
/**
* {@inheritDoc}
*
* Retrieves properties from context {@code USER_SESSION} or an explicitly specified context with
* {@code RDB$GET_CONTEXT}. Names ending in {@code @USER_SESSION}, {@code @USER_TRANSACTION} or {@code @SYSTEM} are
* handled as {@code @}, and the property is retrieved from that context. Bare names
* are handled the same as {@code name + "@USER_SESSION"}, unknown or unsupported context suffixes are handled as a
* property name in {@code USER_SESSION} (that is {@code DDL_EVENT@DDL_TRIGGER} is handled as a property with that
* name in {@code USER_SESSION}, and not as a property DDL_EVENT in context DDL_TRIGGER, which only exists in
* PSQL DDL triggers).
*
*
* When auto-commit is enabled, properties in context {@code USER_TRANSACTION} will always return {@code null},
* and no attempts are made to retrieve the property.
*
*
* Successful retrieval of a property will register it as a known property for this connection only for use
* with {@link #getClientInfo()} (i.e. known properties will be retrieved) and {@link #setClientInfo(Properties)}
* (i.e. if a known property is not included, it will be cleared).
*
*/
@Override
public String getClientInfo(String name) throws SQLException {
try (LockCloseable ignored = withLock()) {
return getClientInfoProvider().getClientInfo(name);
}
}
/**
* {@inheritDoc}
*
* Sets the properties in {@code properties} in context {@code USER_SESSION} or an explicitly specified user context
* with {@code RDB$SET_CONTEXT}. See also {@link #getClientInfo(String)} for handling of property names. Absent, but
* known properties are cleared. Contrary to {@link #setClientInfo(String, String)}, properties in the
* {@code SYSTEM} context are silently ignored as they are read-only.
*
*
* When auto-commit is enabled, properties in context {@code USER_TRANSACTION} are silently ignored (they are not
* set nor cleared).
*
*
* Successful setting of properties will register it as a known property for this connection only for use
* with {@link #getClientInfo()} (i.e. known properties will be retrieved) and {@link #setClientInfo(Properties)}
* (i.e. if a known property is not included, it will be cleared).
*
*/
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
try (LockCloseable ignored = withLock()) {
getClientInfoProvider().setClientInfo(properties);
} catch (SQLClientInfoException e) {
throw e;
} catch (SQLException e) {
Map failedProperties = properties.stringPropertyNames().stream()
.collect(toMap(Function.identity(), name -> ClientInfoStatus.REASON_UNKNOWN));
throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(), failedProperties, e);
}
}
/**
* {@inheritDoc}
*
* Sets properties in context {@code USER_SESSION} or an explicitly specified user context with
* {@code RDB$SET_CONTEXT}. See also {@link #getClientInfo(String)} for handling of property names.
*
*
* When auto-commit is enabled, properties in context {@code USER_TRANSACTION} will not be set.
*
*
* Successful storing of a property will register it as a known property for this connection only for use
* with {@link #getClientInfo()} (i.e. known properties will be retrieved) and {@link #setClientInfo(Properties)}
* (i.e. if a known property is not included, it will be cleared).
*
*
* @param name
* name of the client info property to set (cannot be a name ending in {@code @SYSTEM} as those are
* read-only)
* @throws SQLClientInfoException
* if {@code name} is {@code null} or ends in {@code @SYSTEM}, or for database access errors
* @see #getClientInfo(String)
*/
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
try (LockCloseable ignored = withLock()) {
getClientInfoProvider().setClientInfo(name, value);
} catch (SQLClientInfoException e) {
throw e;
} catch (SQLException e) {
throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(),
Map.of(name, ClientInfoStatus.REASON_UNKNOWN), e);
}
}
@Override
public void resetKnownClientInfoProperties() {
try (LockCloseable ignored = withLock()) {
if (isClosed()) return;
ClientInfoProvider clientInfoProvider = this.clientInfoProvider;
if (clientInfoProvider != null) {
clientInfoProvider.resetKnownProperties();
}
}
}
@Override
@SuppressWarnings("java:S1141")
public void abort(Executor executor) throws SQLException {
// NOTE: None of these operations are performed under lock (except maybe very late in the cleanup)
if (isClosed()) return;
PERMISSION_CALL_ABORT.checkGuard(this);
if (executor == null) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_invalidExecutor).toSQLException();
}
final FbDatabase fbDatabase;
try {
fbDatabase = getFbDatabase();
} finally {
metaData = null;
FBManagedConnection mc = this.mc;
if (mc != null) {
// Avoid lock in Connection.setManagedConnection during mc.close
this.mc = null;
mc.close(this);
}
}
executor.execute(() -> {
try {
try {
// Try to close statements and result sets
txCoordinator.handleConnectionAbort();
} finally {
try {
// should forcibly close connection
fbDatabase.cancelOperation(fb_cancel_abort);
} catch (SQLException e) {
log.log(DEBUG, "Failed to raise abort on FbDatabase", e);
}
}
} catch (SQLException e) {
log.log(DEBUG, "txCoordinator.handleConnectionAbort() caused an exception", e);
} finally {
try {
// alternative attempt to forcibly close connection
fbDatabase.forceClose();
} catch (SQLException e) {
log.log(DEBUG, "Could not forceClose FbDatabase on abort", e);
}
}
});
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
PERMISSION_SET_NETWORK_TIMEOUT.checkGuard(this);
if (executor == null) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_invalidExecutor).toSQLException();
}
if (milliseconds < 0) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_invalidTimeout).toSQLException();
}
try (LockCloseable ignored = withLock()) {
checkValidity();
getFbDatabase().setNetworkTimeout(milliseconds);
}
}
@Override
public int getNetworkTimeout() throws SQLException {
return getFbDatabase().getNetworkTimeout();
}
/**
* @see org.firebirdsql.gds.ng.FbAttachment#withLock()
*/
protected final LockCloseable withLock() {
FBManagedConnection mc = this.mc;
if (mc != null) {
return mc.withLock();
}
return LockCloseable.NO_OP;
}
protected final boolean isLockedByCurrentThread() {
FBManagedConnection mc = this.mc;
if (mc != null) {
return mc.isLockedByCurrentThread();
}
return false;
}
/**
* Get the quote strategy appropriate for the dialect of this connection.
*
* @return Quote strategy
* @throws SQLException
* If the connection is closed
*/
QuoteStrategy getQuoteStrategy() throws SQLException {
return QuoteStrategy.forDialect(getGDSHelper().getDialect());
}
GeneratedKeysSupport getGeneratedKeysSupport() throws SQLException {
if (generatedKeysSupport == null) {
generatedKeysSupport = GeneratedKeysSupportFactory
.createFor(getGeneratedKeysEnabled(), (FirebirdDatabaseMetaData) getMetaData());
}
return generatedKeysSupport;
}
private String getGeneratedKeysEnabled() {
DatabaseConnectionProperties props = connectionProperties();
return props != null ? props.getGeneratedKeysEnabled() : null;
}
boolean isIgnoreProcedureType() {
DatabaseConnectionProperties props = connectionProperties();
return props != null && props.isIgnoreProcedureType();
}
/**
* Checks (case-insensitive) value of the {@code scrollableCursor} connection property.
*
* Important: this does not verify actual server support for the requested feature, just the value of
* the connection property.
*
*
* @param scrollableCursor
* Value to check (case-insensitive)
* @return {@code true} if the {@code scrollableCursor} connection property matches the specified value,
* {@code false} otherwise.
*/
boolean isScrollableCursor(String scrollableCursor) {
DatabaseConnectionProperties props = connectionProperties();
return props != null && scrollableCursor != null
&& scrollableCursor.equalsIgnoreCase(props.getScrollableCursor());
}
boolean isUseServerBatch() {
DatabaseConnectionProperties props = connectionProperties();
return props != null && props.isUseServerBatch();
}
int getServerBatchBufferSize() {
DatabaseConnectionProperties props = connectionProperties();
return props != null ? props.getServerBatchBufferSize() : PropertyConstants.DEFAULT_SERVER_BATCH_BUFFER_SIZE;
}
boolean isExtendedMetadata() {
DatabaseConnectionProperties props = connectionProperties();
return props != null && props.isExtendedMetadata();
}
boolean isIgnoreSQLWarnings() {
DatabaseConnectionProperties props = connectionProperties();
return props != null
&& PropertyConstants.REPORT_SQL_WARNINGS_NONE.equalsIgnoreCase(props.getReportSQLWarnings());
}
}