diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc index 5c88556cb..e857a7df8 100644 --- a/src/docs/asciidoc/release_notes.adoc +++ b/src/docs/asciidoc/release_notes.adoc @@ -1034,6 +1034,7 @@ Given this is a non-standard extension, it is advisable to retrieve these column + To be clear, Jaybird does not provide statement pooling. This is change is only about returning and recording the poolable information for JDBC compliance, so it can be used by -- for example -- connection pool implementations that provide statement pooling. +* The state of `Connection.setReadOnly(boolean)` was not retained after calling `Connection.setTransactionIsolation(int)` or other method calls that changed the current transaction parameter buffer (https://github.com/FirebirdSQL/jaybird/issues/805[#805]) [#potentially-breaking-changes] === Potentially breaking changes @@ -1194,6 +1195,21 @@ Switch to using one of the normal execute methods. See also <>. +[#compat-read-only] +=== Read-only behaviour of connections + +In previous versions of Jaybird the read-only state of a connection was not retained if the transaction parameter buffer was replaced, for example by calls to `setTransactionIsolation(int)`. + +Now this has been corrected, it is possible that your code unexpectedly throws an exception with message "`__attempted update during read-only transaction [SQLState:42000, ISC error code:335544361]__`" (error `isc_read_only_trans`). + +You need to make sure to call `setReadOnly(false)` if the connection was previously marked read-only. +If you're using a connection pool, you need to ensure it properly resets the read-only state of the connection when checking in or checking out the connection. +For example, both Apache DBCP and Apache Tomcat connection pools requires the `defaultReadOnly` property to be set (i.e. to `false`), otherwise it will not reset the read-only state. + +If overridden transaction mappings are used, and the default isolation level has `isc_tpb_read`, the connection will be marked as read-only. +As a result, switching isolation levels will now also result in read-only transactions, even if the mapping of the other isolation level is defined with `isc_tpb_write`. +You will need to explicitly call `setReadOnly(false)`, or -- better yet -- do not override transaction mappings with a `isc_tpb_read`, but always use `isc_tpb_write`, and control read-only state only through `setReadOnly`. + // TODO Document compatibility issues [#removal-of-classes-packages-and-methods-without-deprecation] @@ -1576,7 +1592,11 @@ this package is internal API only, and not exported from the module (see also ea ** Constructor `SQLExceptionChainBuilder(SQLException)` was removed, as in practice this was never used; replacement is `new SQLExceptionChainBuilder().append(exception)` * `FBTpb` was removed, and its usages were replaced with `TransactionParameterBuffer` +* `FBTpbMapper` no longer implements `Cloneable`, use `FBTpbMapper.copyOf(FBTpbMapper)` instead * `ParameterBuffer` now extends `Serializable`, as all implementations are serializable, and some usages expect serializable behaviour even when the interfaces were used (though in practice, these objects are hardly ever serialized) +* `FBManagedConnection` +** `setReadOnly(boolean)` was renamed to `setTpbReadOnly(boolean)` to reflect what it actually does +** `isReadOnly()` was renamed to `isTpbReadOnly()` to reflect what it actually does [#breaking-changes-unlikely] === Unlikely breaking changes diff --git a/src/main/org/firebirdsql/gds/ParameterBuffer.java b/src/main/org/firebirdsql/gds/ParameterBuffer.java index d74e1c0a6..26c5f6e62 100644 --- a/src/main/org/firebirdsql/gds/ParameterBuffer.java +++ b/src/main/org/firebirdsql/gds/ParameterBuffer.java @@ -120,7 +120,7 @@ public interface ParameterBuffer extends Iterable, Serializable { void addArgument(int argumentType, byte[] content); /** - * Remove specified argument. + * Remove the first occurrence of the specified argument. * * @param argumentType * type of argument to remove. diff --git a/src/main/org/firebirdsql/gds/TransactionParameterBuffer.java b/src/main/org/firebirdsql/gds/TransactionParameterBuffer.java index 381720777..19a799f89 100644 --- a/src/main/org/firebirdsql/gds/TransactionParameterBuffer.java +++ b/src/main/org/firebirdsql/gds/TransactionParameterBuffer.java @@ -60,9 +60,18 @@ default void copyTo(TransactionParameterBuffer destination) { * @since 6 */ default void setReadOnly(boolean readOnly) { - removeArgument(isc_tpb_read); - removeArgument(isc_tpb_write); - addArgument(readOnly ? isc_tpb_read : isc_tpb_write); + if (readOnly) { + ensurePresentAbsent(isc_tpb_read, isc_tpb_write); + } else { + ensurePresentAbsent(isc_tpb_write, isc_tpb_read); + } + } + + private void ensurePresentAbsent(int present, int absent) { + if (!hasArgument(present)) { + addArgument(present); + } + removeArgument(absent); } /** @@ -88,9 +97,12 @@ default boolean isReadOnly() { * @since 6 */ default void setAutoCommit(boolean autoCommit) { - removeArgument(isc_tpb_autocommit); if (autoCommit) { - addArgument(isc_tpb_autocommit); + if (!hasArgument(isc_tpb_autocommit)) { + addArgument(isc_tpb_autocommit); + } + } else { + removeArgument(isc_tpb_autocommit); } } diff --git a/src/main/org/firebirdsql/gds/impl/ParameterBufferBase.java b/src/main/org/firebirdsql/gds/impl/ParameterBufferBase.java index 4c7162a84..9e69f0325 100644 --- a/src/main/org/firebirdsql/gds/impl/ParameterBufferBase.java +++ b/src/main/org/firebirdsql/gds/impl/ParameterBufferBase.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Optional; /** * Base class for parameter buffers @@ -45,7 +46,7 @@ public abstract class ParameterBufferBase implements ParameterBuffer, Serializab @Serial private static final long serialVersionUID = 8812835147477954476L; - + private final List arguments = new ArrayList<>(); private final String defaultEncodingName; @@ -98,72 +99,68 @@ public final void addArgument(int argumentType, String value) { @Override public final void addArgument(int argumentType, String value, Encoding encoding) { - getArgumentsList().add(new StringArgument(argumentType, parameterBufferMetaData.getStringArgumentType(argumentType), value, encoding)); + addArgument(new StringArgument( + argumentType, parameterBufferMetaData.getStringArgumentType(argumentType), value, encoding)); } @Override - public void addArgument(int argumentType, byte value) { - getArgumentsList().add(new ByteArgument(argumentType, parameterBufferMetaData.getByteArgumentType(argumentType), value)); + public final void addArgument(int argumentType, byte value) { + addArgument(new ByteArgument(argumentType, parameterBufferMetaData.getByteArgumentType(argumentType), value)); } @Override public final void addArgument(int argumentType, int value) { - getArgumentsList().add(new NumericArgument(argumentType, parameterBufferMetaData.getIntegerArgumentType(argumentType), value)); + addArgument(new NumericArgument( + argumentType, parameterBufferMetaData.getIntegerArgumentType(argumentType), value)); } @Override public final void addArgument(int argumentType, long value) { - getArgumentsList().add(new BigIntArgument(argumentType, parameterBufferMetaData.getIntegerArgumentType(argumentType), value)); + addArgument(new BigIntArgument( + argumentType, parameterBufferMetaData.getIntegerArgumentType(argumentType), value)); } @Override public final void addArgument(int argumentType) { - getArgumentsList().add(new SingleItem(argumentType, parameterBufferMetaData.getSingleArgumentType(argumentType))); + addArgument(new SingleItem(argumentType, parameterBufferMetaData.getSingleArgumentType(argumentType))); } @Override public final void addArgument(int type, byte[] content) { - getArgumentsList().add(new ByteArrayArgument(type, parameterBufferMetaData.getByteArrayArgumentType(type), content)); + addArgument(new ByteArrayArgument(type, parameterBufferMetaData.getByteArrayArgumentType(type), content)); + } + + protected final void addArgument(Argument argument) { + arguments.add(argument); } @Override public final String getArgumentAsString(int type) { - final List argumentsList = getArgumentsList(); - for (final Argument argument : argumentsList) { - if (argument.getType() == type) { - return argument.getValueAsString(); - } - } - return null; + return findFirst(type).map(Argument::getValueAsString).orElse(null); } + @SuppressWarnings("OptionalIsPresent") @Override public final int getArgumentAsInt(int type) { - final List argumentsList = getArgumentsList(); - for (final Argument argument : argumentsList) { - if (argument.getType() == type) { - return argument.getValueAsInt(); - } - } - return 0; + Optional argumentOpt = findFirst(type); + return argumentOpt.isPresent() ? argumentOpt.get().getValueAsInt() : 0; } @Override public final boolean hasArgument(int type) { - final List argumentsList = getArgumentsList(); - for (final Argument argument : argumentsList) { - if (argument.getType() == type) return true; - } - return false; + return findFirst(type).isPresent(); + } + + protected Optional findFirst(int type) { + return arguments.stream().filter(argument -> argument.getType() == type).findFirst(); } @Override public final void removeArgument(int type) { - final List argumentsList = getArgumentsList(); - for (int i = 0, n = argumentsList.size(); i < n; i++) { - final Argument argument = argumentsList.get(i); - if (argument.getType() == type) { - argumentsList.remove(i); + Iterator argumentIterator = arguments.iterator(); + while (argumentIterator.hasNext()) { + if (argumentIterator.next().getType() == type) { + argumentIterator.remove(); return; } } @@ -175,7 +172,7 @@ public final Iterator iterator() { } public final void writeArgumentsTo(OutputStream outputStream) throws IOException { - for (final Argument currentArgument : arguments) { + for (Argument currentArgument : arguments) { currentArgument.writeTo(outputStream); } } @@ -186,9 +183,8 @@ public final Xdrable toXdrable() { } protected final int getLength() { - final List argumentsList = getArgumentsList(); int length = 0; - for (final Argument currentArgument : argumentsList) { + for (Argument currentArgument : arguments) { length += currentArgument.getLength(); } return length; @@ -200,7 +196,7 @@ protected final List getArgumentsList() { @Override public final byte[] toBytes() { - final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + var bout = new ByteArrayOutputStream(); try { writeArgumentsTo(bout); } catch (IOException e) { @@ -211,7 +207,7 @@ public final byte[] toBytes() { @Override public final byte[] toBytesWithType() { - final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final var bout = new ByteArrayOutputStream(); try { bout.write(getType()); writeArgumentsTo(bout); @@ -229,29 +225,30 @@ public final int size() { @Override @SuppressWarnings("java:S2097") public final boolean equals(Object other) { - if (other == null || !(this.getClass().isAssignableFrom(other.getClass()))) + if (other == null || !(this.getClass().isAssignableFrom(other.getClass()))) { return false; + } final ParameterBufferBase otherServiceBufferBase = (ParameterBufferBase) other; - return otherServiceBufferBase.getArgumentsList().equals(this.getArgumentsList()); + return otherServiceBufferBase.arguments.equals(this.arguments); } @Override public final int hashCode() { - return getArgumentsList().hashCode(); + return arguments.hashCode(); } /** * Default implementation for serializing the parameter buffer to the XDR output stream */ - private class ParameterBufferXdrable implements Xdrable { + private final class ParameterBufferXdrable implements Xdrable { @Override public int getLength() { return ParameterBufferBase.this.getLength(); } @Override - public void read(XdrInputStream inputStream, int length) throws IOException { + public void read(XdrInputStream inputStream, int length) { throw new UnsupportedOperationException(); } diff --git a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java index 54856b08e..00c8b74f6 100644 --- a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java +++ b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java @@ -1211,21 +1211,23 @@ public FBManagedConnectionFactory getManagedConnectionFactory() { } /** - * Set whether this connection is to be readonly + * Set the current TPB to read-only. * * @param readOnly - * If {@code true}, the connection will be set read-only, otherwise it will be writable + * if {@code true}, the connection will be set read-only, otherwise it will be writable + * @since 6 */ - public void setReadOnly(boolean readOnly) { + public void setTpbReadOnly(boolean readOnly) { tpb.setReadOnly(readOnly); } /** - * Retrieve whether this connection is readonly. + * Retrieve whether the current TPB is read-only. * * @return {@code true} if this connection is readonly, {@code false} otherwise + * @since 6 */ - public boolean isReadOnly() { + public boolean isTpbReadOnly() { return tpb.isReadOnly(); } diff --git a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnectionFactory.java b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnectionFactory.java index 227d621a0..c80af5865 100644 --- a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnectionFactory.java +++ b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnectionFactory.java @@ -323,7 +323,7 @@ public TransactionParameterBuffer getTpb(int isolation) throws SQLException { * For errors on obtaining or creating the transaction mapping */ FBTpbMapper getTransactionMappingCopy() throws SQLException { - return (FBTpbMapper) connectionProperties.getMapper().clone(); + return FBTpbMapper.copyOf(connectionProperties.getMapper()); } /** diff --git a/src/main/org/firebirdsql/jdbc/FBConnection.java b/src/main/org/firebirdsql/jdbc/FBConnection.java index ba295f890..a90a5f286 100644 --- a/src/main/org/firebirdsql/jdbc/FBConnection.java +++ b/src/main/org/firebirdsql/jdbc/FBConnection.java @@ -91,6 +91,7 @@ public class FBConnection implements FirebirdConnection { private StoredProcedureMetaData storedProcedureMetaData; private GeneratedKeysSupport generatedKeysSupport; private ClientInfoProvider clientInfoProvider; + private boolean readOnly; /** * Create a new FBConnection instance based on a {@link FBManagedConnection}. @@ -107,6 +108,9 @@ public FBConnection(FBManagedConnection mc) { 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 @@ -603,7 +607,8 @@ public void setReadOnly(boolean readOnly) throws SQLException { "Calling setReadOnly(boolean) method is not allowed when transaction is already started.", SQL_STATE_TX_ACTIVE); } - mc.setReadOnly(readOnly); + this.readOnly = readOnly; + mc.setTpbReadOnly(readOnly); } } @@ -611,7 +616,7 @@ public void setReadOnly(boolean readOnly) throws SQLException { public boolean isReadOnly() throws SQLException { try (LockCloseable ignored = withLock()) { checkValidity(); - return mc.isReadOnly(); + return readOnly; } } diff --git a/src/main/org/firebirdsql/jdbc/FBConnectionProperties.java b/src/main/org/firebirdsql/jdbc/FBConnectionProperties.java index 2cdcd36f6..baea75d38 100644 --- a/src/main/org/firebirdsql/jdbc/FBConnectionProperties.java +++ b/src/main/org/firebirdsql/jdbc/FBConnectionProperties.java @@ -108,7 +108,7 @@ public Object clone() { clone.properties = (FbConnectionProperties) properties.asNewMutable(); clone.properties.registerPropertyUpdateListener(clone.createPropertyUpdateListener()); clone.customMapping = new HashMap<>(customMapping); - clone.mapper = mapper != null ? (FBTpbMapper) mapper.clone() : null; + clone.mapper = mapper != null ? FBTpbMapper.copyOf(mapper) : null; return clone; } catch (CloneNotSupportedException ex) { diff --git a/src/main/org/firebirdsql/jdbc/FBTpbMapper.java b/src/main/org/firebirdsql/jdbc/FBTpbMapper.java index 0a1ddff09..e3a2fb7fb 100644 --- a/src/main/org/firebirdsql/jdbc/FBTpbMapper.java +++ b/src/main/org/firebirdsql/jdbc/FBTpbMapper.java @@ -39,11 +39,16 @@ * * @author Roman Rokytskyy */ -public final class FBTpbMapper implements Serializable, Cloneable { +public final class FBTpbMapper implements Serializable { @Serial private static final long serialVersionUID = 1690658870275668176L; + /** + * Creates a new instance with the default configuration. + * + * @return instance with the default mapper configuration + */ public static FBTpbMapper getDefaultMapper() { return new FBTpbMapper(); } @@ -117,7 +122,7 @@ public static int getTransactionIsolationLevel(String isolationName) { } // ConcurrentHashMap because changes can - potentially - be made concurrently - private Map mapping = new ConcurrentHashMap<>(); + private final Map mapping; private int defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED; /** @@ -126,25 +131,27 @@ public static int getTransactionIsolationLevel(String isolationName) { */ public FBTpbMapper() { // TODO instance creation should be delegated to FbDatabase or another factory - TransactionParameterBuffer serializableTpb = new TransactionParameterBufferImpl(); + var serializableTpb = new TransactionParameterBufferImpl(); serializableTpb.addArgument(isc_tpb_write); serializableTpb.addArgument(isc_tpb_wait); serializableTpb.addArgument(isc_tpb_consistency); - TransactionParameterBuffer repeatableReadTpb = new TransactionParameterBufferImpl(); + var repeatableReadTpb = new TransactionParameterBufferImpl(); repeatableReadTpb.addArgument(isc_tpb_write); repeatableReadTpb.addArgument(isc_tpb_wait); repeatableReadTpb.addArgument(isc_tpb_concurrency); - TransactionParameterBuffer readCommittedTpb = new TransactionParameterBufferImpl(); + var readCommittedTpb = new TransactionParameterBufferImpl(); readCommittedTpb.addArgument(isc_tpb_write); readCommittedTpb.addArgument(isc_tpb_wait); readCommittedTpb.addArgument(isc_tpb_read_committed); readCommittedTpb.addArgument(isc_tpb_rec_version); + var mapping = new ConcurrentHashMap(3); mapping.put(Connection.TRANSACTION_SERIALIZABLE, serializableTpb); mapping.put(Connection.TRANSACTION_REPEATABLE_READ, repeatableReadTpb); mapping.put(Connection.TRANSACTION_READ_COMMITTED, readCommittedTpb); + this.mapping = mapping; } /** @@ -182,10 +189,20 @@ public FBTpbMapper() { * @throws SQLException if mapping contains incorrect values. */ public FBTpbMapper(Map stringMapping) throws SQLException { + // Ensure defaults are populated this(); processMapping(stringMapping); } + private FBTpbMapper(FBTpbMapper source) { + var newMapping = new ConcurrentHashMap(source.mapping.size()); + for (Map.Entry entry : source.mapping.entrySet()) { + newMapping.put(entry.getKey(), entry.getValue().deepCopy()); + } + mapping = newMapping; + defaultIsolationLevel = source.defaultIsolationLevel; + } + /** * Process specified string mapping. This method updates default mapping with values specified in * a {@code stringMapping}. @@ -229,25 +246,25 @@ private static String unsupportedIsolationLevel(String jdbcTxIsolation) { * if resource cannot be loaded or contains incorrect values. */ public FBTpbMapper(String mappingResource, ClassLoader cl) throws SQLException { + // Ensure defaults are populated + this(); // Make sure the old documented 'res:' protocol works // TODO Remove in Jaybird 7 or later? if (mappingResource.startsWith("res:")) { mappingResource = mappingResource.substring(4); } try { - ResourceBundle res = ResourceBundle.getBundle(mappingResource, Locale.getDefault(), cl); + var res = ResourceBundle.getBundle(mappingResource, Locale.getDefault(), cl); - Map mapping = new HashMap<>(); + var mapping = new HashMap(); Enumeration en = res.getKeys(); while (en.hasMoreElements()) { String key = en.nextElement(); - String value = res.getString(key); - mapping.put(key, value); + mapping.put(key, res.getString(key)); } processMapping(mapping); - } catch (MissingResourceException mrex) { // TODO More specific exception, Jaybird error code throw new SQLException("Cannot load TPB mapping. " + mrex.getMessage(), mrex); @@ -322,9 +339,9 @@ public static void processMapping(FirebirdConnectionProperties connectionPropert */ public static TransactionParameterBuffer processMapping(String mapping) throws SQLException { // TODO instance creation should be delegated to FbDatabase - TransactionParameterBuffer result = new TransactionParameterBufferImpl(); + var result = new TransactionParameterBufferImpl(); - StringTokenizer st = new StringTokenizer(mapping, ","); + var st = new StringTokenizer(mapping, ","); while (st.hasMoreTokens()) { String token = st.nextToken(); Integer argValue = null; @@ -423,21 +440,8 @@ public int hashCode() { return Objects.hash(mapping, defaultIsolationLevel); } - public Object clone() { - try { - FBTpbMapper clone = (FBTpbMapper) super.clone(); - - var newMapping = new ConcurrentHashMap(); - for (Map.Entry entry : mapping.entrySet()) { - newMapping.put(entry.getKey(), entry.getValue().deepCopy()); - } - - clone.mapping = newMapping; - - return clone; - } catch (CloneNotSupportedException e) { - throw new AssertionError("clone() unexpectedly not supported", e); - } + public static FBTpbMapper copyOf(FBTpbMapper tpbMapper) { + return new FBTpbMapper(tpbMapper); } private static final class TpbMapping { @@ -448,8 +452,7 @@ private static final class TpbMapping { // Initialize mappings between TPB constant names and their values; should be executed only once. static { - final Map tempTpbTypes = new HashMap<>(64); - + final var tempTpbTypes = new HashMap(64); final Field[] fields = TpbItems.class.getFields(); for (Field field : fields) { diff --git a/src/main/org/firebirdsql/jdbc/InternalTransactionCoordinator.java b/src/main/org/firebirdsql/jdbc/InternalTransactionCoordinator.java index 3b21824d9..9db7a75cf 100644 --- a/src/main/org/firebirdsql/jdbc/InternalTransactionCoordinator.java +++ b/src/main/org/firebirdsql/jdbc/InternalTransactionCoordinator.java @@ -291,6 +291,7 @@ final void internalRollback() throws SQLException { public void ensureTransaction() throws SQLException { configureFirebirdAutoCommit(); + configureReadOnly(); if (!localTransaction.inTransaction()) { localTransaction.begin(); } @@ -303,6 +304,10 @@ private void configureFirebirdAutoCommit() throws SQLException { } } + private void configureReadOnly() throws SQLException { + connection.getManagedConnection().setTpbReadOnly(connection.isReadOnly()); + } + public abstract void commit() throws SQLException; public abstract void rollback() throws SQLException; diff --git a/src/test/org/firebirdsql/jdbc/FBConnectionTest.java b/src/test/org/firebirdsql/jdbc/FBConnectionTest.java index 281cec481..90f430639 100644 --- a/src/test/org/firebirdsql/jdbc/FBConnectionTest.java +++ b/src/test/org/firebirdsql/jdbc/FBConnectionTest.java @@ -1015,6 +1015,54 @@ void abortClosesStatementsAndResultSets() throws Exception { } } + @Test + void setReadOnlyIsPreservedAfterSetTransactionIsolation() throws Exception { + try (var connection = getConnectionViaDriverManager()) { + assertFalse(connection.isReadOnly(), "Connection initially not read-only"); + connection.setReadOnly(true); + assertTrue(connection.isReadOnly(), "Connection read-only after setReadOnly(true)"); + connection.setTransactionIsolation(connection.getTransactionIsolation()); + assertTrue(connection.isReadOnly(), "Connection should still be read-only after setTransactionIsolation"); + } + } + + @Test + void setReadOnly_happyPath() throws Exception { + try (var connection = getConnectionViaDriverManager()) { + assertFalse(connection.isReadOnly(), "Connection initially not read-only"); + connection.setReadOnly(true); + assertTrue(connection.isReadOnly(), "Connection read-only after setReadOnly(true)"); + + try (var stmt = connection.prepareStatement(INSERT_DATA)) { + stmt.setInt(1, 5); + var exception = assertThrows(SQLException.class, stmt::executeUpdate); + assertThat(exception, errorCodeEquals(ISCConstants.isc_read_only_trans)); + } + } + } + + @Test + void readOnlyShouldInheritFromTransactionConfigurationOfDefaultIsolation() throws Exception { + try (var connection = getConnectionViaDriverManager( + "TRANSACTION_READ_COMMITTED", "read_committed,rec_version,read,wait")) { + assertTrue(connection.isReadOnly(), "Connection initially read-only"); + + try (var stmt = connection.prepareStatement(INSERT_DATA)) { + stmt.setInt(1, 5); + var exception = assertThrows(SQLException.class, stmt::execute); + assertThat(exception, errorCodeEquals(ISCConstants.isc_read_only_trans)); + } + + connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + + try (var stmt = connection.prepareStatement(INSERT_DATA)) { + stmt.setInt(1, 5); + var exception = assertThrows(SQLException.class, stmt::execute); + assertThat(exception, errorCodeEquals(ISCConstants.isc_read_only_trans)); + } + } + } + /** * Single-use executor, delays the command to be executed until signalled. */ diff --git a/src/test/org/firebirdsql/jdbc/FBTpbMapperTest.java b/src/test/org/firebirdsql/jdbc/FBTpbMapperTest.java index e97928b44..e71ba3462 100644 --- a/src/test/org/firebirdsql/jdbc/FBTpbMapperTest.java +++ b/src/test/org/firebirdsql/jdbc/FBTpbMapperTest.java @@ -355,22 +355,23 @@ void testSetMapping_NONE_throwsIllegalArgumentException() throws Exception { } @Test - void testClone_equalToOriginal() { - FBTpbMapper original = new FBTpbMapper(); - FBTpbMapper clone = (FBTpbMapper) original.clone(); + void testCopy_equalToOriginal() { + var original = new FBTpbMapper(); + var copy = FBTpbMapper.copyOf(original); - assertEquals(original, clone); + assertEquals(original, copy); + assertEquals(original.hashCode(), copy.hashCode()); } @Test - void testModifyingClone_doesNotModifyOriginal() throws Exception { - FBTpbMapper original = new FBTpbMapper(); - FBTpbMapper clone = (FBTpbMapper) original.clone(); + void testModifyingCopy_doesNotModifyOriginal() throws Exception { + var original = new FBTpbMapper(); + var copy = FBTpbMapper.copyOf(original); TransactionParameterBuffer newTpb = FBTpbMapper.processMapping("isc_tpb_read_committed,isc_tpb_no_rec_version,isc_tpb_write,isc_tpb_wait"); - clone.setMapping(Connection.TRANSACTION_READ_COMMITTED, newTpb); + copy.setMapping(Connection.TRANSACTION_READ_COMMITTED, newTpb); - assertNotEquals(original, clone); + assertNotEquals(original, copy); } }