From ab4816ec9a583dbc49a1b86e4dfc16a7c1b0f55b Mon Sep 17 00:00:00 2001 From: Billy Yuan Date: Sun, 17 May 2020 12:33:48 +0800 Subject: [PATCH] add support for customizing transaction start - fixes #432 Signed-off-by: Billy Yuan --- .../main/java/examples/SqlClientExamples.java | 44 ++++++++--- .../db2client/impl/DB2SocketConnection.java | 41 ++++++---- .../impl/util/TransactionSqlBuilder.java | 58 ++++++++++++++ .../db2client/tck/DB2TransactionTest.java | 22 ++++-- .../util/TransactionSqlBuilderTest.java | 27 +++++++ .../main/java/examples/SqlClientExamples.java | 32 ++++++++ .../vertx/mssqlclient/impl/MSSQLPoolImpl.java | 2 - .../main/java/examples/SqlClientExamples.java | 45 ++++++++--- .../impl/MySQLSocketConnection.java | 44 ++++++++--- .../impl/util/TransactionSqlBuilder.java | 41 ++++++++++ .../mysqlclient/MySQLConnectionTestBase.java | 2 +- .../mysqlclient/MySQLTransactionTest.java | 48 +++++++++++ .../impl/util/TransactionSqlBuilderTest.java | 26 ++++++ .../main/java/examples/SqlClientExamples.java | 45 ++++++++--- .../pgclient/impl/PgSocketConnection.java | 20 +++-- .../impl/util/TransactionSqlBuilder.java | 58 ++++++++++++++ .../vertx/pgclient/PgConnectionTestBase.java | 32 ++++++-- .../impl/util/TransactionSqlBuilderTest.java | 26 ++++++ .../src/main/asciidoc/dataobjects.adoc | 20 +++++ vertx-sql-client/src/main/asciidoc/enums.adoc | 56 +++++++++++++ .../src/main/asciidoc/transactions.adoc | 21 ++++- .../TransactionOptionsConverter.java | 44 +++++++++++ .../main/java/examples/SqlClientExamples.java | 45 ++++++++--- .../main/java/io/vertx/sqlclient/Pool.java | 20 ++++- .../io/vertx/sqlclient/SqlConnection.java | 17 ++++ .../io/vertx/sqlclient/impl/PoolBase.java | 22 ++++-- .../sqlclient/impl/SqlConnectionImpl.java | 33 +++++--- .../vertx/sqlclient/impl/TransactionImpl.java | 10 ++- .../impl/command/StartTxCommand.java | 16 ++++ .../sqlclient/impl/command/TxCommand.java | 9 +-- .../{ => transaction}/Transaction.java | 2 +- .../transaction/TransactionAccessMode.java | 21 +++++ .../TransactionIsolationLevel.java | 39 +++++++++ .../transaction/TransactionOptions.java | 79 +++++++++++++++++++ .../TransactionRollbackException.java | 2 +- .../sqlclient/tck/TransactionTestBase.java | 38 +++++++-- 36 files changed, 968 insertions(+), 139 deletions(-) create mode 100644 vertx-db2-client/src/main/java/io/vertx/db2client/impl/util/TransactionSqlBuilder.java create mode 100644 vertx-db2-client/src/test/java/io/vertx/db2client/util/TransactionSqlBuilderTest.java create mode 100644 vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilder.java create mode 100644 vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLTransactionTest.java create mode 100644 vertx-mysql-client/src/test/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilderTest.java create mode 100644 vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/TransactionSqlBuilder.java create mode 100644 vertx-pg-client/src/test/java/io/vertx/pgclient/impl/util/TransactionSqlBuilderTest.java create mode 100644 vertx-sql-client/src/main/asciidoc/enums.adoc create mode 100644 vertx-sql-client/src/main/generated/io/vertx/sqlclient/transaction/TransactionOptionsConverter.java create mode 100644 vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/StartTxCommand.java rename vertx-sql-client/src/main/java/io/vertx/sqlclient/{ => transaction}/Transaction.java (97%) create mode 100644 vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionAccessMode.java create mode 100644 vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionIsolationLevel.java create mode 100644 vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionOptions.java rename vertx-sql-client/src/main/java/io/vertx/sqlclient/{ => transaction}/TransactionRollbackException.java (96%) diff --git a/vertx-db2-client/src/main/java/examples/SqlClientExamples.java b/vertx-db2-client/src/main/java/examples/SqlClientExamples.java index dbf6f5d97..5c235863c 100644 --- a/vertx-db2-client/src/main/java/examples/SqlClientExamples.java +++ b/vertx-db2-client/src/main/java/examples/SqlClientExamples.java @@ -20,17 +20,11 @@ import io.vertx.core.Vertx; import io.vertx.docgen.Source; -import io.vertx.sqlclient.Cursor; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PreparedStatement; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.RowStream; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnectOptions; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; @Source public class SqlClientExamples { @@ -311,6 +305,34 @@ public void transaction03(Pool pool) { }); } + public void transaction04(SqlConnection sqlConnection) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + sqlConnection.begin(txOptions, ar -> { + if (ar.succeeded()) { + // start a transaction which is read-only + Transaction tx = ar.result(); + } else { + // Failed to start a transaction + System.out.println("Transaction failed " + ar.cause().getMessage()); + } + }); + } + + public void transaction05(Pool pool) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + pool.withTransaction(txOptions, client -> client + .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')") + .execute() + ).onFailure(error -> { + // Failed to insert the record because the transaction is read-only + System.out.println("Transaction failed " + error.getMessage()); + }); + } + public void usingCursors01(SqlConnection connection) { connection.prepare("SELECT * FROM users WHERE first_name LIKE $1", ar0 -> { if (ar0.succeeded()) { diff --git a/vertx-db2-client/src/main/java/io/vertx/db2client/impl/DB2SocketConnection.java b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/DB2SocketConnection.java index 69b5e4836..768233e47 100644 --- a/vertx-db2-client/src/main/java/io/vertx/db2client/impl/DB2SocketConnection.java +++ b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/DB2SocketConnection.java @@ -28,29 +28,26 @@ import io.vertx.sqlclient.impl.Connection; import io.vertx.sqlclient.impl.QueryResultHandler; import io.vertx.sqlclient.impl.SocketConnectionBase; -import io.vertx.sqlclient.impl.command.CommandBase; -import io.vertx.sqlclient.impl.command.CommandResponse; -import io.vertx.sqlclient.impl.command.QueryCommandBase; -import io.vertx.sqlclient.impl.command.SimpleQueryCommand; -import io.vertx.sqlclient.impl.command.TxCommand; +import io.vertx.sqlclient.impl.command.*; +import io.vertx.db2client.impl.util.TransactionSqlBuilder; public class DB2SocketConnection extends SocketConnectionBase { private DB2Codec codec; private Handler closeHandler; - public DB2SocketConnection(NetSocketInternal socket, - boolean cachePreparedStatements, + public DB2SocketConnection(NetSocketInternal socket, + boolean cachePreparedStatements, int preparedStatementCacheSize, - int preparedStatementCacheSqlLimit, - int pipeliningLimit, + int preparedStatementCacheSqlLimit, + int pipeliningLimit, ContextInternal context) { super(socket, cachePreparedStatements, preparedStatementCacheSize, preparedStatementCacheSqlLimit, pipeliningLimit, context); } - void sendStartupMessage(String username, - String password, - String database, + void sendStartupMessage(String username, + String password, + String database, Map properties, Promise completionHandler) { InitialHandshakeCommand cmd = new InitialHandshakeCommand(this, username, password, database, properties); @@ -70,12 +67,22 @@ protected void doSchedule(CommandBase cmd, Handler> handle if (cmd instanceof TxCommand) { TxCommand txCmd = (TxCommand) cmd; if (txCmd.kind == TxCommand.Kind.BEGIN) { - // DB2 always implicitly starts a transaction with each query, and does - // not support the 'BEGIN' keyword. Instead we can no-op BEGIN commands - cmd.handler = handler; - cmd.complete(CommandResponse.success(txCmd.result).toAsyncResult()); + StartTxCommand startTxCommand = (StartTxCommand) txCmd; + if (startTxCommand.isolationLevel != null || startTxCommand.accessMode != null) { + // customized transaction + String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(startTxCommand.isolationLevel, startTxCommand.accessMode); + SimpleQueryCommand setTxCmd = new SimpleQueryCommand<>(sql, false, false, + QueryCommandBase.NULL_COLLECTOR, QueryResultHandler.NOOP_HANDLER); + + super.doSchedule(setTxCmd, ar -> handler.handle(ar.map(txCmd.result))); + } else { + // DB2 always implicitly starts a transaction with each query, and does + // not support the 'BEGIN' keyword. Instead we can no-op BEGIN commands + cmd.handler = handler; + cmd.complete(CommandResponse.success(txCmd.result).toAsyncResult()); + } } else { - SimpleQueryCommand cmd2 = new SimpleQueryCommand<>(txCmd.kind.sql, false, false, + SimpleQueryCommand cmd2 = new SimpleQueryCommand<>(txCmd.kind.name(), false, false, QueryCommandBase.NULL_COLLECTOR, QueryResultHandler.NOOP_HANDLER); super.doSchedule(cmd2, ar -> handler.handle(ar.map(txCmd.result))); diff --git a/vertx-db2-client/src/main/java/io/vertx/db2client/impl/util/TransactionSqlBuilder.java b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/util/TransactionSqlBuilder.java new file mode 100644 index 000000000..a6dd6b2b7 --- /dev/null +++ b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/util/TransactionSqlBuilder.java @@ -0,0 +1,58 @@ +package io.vertx.db2client.impl.util; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; + +public class TransactionSqlBuilder { + private static final String SET_ISOLATION = "SET TRANSACTION"; + + private static final String PREDEFINED_TX_REPEATABLE_READ = " ISOLATION LEVEL REPEATABLE READ"; + private static final String PREDEFINED_TX_SERIALIZABLE = " ISOLATION LEVEL SERIALIZABLE"; + private static final String PREDEFINED_TX_READ_COMMITTED = " ISOLATION LEVEL READ COMMITTED"; + private static final String PREDEFINED_TX_READ_UNCOMMITTED = " ISOLATION LEVEL READ UNCOMMITTED"; + + + private static final String PREDEFINED_TX_RW = " READ WRITE"; + private static final String PREDEFINED_TX_RO = " READ ONLY"; + + public static String buildSetTxIsolationLevelSql(TransactionIsolationLevel isolationLevel, TransactionAccessMode accessMode) { + boolean isCharacteristicExisted = false; + StringBuilder sqlBuilder = new StringBuilder(SET_ISOLATION); + + if (isolationLevel != null) { + switch (isolationLevel) { + case READ_UNCOMMITTED: + sqlBuilder.append(PREDEFINED_TX_READ_UNCOMMITTED); + break; + case READ_COMMITTED: + sqlBuilder.append(PREDEFINED_TX_READ_COMMITTED); + break; + case REPEATABLE_READ: + sqlBuilder.append(PREDEFINED_TX_REPEATABLE_READ); + break; + case SERIALIZABLE: + sqlBuilder.append(PREDEFINED_TX_SERIALIZABLE); + break; + } + isCharacteristicExisted = true; + } + + if (accessMode != null) { + if (isCharacteristicExisted) { + sqlBuilder.append(','); + } else { + isCharacteristicExisted = true; + } + switch (accessMode) { + case READ_ONLY: + sqlBuilder.append(PREDEFINED_TX_RO); + break; + case READ_WRITE: + sqlBuilder.append(PREDEFINED_TX_RW); + break; + } + } + + return sqlBuilder.toString(); + } +} diff --git a/vertx-db2-client/src/test/java/io/vertx/db2client/tck/DB2TransactionTest.java b/vertx-db2-client/src/test/java/io/vertx/db2client/tck/DB2TransactionTest.java index 2d7b6d7db..d711b345e 100644 --- a/vertx-db2-client/src/test/java/io/vertx/db2client/tck/DB2TransactionTest.java +++ b/vertx-db2-client/src/test/java/io/vertx/db2client/tck/DB2TransactionTest.java @@ -17,10 +17,7 @@ import static org.junit.Assume.assumeFalse; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.TestName; import org.junit.runner.RunWith; @@ -38,7 +35,7 @@ public class DB2TransactionTest extends TransactionTestBase { @ClassRule public static DB2Resource rule = DB2Resource.SHARED_INSTANCE; - + @Rule public TestName testName = new TestName(); @@ -74,4 +71,19 @@ public void testDelayedCommit(TestContext ctx) { super.testDelayedCommit(ctx); } + @Override + @Ignore + @Test + public void testStartReadOnlyTransaction(TestContext ctx) { + // FIXME + super.testStartReadOnlyTransaction(ctx); + } + + @Override + @Ignore + @Test + public void testWithReadOnlyTransactionStart(TestContext ctx) { + // FIXME + super.testWithReadOnlyTransactionStart(ctx); + } } diff --git a/vertx-db2-client/src/test/java/io/vertx/db2client/util/TransactionSqlBuilderTest.java b/vertx-db2-client/src/test/java/io/vertx/db2client/util/TransactionSqlBuilderTest.java new file mode 100644 index 000000000..fd58c1b70 --- /dev/null +++ b/vertx-db2-client/src/test/java/io/vertx/db2client/util/TransactionSqlBuilderTest.java @@ -0,0 +1,27 @@ +package io.vertx.db2client.util; + +import io.vertx.db2client.impl.util.TransactionSqlBuilder; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import org.junit.Assert; +import org.junit.Test; + +public class TransactionSqlBuilderTest { + @Test + public void testSetReadCommitted() { + String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(TransactionIsolationLevel.READ_COMMITTED, null); + Assert.assertEquals("SET TRANSACTION ISOLATION LEVEL READ COMMITTED" ,sql); + } + + @Test + public void testSetReadOnly() { + String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(null, TransactionAccessMode.READ_ONLY); + Assert.assertEquals("SET TRANSACTION READ ONLY" ,sql); + } + + @Test + public void testSerializableReadOnly() { + String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(TransactionIsolationLevel.SERIALIZABLE, TransactionAccessMode.READ_ONLY); + Assert.assertEquals("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY" ,sql); + } +} diff --git a/vertx-mssql-client/src/main/java/examples/SqlClientExamples.java b/vertx-mssql-client/src/main/java/examples/SqlClientExamples.java index 5190b33c1..6e21b77dc 100644 --- a/vertx-mssql-client/src/main/java/examples/SqlClientExamples.java +++ b/vertx-mssql-client/src/main/java/examples/SqlClientExamples.java @@ -19,6 +19,10 @@ import io.vertx.core.Vertx; import io.vertx.docgen.Source; import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.ArrayList; import java.util.List; @@ -298,6 +302,34 @@ public void transaction03(Pool pool) { }); } + public void transaction04(SqlConnection sqlConnection) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + sqlConnection.begin(txOptions, ar -> { + if (ar.succeeded()) { + // start a transaction which is read-only + Transaction tx = ar.result(); + } else { + // Failed to start a transaction + System.out.println("Transaction failed " + ar.cause().getMessage()); + } + }); + } + + public void transaction05(Pool pool) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + pool.withTransaction(txOptions, client -> client + .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')") + .execute() + ).onFailure(error -> { + // Failed to insert the record because the transaction is read-only + System.out.println("Transaction failed " + error.getMessage()); + }); + } + public void usingCursors01(SqlConnection connection) { connection.prepare("SELECT * FROM users WHERE age > @p1", ar1 -> { if (ar1.succeeded()) { diff --git a/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/MSSQLPoolImpl.java b/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/MSSQLPoolImpl.java index be4b2bc90..7916e54da 100644 --- a/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/MSSQLPoolImpl.java +++ b/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/MSSQLPoolImpl.java @@ -19,11 +19,9 @@ import io.vertx.core.Handler; import io.vertx.sqlclient.PoolOptions; import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.Transaction; import io.vertx.sqlclient.impl.Connection; import io.vertx.sqlclient.impl.PoolBase; import io.vertx.sqlclient.impl.SqlConnectionImpl; -import io.vertx.sqlclient.impl.pool.ConnectionPool; import java.util.function.Function; diff --git a/vertx-mysql-client/src/main/java/examples/SqlClientExamples.java b/vertx-mysql-client/src/main/java/examples/SqlClientExamples.java index 5f7fa5f16..fea254835 100644 --- a/vertx-mysql-client/src/main/java/examples/SqlClientExamples.java +++ b/vertx-mysql-client/src/main/java/examples/SqlClientExamples.java @@ -18,18 +18,11 @@ import io.vertx.core.Vertx; import io.vertx.docgen.Source; -import io.vertx.sqlclient.Cursor; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; -import io.vertx.sqlclient.PreparedStatement; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.RowStream; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnectOptions; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.ArrayList; import java.util.List; @@ -311,6 +304,34 @@ public void transaction03(Pool pool) { }); } + public void transaction04(SqlConnection sqlConnection) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + sqlConnection.begin(txOptions, ar -> { + if (ar.succeeded()) { + // start a transaction which is read-only + Transaction tx = ar.result(); + } else { + // Failed to start a transaction + System.out.println("Transaction failed " + ar.cause().getMessage()); + } + }); + } + + public void transaction05(Pool pool) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + pool.withTransaction(txOptions, client -> client + .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')") + .execute() + ).onFailure(error -> { + // Failed to insert the record because the transaction is read-only + System.out.println("Transaction failed " + error.getMessage()); + }); + } + public void usingCursors01(SqlConnection connection) { connection.prepare("SELECT * FROM users WHERE age > ?", ar1 -> { if (ar1.succeeded()) { diff --git a/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/MySQLSocketConnection.java b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/MySQLSocketConnection.java index dd498f65a..44aa6be5b 100644 --- a/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/MySQLSocketConnection.java +++ b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/MySQLSocketConnection.java @@ -25,13 +25,11 @@ import io.vertx.mysqlclient.SslMode; import io.vertx.mysqlclient.impl.codec.MySQLCodec; import io.vertx.mysqlclient.impl.command.InitialHandshakeCommand; +import io.vertx.mysqlclient.impl.util.TransactionSqlBuilder; import io.vertx.sqlclient.impl.Connection; import io.vertx.sqlclient.impl.QueryResultHandler; import io.vertx.sqlclient.impl.SocketConnectionBase; -import io.vertx.sqlclient.impl.command.CommandBase; -import io.vertx.sqlclient.impl.command.QueryCommandBase; -import io.vertx.sqlclient.impl.command.SimpleQueryCommand; -import io.vertx.sqlclient.impl.command.TxCommand; +import io.vertx.sqlclient.impl.command.*; import java.nio.charset.Charset; import java.util.Map; @@ -76,14 +74,27 @@ public void init() { @Override protected void doSchedule(CommandBase cmd, Handler> handler) { if (cmd instanceof TxCommand) { - TxCommand tx = (TxCommand) cmd; - SimpleQueryCommand cmd2 = new SimpleQueryCommand<>( - tx.kind.sql, - false, - false, - QueryCommandBase.NULL_COLLECTOR, - QueryResultHandler.NOOP_HANDLER); - super.doSchedule(cmd2, ar -> handler.handle(ar.map(tx.result))); + TxCommand txCmd = (TxCommand) cmd; + String txSql; + if (txCmd.kind == TxCommand.Kind.BEGIN) { + StartTxCommand startTxCommand = (StartTxCommand) txCmd; + + if (startTxCommand.accessMode != null) { + txSql = TransactionSqlBuilder.buildStartTxSql(startTxCommand.accessMode); + } else { + txSql = txCmd.kind.name(); + } + + if (startTxCommand.isolationLevel != null) { + // MySQL could not set transaction level at the transaction start + SimpleQueryCommand setTxIsolationLevelCmd = buildNoOpQueryCommand(TransactionSqlBuilder.buildSetTxIsolationLevelSql(startTxCommand.isolationLevel)); + super.doSchedule(new BiCommand<>(setTxIsolationLevelCmd, v -> Future.succeededFuture(buildNoOpQueryCommand(txSql))), ar -> handler.handle(ar.map(txCmd.result))); + return; + } + } else { + txSql = txCmd.kind.name(); + } + super.doSchedule(buildNoOpQueryCommand(txSql), ar -> handler.handle(ar.map(txCmd.result))); } else { super.doSchedule(cmd, handler); } @@ -92,4 +103,13 @@ protected void doSchedule(CommandBase cmd, Handler> handle public void upgradeToSsl(Handler> completionHandler) { socket.upgradeToSsl(completionHandler); } + + private SimpleQueryCommand buildNoOpQueryCommand(String sql) { + return new SimpleQueryCommand<>( + sql, + false, + false, + QueryCommandBase.NULL_COLLECTOR, + QueryResultHandler.NOOP_HANDLER); + } } diff --git a/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilder.java b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilder.java new file mode 100644 index 000000000..67fdd7b46 --- /dev/null +++ b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilder.java @@ -0,0 +1,41 @@ +package io.vertx.mysqlclient.impl.util; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; + +public class TransactionSqlBuilder { + private static final String START_TX_DEFAULT = "START TRANSACTION"; + private static final String SET_ISOLATION = "SET TRANSACTION"; + + private static final String PREDEFINED_TX_REPEATABLE_READ = " ISOLATION LEVEL REPEATABLE READ"; + private static final String PREDEFINED_TX_SERIALIZABLE = " ISOLATION LEVEL SERIALIZABLE"; + private static final String PREDEFINED_TX_READ_COMMITTED = " ISOLATION LEVEL READ COMMITTED"; + private static final String PREDEFINED_TX_READ_UNCOMMITTED = " ISOLATION LEVEL READ UNCOMMITTED"; + + + private static final String PREDEFINED_TX_RW = " READ WRITE"; + private static final String PREDEFINED_TX_RO = " READ ONLY"; + + + public static String buildStartTxSql(TransactionAccessMode accessMode) { + if (accessMode == TransactionAccessMode.READ_ONLY) { + return START_TX_DEFAULT + PREDEFINED_TX_RO; + } else if (accessMode == TransactionAccessMode.READ_WRITE) { + return START_TX_DEFAULT + PREDEFINED_TX_RW; + } else { + return START_TX_DEFAULT; + } + } + + public static String buildSetTxIsolationLevelSql(TransactionIsolationLevel isolationLevel) { + if (isolationLevel == TransactionIsolationLevel.READ_UNCOMMITTED) { + return SET_ISOLATION + PREDEFINED_TX_READ_UNCOMMITTED; + } else if (isolationLevel == TransactionIsolationLevel.READ_COMMITTED) { + return SET_ISOLATION + PREDEFINED_TX_READ_COMMITTED; + } else if (isolationLevel == TransactionIsolationLevel.REPEATABLE_READ) { + return SET_ISOLATION + PREDEFINED_TX_REPEATABLE_READ; + } else { + return SET_ISOLATION + PREDEFINED_TX_SERIALIZABLE; + } + } +} diff --git a/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLConnectionTestBase.java b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLConnectionTestBase.java index 151caea98..ad847ff2d 100644 --- a/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLConnectionTestBase.java +++ b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLConnectionTestBase.java @@ -15,7 +15,7 @@ import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; -import io.vertx.sqlclient.TransactionRollbackException; +import io.vertx.sqlclient.transaction.TransactionRollbackException; import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLTransactionTest.java b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLTransactionTest.java new file mode 100644 index 000000000..6a161284d --- /dev/null +++ b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLTransactionTest.java @@ -0,0 +1,48 @@ +package io.vertx.mysqlclient; + +import io.vertx.core.Vertx; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class MySQLTransactionTest extends MySQLTestBase { + Vertx vertx; + MySQLConnectOptions options; + + @Before + public void setup() { + vertx = Vertx.vertx(); + options = new MySQLConnectOptions(MySQLTestBase.options); + } + + @After + public void tearDown(TestContext ctx) { + vertx.close(ctx.asyncAssertSuccess()); + } + + @Test + public void testTxOptions(TestContext ctx) { + Async async = ctx.async(); + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + txOptions.setIsolationLevel(TransactionIsolationLevel.SERIALIZABLE); + MySQLConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + conn.begin(txOptions, ctx.asyncAssertSuccess(tx -> { + conn.query("INSERT INTO mutable (id, val) VALUES (1, 'hello-1')") + .execute(ctx.asyncAssertFailure(error -> { + tx.rollback(); + conn.close(); + async.complete(); + })); + })); + })); + } +} diff --git a/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilderTest.java b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilderTest.java new file mode 100644 index 000000000..d97d5a87f --- /dev/null +++ b/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/impl/util/TransactionSqlBuilderTest.java @@ -0,0 +1,26 @@ +package io.vertx.mysqlclient.impl.util; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import org.junit.Assert; +import org.junit.Test; + +public class TransactionSqlBuilderTest { + @Test + public void testBuildSetIsolationLevel() { + String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(TransactionIsolationLevel.SERIALIZABLE); + Assert.assertEquals("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", sql); + } + + @Test + public void testStartReadOnlyTx() { + String sql = TransactionSqlBuilder.buildStartTxSql(TransactionAccessMode.READ_ONLY); + Assert.assertEquals("START TRANSACTION READ ONLY", sql); + } + + @Test + public void testStartDefaultTx() { + String sql = TransactionSqlBuilder.buildStartTxSql(null); + Assert.assertEquals("START TRANSACTION", sql); + } +} diff --git a/vertx-pg-client/src/main/java/examples/SqlClientExamples.java b/vertx-pg-client/src/main/java/examples/SqlClientExamples.java index 515e79717..4c5c16660 100644 --- a/vertx-pg-client/src/main/java/examples/SqlClientExamples.java +++ b/vertx-pg-client/src/main/java/examples/SqlClientExamples.java @@ -18,18 +18,11 @@ import io.vertx.core.Vertx; import io.vertx.docgen.Source; -import io.vertx.sqlclient.Cursor; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; -import io.vertx.sqlclient.PreparedStatement; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.RowStream; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnectOptions; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.ArrayList; import java.util.List; @@ -311,6 +304,34 @@ public void transaction03(Pool pool) { } }); } + public void transaction04(SqlConnection sqlConnection) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + sqlConnection.begin(txOptions, ar -> { + if (ar.succeeded()) { + // start a transaction which is read-only + Transaction tx = ar.result(); + } else { + // Failed to start a transaction + System.out.println("Transaction failed " + ar.cause().getMessage()); + } + }); + } + + public void transaction05(Pool pool) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + pool.withTransaction(txOptions, client -> client + .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')") + .execute() + ).onFailure(error -> { + // Failed to insert the record because the transaction is read-only + System.out.println("Transaction failed " + error.getMessage()); + }); + } + public void usingCursors01(SqlConnection connection) { connection.prepare("SELECT * FROM users WHERE first_name LIKE $1", ar0 -> { if (ar0.succeeded()) { diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java index 9eafe9a74..84ab8b650 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java @@ -26,14 +26,11 @@ import io.vertx.sqlclient.impl.Notification; import io.vertx.sqlclient.impl.QueryResultHandler; import io.vertx.sqlclient.impl.SocketConnectionBase; -import io.vertx.sqlclient.impl.command.CommandBase; -import io.vertx.sqlclient.impl.command.InitCommand; +import io.vertx.sqlclient.impl.command.*; import io.vertx.core.*; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.impl.NetSocketInternal; -import io.vertx.sqlclient.impl.command.QueryCommandBase; -import io.vertx.sqlclient.impl.command.SimpleQueryCommand; -import io.vertx.sqlclient.impl.command.TxCommand; +import io.vertx.pgclient.impl.util.TransactionSqlBuilder; import java.util.Map; @@ -135,14 +132,21 @@ void upgradeToSSLConnection(Handler> completionHandler) { @Override protected void doSchedule(CommandBase cmd, Handler> handler) { if (cmd instanceof TxCommand) { - TxCommand tx = (TxCommand) cmd; + String txSql; + TxCommand txCmd = (TxCommand) cmd; + if (txCmd.kind == TxCommand.Kind.BEGIN) { + StartTxCommand startTxCommand = (StartTxCommand) txCmd; + txSql = TransactionSqlBuilder.buildStartTxSql(startTxCommand.isolationLevel, startTxCommand.accessMode); + } else { + txSql = txCmd.kind.name(); + } SimpleQueryCommand cmd2 = new SimpleQueryCommand<>( - tx.kind.sql, + txSql, false, false, QueryCommandBase.NULL_COLLECTOR, QueryResultHandler.NOOP_HANDLER); - super.doSchedule(cmd2, ar -> handler.handle(ar.map(tx.result))); + super.doSchedule(cmd2, ar -> handler.handle(ar.map(txCmd.result))); } else { super.doSchedule(cmd, handler); } diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/TransactionSqlBuilder.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/TransactionSqlBuilder.java new file mode 100644 index 000000000..e805acc72 --- /dev/null +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/TransactionSqlBuilder.java @@ -0,0 +1,58 @@ +package io.vertx.pgclient.impl.util; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; + +public class TransactionSqlBuilder { + private static final String START_TX_DEFAULT = "START TRANSACTION"; + + private static final String PREDEFINED_TX_REPEATABLE_READ = " ISOLATION LEVEL REPEATABLE READ"; + private static final String PREDEFINED_TX_SERIALIZABLE = " ISOLATION LEVEL SERIALIZABLE"; + private static final String PREDEFINED_TX_READ_COMMITTED = " ISOLATION LEVEL READ COMMITTED"; + private static final String PREDEFINED_TX_READ_UNCOMMITTED = " ISOLATION LEVEL READ UNCOMMITTED"; + + + private static final String PREDEFINED_TX_RW = " READ WRITE"; + private static final String PREDEFINED_TX_RO = " READ ONLY"; + + public static String buildStartTxSql(TransactionIsolationLevel isolationLevel, TransactionAccessMode accessMode) { + boolean isCharacteristicExisted = false; + StringBuilder sqlBuilder = new StringBuilder(START_TX_DEFAULT); + + if (isolationLevel != null) { + switch (isolationLevel) { + case READ_UNCOMMITTED: + sqlBuilder.append(PREDEFINED_TX_READ_UNCOMMITTED); + break; + case READ_COMMITTED: + sqlBuilder.append(PREDEFINED_TX_READ_COMMITTED); + break; + case REPEATABLE_READ: + sqlBuilder.append(PREDEFINED_TX_REPEATABLE_READ); + break; + case SERIALIZABLE: + sqlBuilder.append(PREDEFINED_TX_SERIALIZABLE); + break; + } + isCharacteristicExisted = true; + } + + if (accessMode != null) { + if (isCharacteristicExisted) { + sqlBuilder.append(','); + } else { + isCharacteristicExisted = true; + } + switch (accessMode) { + case READ_ONLY: + sqlBuilder.append(PREDEFINED_TX_RO); + break; + case READ_WRITE: + sqlBuilder.append(PREDEFINED_TX_RW); + break; + } + } + + return sqlBuilder.toString(); + } +} diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/PgConnectionTestBase.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/PgConnectionTestBase.java index 30b3f9311..d2ac1e99d 100644 --- a/vertx-pg-client/src/test/java/io/vertx/pgclient/PgConnectionTestBase.java +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/PgConnectionTestBase.java @@ -17,15 +17,15 @@ package io.vertx.pgclient; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.TransactionRollbackException; -import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.*; import io.vertx.core.*; import io.vertx.core.buffer.Buffer; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; +import io.vertx.sqlclient.transaction.TransactionRollbackException; import org.junit.Test; import java.util.ArrayList; @@ -462,4 +462,26 @@ public void testTransactionAbort(TestContext ctx) { }); })); } + + @Test + public void testTxOptions(TestContext ctx) { + Async async = ctx.async(); + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + connector.accept(ctx.asyncAssertSuccess(conn -> { + conn.begin(txOptions, ctx.asyncAssertSuccess(tx -> { + conn.query("SHOW TRANSACTION ISOLATION LEVEL;") + .execute(ctx.asyncAssertSuccess(res -> { + ctx.assertEquals("repeatable read", res.iterator().next().getString(0)); + conn.query("INSERT INTO mutable (id, val) VALUES (1, 'hello-1')") + .execute(ctx.asyncAssertFailure(error -> { + tx.rollback(); + conn.close(); + async.complete(); + })); + })); + })); + })); + } } diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/impl/util/TransactionSqlBuilderTest.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/impl/util/TransactionSqlBuilderTest.java new file mode 100644 index 000000000..89c343cc2 --- /dev/null +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/impl/util/TransactionSqlBuilderTest.java @@ -0,0 +1,26 @@ +package io.vertx.pgclient.impl.util; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import org.junit.Assert; +import org.junit.Test; + +public class TransactionSqlBuilderTest { + @Test + public void testStartTxReadOnly() { + String sql = TransactionSqlBuilder.buildStartTxSql(null, TransactionAccessMode.READ_ONLY); + Assert.assertEquals("START TRANSACTION READ ONLY", sql); + } + + @Test + public void testStartTxSerializable() { + String sql = TransactionSqlBuilder.buildStartTxSql(TransactionIsolationLevel.SERIALIZABLE, null); + Assert.assertEquals("START TRANSACTION ISOLATION LEVEL SERIALIZABLE", sql); + } + + @Test + public void testStartTxCommittedReadAndReadOnly() { + String sql = TransactionSqlBuilder.buildStartTxSql(TransactionIsolationLevel.SERIALIZABLE, TransactionAccessMode.READ_ONLY); + Assert.assertEquals("START TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY", sql); + } +} diff --git a/vertx-sql-client/src/main/asciidoc/dataobjects.adoc b/vertx-sql-client/src/main/asciidoc/dataobjects.adoc index 886fe3219..ae5f7758a 100644 --- a/vertx-sql-client/src/main/asciidoc/dataobjects.adoc +++ b/vertx-sql-client/src/main/asciidoc/dataobjects.adoc @@ -91,3 +91,23 @@ Specify the user account to be used for the authentication. +++ |=== +[[TransactionOptions]] +== TransactionOptions + +++++ + Transaction options which could be used to control the characteristics at the start of transaction. +++++ +''' + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[accessMode]]`@accessMode`|`link:enums.html#TransactionAccessMode[TransactionAccessMode]`|+++ +Set the link in the options. ++++ +|[[isolationLevel]]`@isolationLevel`|`link:enums.html#TransactionIsolationLevel[TransactionIsolationLevel]`|+++ +Set the link in the options. ++++ +|=== + diff --git a/vertx-sql-client/src/main/asciidoc/enums.adoc b/vertx-sql-client/src/main/asciidoc/enums.adoc new file mode 100644 index 000000000..7bbeec80c --- /dev/null +++ b/vertx-sql-client/src/main/asciidoc/enums.adoc @@ -0,0 +1,56 @@ += Enums + +[[TransactionAccessMode]] +== TransactionAccessMode + +++++ + Transaction access mode is the transaction characteristic which can be used to control whether the transaction is read/write or read only. +++++ +''' + +[cols=">25%,75%"] +[frame="topbot"] +|=== +^|Name | Description +|[[READ_WRITE]]`READ_WRITE`|+++ +Indicate the transaction is in read write mode ++++ +|[[READ_ONLY]]`READ_ONLY`|+++ +Indicate the transaction is in read only mode ++++ +|=== + +[[TransactionIsolationLevel]] +== TransactionIsolationLevel + +++++ + Transaction isolation level is the transaction characteristic to determine the visibility of data in a transaction to other transactions running concurrently. +++++ +''' + +[cols=">25%,75%"] +[frame="topbot"] +|=== +^|Name | Description +|[[READ_UNCOMMITTED]]`READ_UNCOMMITTED`|+++ +Implements dirty read, or isolation level 0 locking, which means that no shared locks are issued and no exclusive + locks are honored. When this option is set, it is possible to read uncommitted or dirty data; values in the data + can be changed and rows can appear or disappear in the data set before the end of the transaction. This is the + least restrictive of the four isolation levels. ++++ +|[[READ_COMMITTED]]`READ_COMMITTED`|+++ +Specifies that shared locks are held while the data is being read to avoid dirty reads, but the data can be changed + before the end of the transaction, resulting in nonrepeatable reads or phantom data. ++++ +|[[REPEATABLE_READ]]`REPEATABLE_READ`|+++ +Locks are placed on all data that is used in a query, preventing other users from updating the data, but new + phantom rows can be inserted into the data set by another user and are included in later reads in the current + transaction. Because concurrency is lower than the default isolation level, use this option only when necessary. ++++ +|[[SERIALIZABLE]]`SERIALIZABLE`|+++ +Places a range lock on the data set, preventing other users from updating or inserting rows into the data set until + the transaction is complete. This is the most restrictive of the four isolation levels. Because concurrency is + lower, use this option only when necessary. ++++ +|=== + diff --git a/vertx-sql-client/src/main/asciidoc/transactions.adoc b/vertx-sql-client/src/main/asciidoc/transactions.adoc index 05f6b853c..92831ec39 100644 --- a/vertx-sql-client/src/main/asciidoc/transactions.adoc +++ b/vertx-sql-client/src/main/asciidoc/transactions.adoc @@ -13,12 +13,19 @@ Or you can use the transaction API of {@link io.vertx.sqlclient.SqlConnection}: ---- When the database server reports the current transaction is failed (e.g the infamous _current transaction is aborted, commands ignored until -end of transaction block_), the transaction is rollbacked and the {@link io.vertx.sqlclient.Transaction#completion()} future -is failed with a {@link io.vertx.sqlclient.TransactionRollbackException}: +end of transaction block_), the transaction is rollbacked and the {@link io.vertx.sqlclient.transaction.Transaction#completion()} future +is failed with a {@link io.vertx.sqlclient.transaction.TransactionRollbackException}: [source,$lang] ---- -{@link examples.SqlClientExamples#transaction02(io.vertx.sqlclient.Transaction)} +{@link examples.SqlClientExamples#transaction02(io.vertx.sqlclient.transaction.Transaction)} +---- + +You can also start the transaction in a customized way like this so that you can configure transaction isolation level or access mode at the transaction start: + +[source,$lang] +---- +{@link examples.SqlClientExamples#transaction04(io.vertx.sqlclient.SqlConnection)} ---- === Simplified transaction API @@ -40,3 +47,11 @@ After the transaction completes, the connection is returned to the pool and the ---- {@link examples.SqlClientExamples#transaction03(io.vertx.sqlclient.Pool)} ---- + +You can also start the transaction in a customized way like this so that you can configure transaction isolation level or access mode at the transaction start: + +[source,$lang] +---- +{@link examples.SqlClientExamples#transaction05(io.vertx.sqlclient.Pool)} +---- + diff --git a/vertx-sql-client/src/main/generated/io/vertx/sqlclient/transaction/TransactionOptionsConverter.java b/vertx-sql-client/src/main/generated/io/vertx/sqlclient/transaction/TransactionOptionsConverter.java new file mode 100644 index 000000000..d953823c8 --- /dev/null +++ b/vertx-sql-client/src/main/generated/io/vertx/sqlclient/transaction/TransactionOptionsConverter.java @@ -0,0 +1,44 @@ +package io.vertx.sqlclient.transaction; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +/** + * Converter and mapper for {@link io.vertx.sqlclient.transaction.TransactionOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.sqlclient.transaction.TransactionOptions} original class using Vert.x codegen. + */ +public class TransactionOptionsConverter { + + + public static void fromJson(Iterable> json, TransactionOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "accessMode": + if (member.getValue() instanceof String) { + obj.setAccessMode(io.vertx.sqlclient.transaction.TransactionAccessMode.valueOf((String)member.getValue())); + } + break; + case "isolationLevel": + if (member.getValue() instanceof String) { + obj.setIsolationLevel(io.vertx.sqlclient.transaction.TransactionIsolationLevel.valueOf((String)member.getValue())); + } + break; + } + } + } + + public static void toJson(TransactionOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + public static void toJson(TransactionOptions obj, java.util.Map json) { + if (obj.getAccessMode() != null) { + json.put("accessMode", obj.getAccessMode().name()); + } + if (obj.getIsolationLevel() != null) { + json.put("isolationLevel", obj.getIsolationLevel().name()); + } + } +} diff --git a/vertx-sql-client/src/main/java/examples/SqlClientExamples.java b/vertx-sql-client/src/main/java/examples/SqlClientExamples.java index 785ed3cb2..b32996d78 100644 --- a/vertx-sql-client/src/main/java/examples/SqlClientExamples.java +++ b/vertx-sql-client/src/main/java/examples/SqlClientExamples.java @@ -17,18 +17,11 @@ package examples; import io.vertx.core.Vertx; -import io.vertx.sqlclient.Cursor; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; -import io.vertx.sqlclient.PreparedStatement; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.RowStream; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnectOptions; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.ArrayList; import java.util.List; @@ -310,6 +303,34 @@ public void transaction03(Pool pool) { }); } + public void transaction04(SqlConnection sqlConnection) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + sqlConnection.begin(txOptions, ar -> { + if (ar.succeeded()) { + // start a transaction which is read-only + Transaction tx = ar.result(); + } else { + // Failed to start a transaction + System.out.println("Transaction failed " + ar.cause().getMessage()); + } + }); + } + + public void transaction05(Pool pool) { + TransactionOptions txOptions = new TransactionOptions(); + txOptions.setIsolationLevel(TransactionIsolationLevel.REPEATABLE_READ); + txOptions.setAccessMode(TransactionAccessMode.READ_ONLY); + pool.withTransaction(txOptions, client -> client + .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')") + .execute() + ).onFailure(error -> { + // Failed to insert the record because the transaction is read-only + System.out.println("Transaction failed " + error.getMessage()); + }); + } + public void usingCursors01(SqlConnection connection) { connection.prepare("SELECT * FROM users WHERE first_name LIKE $1", ar0 -> { if (ar0.succeeded()) { diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/Pool.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/Pool.java index fc8affc26..9ea4a252c 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/Pool.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/Pool.java @@ -22,12 +22,14 @@ import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import io.vertx.codegen.annotations.GenIgnore; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.sqlclient.spi.Driver; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.function.Function; @@ -38,7 +40,7 @@ */ @VertxGen public interface Pool extends SqlClient { - + /** * Create a connection pool to the database configured with the given {@code connectOptions} and default {@link PoolOptions} * @@ -49,7 +51,7 @@ public interface Pool extends SqlClient { static Pool pool(SqlConnectOptions connectOptions) { return pool(connectOptions, new PoolOptions()); } - + /** * Create a connection pool to the database configured with the given {@code connectOptions} and {@code poolOptions}. * @@ -152,6 +154,20 @@ static Pool pool(Vertx vertx, SqlConnectOptions connectOptions, PoolOptions pool */ Future withTransaction(Function> function); + /** + * Like {@link #withTransaction(Function, Handler)} but provides a customized way to start the transaction + * so that you could configure the transaction such as isolation level or access mode at the start. + * + * @param txOptions the transaction options + * @param handler the handler notified with the transaction asynchronously + */ + void withTransaction(TransactionOptions txOptions, Function> function, Handler> handler); + + /** + * Like {@link #withTransaction(TransactionOptions, Function, Handler)} but returns a {@code Future} of the asynchronous result + */ + Future withTransaction(TransactionOptions txOptions, Function> function); + /** * Close the pool and release the associated resources. */ diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/SqlConnection.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/SqlConnection.java index 27c86ab4f..0b4dfc8d9 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/SqlConnection.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/SqlConnection.java @@ -18,10 +18,13 @@ package io.vertx.sqlclient; import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionOptions; /** * A connection to the database server. @@ -77,6 +80,20 @@ public interface SqlConnection extends SqlClient { */ Future begin(); + /** + * Like {@link #begin(Handler)} but provides a customized way to start the transaction + * so that you could configure the transaction such as isolation level or access mode at the start. + * + * @param txOptions the transaction options + * @param handler the handler notified with the transaction asynchronously + */ + void begin(TransactionOptions txOptions, Handler> handler); + + /** + * Like {@link #begin(TransactionOptions, Handler)} but returns a {@code Future} of the asynchronous result + */ + Future begin(TransactionOptions txOptions); + /** * @return whether the connection uses SSL */ diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/PoolBase.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/PoolBase.java index 4be1ee46c..f9d1dd380 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/PoolBase.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/PoolBase.java @@ -21,15 +21,13 @@ import io.vertx.core.Promise; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.VertxInternal; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.*; import io.vertx.sqlclient.impl.command.CommandBase; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.sqlclient.impl.pool.ConnectionPool; +import io.vertx.sqlclient.transaction.TransactionOptions; import java.util.function.Function; @@ -109,17 +107,27 @@ public Future getConnection() { @Override public void withTransaction(Function> function, Handler> handler) { - Future res = withTransaction(function); + withTransaction(TransactionOptions.DEFAULT_TX_OPTIONS, function, handler); + } + + @Override + public Future withTransaction(Function> function) { + return withTransaction(TransactionOptions.DEFAULT_TX_OPTIONS, function); + } + + @Override + public void withTransaction(TransactionOptions txOptions, Function> function, Handler> handler) { + Future res = withTransaction(txOptions, function); if (handler != null) { res.onComplete(handler); } } @Override - public Future withTransaction(Function> function) { + public Future withTransaction(TransactionOptions txOptions, Function> function) { return getConnection() .flatMap(conn -> conn - .begin() + .begin(txOptions) .flatMap(tx -> function .apply(conn) .compose( diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/SqlConnectionImpl.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/SqlConnectionImpl.java index c2f44d6a2..2565df71a 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/SqlConnectionImpl.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/SqlConnectionImpl.java @@ -19,8 +19,9 @@ import io.vertx.core.impl.ContextInternal; import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.transaction.TransactionOptions; import io.vertx.sqlclient.impl.command.CommandBase; -import io.vertx.sqlclient.Transaction; +import io.vertx.sqlclient.transaction.Transaction; import io.vertx.core.*; /** @@ -94,14 +95,7 @@ public C exceptionHandler(Handler handler) { @Override public Future begin() { - if (tx != null) { - throw new IllegalStateException(); - } - tx = new TransactionImpl(context, conn); - tx.completion().onComplete(ar -> { - tx = null; - }); - return tx.begin(); + return begin(TransactionOptions.DEFAULT_TX_OPTIONS); } @Override @@ -115,6 +109,27 @@ public void begin(Handler> handler) { fut.onComplete(handler); } + @Override + public void begin(TransactionOptions txOptions, Handler> handler) { + Future fut = begin(txOptions); + fut.onComplete(handler); + } + + @Override + public Future begin(TransactionOptions txOptions) { + if (txOptions == null) { + return Future.failedFuture(new IllegalArgumentException("Transaction options could not be null")); + } + if (tx != null) { + throw new IllegalStateException(); + } + tx = new TransactionImpl(context, conn); + tx.completion().onComplete(ar -> { + tx = null; + }); + return tx.begin(txOptions); + } + public void handleEvent(Object event) { } diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/TransactionImpl.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/TransactionImpl.java index 162adcac4..b19a8d18b 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/TransactionImpl.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/TransactionImpl.java @@ -26,9 +26,11 @@ import io.vertx.core.VertxException; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.PromiseInternal; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.TransactionRollbackException; +import io.vertx.sqlclient.transaction.Transaction; +import io.vertx.sqlclient.transaction.TransactionOptions; +import io.vertx.sqlclient.transaction.TransactionRollbackException; import io.vertx.sqlclient.impl.command.CommandBase; +import io.vertx.sqlclient.impl.command.StartTxCommand; import io.vertx.sqlclient.impl.command.TxCommand; class TransactionImpl implements Transaction { @@ -62,9 +64,9 @@ static class ScheduledCommand { } } - Future begin() { + Future begin(TransactionOptions transactionOptions) { PromiseInternal promise = context.promise(this::afterBegin); - ScheduledCommand b = doQuery(new TxCommand<>(TxCommand.Kind.BEGIN, this), promise); + ScheduledCommand b = doQuery(new StartTxCommand<>(TxCommand.Kind.BEGIN, this, transactionOptions), promise); doSchedule(b.cmd, b.handler); return promise.future(); } diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/StartTxCommand.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/StartTxCommand.java new file mode 100644 index 000000000..8d67b9558 --- /dev/null +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/StartTxCommand.java @@ -0,0 +1,16 @@ +package io.vertx.sqlclient.impl.command; + +import io.vertx.sqlclient.transaction.TransactionAccessMode; +import io.vertx.sqlclient.transaction.TransactionIsolationLevel; +import io.vertx.sqlclient.transaction.TransactionOptions; + +public class StartTxCommand extends TxCommand { + public final TransactionIsolationLevel isolationLevel; + public final TransactionAccessMode accessMode; + + public StartTxCommand(Kind kind, R result, TransactionOptions transactionOptions) { + super(kind, result); + this.isolationLevel = transactionOptions.getIsolationLevel(); + this.accessMode = transactionOptions.getAccessMode(); + } +} diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/TxCommand.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/TxCommand.java index 96a05a84d..1276eec1c 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/TxCommand.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/command/TxCommand.java @@ -14,14 +14,7 @@ public class TxCommand extends CommandBase { public enum Kind { - - BEGIN(), ROLLBACK(), COMMIT(); - - public final String sql; - - Kind() { - this.sql = name(); - } + BEGIN, ROLLBACK, COMMIT } public final R result; diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/Transaction.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/Transaction.java similarity index 97% rename from vertx-sql-client/src/main/java/io/vertx/sqlclient/Transaction.java rename to vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/Transaction.java index 409638672..6524a5990 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/Transaction.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/Transaction.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.vertx.sqlclient; +package io.vertx.sqlclient.transaction; import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.AsyncResult; diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionAccessMode.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionAccessMode.java new file mode 100644 index 000000000..11851636d --- /dev/null +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionAccessMode.java @@ -0,0 +1,21 @@ +package io.vertx.sqlclient.transaction; + +import io.vertx.codegen.annotations.VertxGen; + +/** + * Transaction access mode is the transaction characteristic which can be used to control whether the transaction is read/write or read only. + */ +@VertxGen +public enum TransactionAccessMode { + + /** + * Indicate the transaction is in read write mode + */ + READ_WRITE, + + /** + * Indicate the transaction is in read only mode + */ + READ_ONLY; + +} diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionIsolationLevel.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionIsolationLevel.java new file mode 100644 index 000000000..4681d007b --- /dev/null +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionIsolationLevel.java @@ -0,0 +1,39 @@ +package io.vertx.sqlclient.transaction; + +import io.vertx.codegen.annotations.VertxGen; + +/** + * Transaction isolation level is the transaction characteristic to determine the visibility of data in a transaction to other transactions running concurrently. + */ +@VertxGen +public enum TransactionIsolationLevel { + + /** + * Implements dirty read, or isolation level 0 locking, which means that no shared locks are issued and no exclusive + * locks are honored. When this option is set, it is possible to read uncommitted or dirty data; values in the data + * can be changed and rows can appear or disappear in the data set before the end of the transaction. This is the + * least restrictive of the four isolation levels. + */ + READ_UNCOMMITTED, + + /** + * Specifies that shared locks are held while the data is being read to avoid dirty reads, but the data can be changed + * before the end of the transaction, resulting in nonrepeatable reads or phantom data. + */ + READ_COMMITTED, + + /** + * Locks are placed on all data that is used in a query, preventing other users from updating the data, but new + * phantom rows can be inserted into the data set by another user and are included in later reads in the current + * transaction. Because concurrency is lower than the default isolation level, use this option only when necessary. + */ + REPEATABLE_READ, + + /** + * Places a range lock on the data set, preventing other users from updating or inserting rows into the data set until + * the transaction is complete. This is the most restrictive of the four isolation levels. Because concurrency is + * lower, use this option only when necessary. + */ + SERIALIZABLE; + +} diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionOptions.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionOptions.java new file mode 100644 index 000000000..e46328700 --- /dev/null +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionOptions.java @@ -0,0 +1,79 @@ +package io.vertx.sqlclient.transaction; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; + +/** + * Transaction options which could be used to control the characteristics at the start of transaction. + */ +@DataObject(generateConverter = true) +public class TransactionOptions { + + public static final TransactionOptions DEFAULT_TX_OPTIONS = new TransactionOptions(); + + private TransactionIsolationLevel isolationLevel; + private TransactionAccessMode accessMode; + + public TransactionOptions() { + } + + public TransactionOptions(JsonObject json) { + TransactionOptionsConverter.fromJson(json, this); + } + + public TransactionOptions(TransactionIsolationLevel isolationLevel, TransactionAccessMode accessMode) { + this.isolationLevel = isolationLevel; + this.accessMode = accessMode; + } + + public TransactionOptions(TransactionOptions other) { + this.isolationLevel = other.isolationLevel; + this.accessMode = other.accessMode; + } + + /** + * Set the {@link TransactionAccessMode transaction access mode} in the options. + * + * @param accessMode the access mode + * @return a reference to this, so the API can be used fluently + */ + public TransactionOptions setAccessMode(TransactionAccessMode accessMode) { + this.accessMode = accessMode; + return this; + } + + /** + * Get the {@link TransactionAccessMode transaction access mode} in the options. + * + * @return the transaction access mode + */ + public TransactionAccessMode getAccessMode() { + return accessMode; + } + + /** + * Set the {@link TransactionIsolationLevel transaction isolation level} in the options. + * + * @param isolationLevel the isolation level to specify + * @return a reference to this, so the API can be used fluently + */ + public TransactionOptions setIsolationLevel(TransactionIsolationLevel isolationLevel) { + this.isolationLevel = isolationLevel; + return this; + } + + /** + * Get the {@link TransactionIsolationLevel transaction isolation level} in the options. + * + * @return the isolation level + */ + public TransactionIsolationLevel getIsolationLevel() { + return isolationLevel; + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + TransactionOptionsConverter.toJson(this, json); + return json; + } +} diff --git a/vertx-sql-client/src/main/java/io/vertx/sqlclient/TransactionRollbackException.java b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionRollbackException.java similarity index 96% rename from vertx-sql-client/src/main/java/io/vertx/sqlclient/TransactionRollbackException.java rename to vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionRollbackException.java index cdf9c2285..39a48ce6a 100644 --- a/vertx-sql-client/src/main/java/io/vertx/sqlclient/TransactionRollbackException.java +++ b/vertx-sql-client/src/main/java/io/vertx/sqlclient/transaction/TransactionRollbackException.java @@ -14,7 +14,7 @@ * limitations under the License. * */ -package io.vertx.sqlclient; +package io.vertx.sqlclient.transaction; import io.vertx.core.impl.NoStackTraceThrowable; diff --git a/vertx-sql-client/src/test/java/io/vertx/sqlclient/tck/TransactionTestBase.java b/vertx-sql-client/src/test/java/io/vertx/sqlclient/tck/TransactionTestBase.java index 4d50d5b02..4eb262b73 100644 --- a/vertx-sql-client/src/test/java/io/vertx/sqlclient/tck/TransactionTestBase.java +++ b/vertx-sql-client/src/test/java/io/vertx/sqlclient/tck/TransactionTestBase.java @@ -18,9 +18,8 @@ import java.util.function.Consumer; import io.vertx.core.Future; -import io.vertx.sqlclient.SqlClient; -import io.vertx.sqlclient.SqlConnection; -import io.vertx.sqlclient.TransactionRollbackException; +import io.vertx.sqlclient.*; +import io.vertx.sqlclient.transaction.*; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,10 +29,6 @@ import io.vertx.core.Vertx; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.Transaction; -import io.vertx.sqlclient.Tuple; public abstract class TransactionTestBase { @@ -314,4 +309,33 @@ public void testWithTransactionImplicitRollback(TestContext ctx) { })); }))); } + + + @Test + public void testStartReadOnlyTransaction(TestContext ctx) { + Async async = ctx.async(); + getPool().getConnection(ctx.asyncAssertSuccess(conn -> { + conn.begin(new TransactionOptions().setAccessMode(TransactionAccessMode.READ_ONLY), ctx.asyncAssertSuccess(transaction -> { + conn.query("INSERT INTO mutable (id, val) VALUES (1, 'hello-1')") + .execute(ctx.asyncAssertFailure(error -> { + // read-only transactions + transaction.rollback(); + conn.close(); + async.complete(); + })); + })); + })); + } + + @Test + public void testWithReadOnlyTransactionStart(TestContext ctx) { + Async async = ctx.async(); + getPool().withTransaction(new TransactionOptions().setAccessMode(TransactionAccessMode.READ_ONLY), client -> client + .query("INSERT INTO mutable (id, val) VALUES (1, 'hello-1')") + .execute() + .onComplete(ctx.asyncAssertFailure(error -> { + // read-only transactions + async.complete(); + }))); + } }