Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for customizing transaction start #643

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions vertx-db2-client/src/main/java/examples/SqlClientExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> 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<String, String> properties,
Promise<Connection> completionHandler) {
InitialHandshakeCommand cmd = new InitialHandshakeCommand(this, username, password, database, properties);
Expand All @@ -70,12 +67,22 @@ protected <R> void doSchedule(CommandBase<R> cmd, Handler<AsyncResult<R>> handle
if (cmd instanceof TxCommand) {
TxCommand<R> txCmd = (TxCommand<R>) 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<R> startTxCommand = (StartTxCommand<R>) txCmd;
if (startTxCommand.isolationLevel != null || startTxCommand.accessMode != null) {
// customized transaction
String sql = TransactionSqlBuilder.buildSetTxIsolationLevelSql(startTxCommand.isolationLevel, startTxCommand.accessMode);
SimpleQueryCommand<Void> 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<Void> cmd2 = new SimpleQueryCommand<>(txCmd.kind.sql, false, false,
SimpleQueryCommand<Void> cmd2 = new SimpleQueryCommand<>(txCmd.kind.name(), false, false,
QueryCommandBase.NULL_COLLECTOR, QueryResultHandler.NOOP_HANDLER);
super.doSchedule(cmd2, ar -> handler.handle(ar.map(txCmd.result)));

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -38,7 +35,7 @@ public class DB2TransactionTest extends TransactionTestBase {

@ClassRule
public static DB2Resource rule = DB2Resource.SHARED_INSTANCE;

@Rule
public TestName testName = new TestName();

Expand Down Expand Up @@ -74,4 +71,19 @@ public void testDelayedCommit(TestContext ctx) {
super.testDelayedCommit(ctx);
}

@Override
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aguibert I failed to make these tests pass, can you help with this? I tried to set transaction characteristic but I got this

>>> BEGIN DB2TransactionTest.testStartReadOnlyTransaction
[2020-05-18 23:53:30] 详细 io.vertx.db2client.impl.codec.DB2Encoder                >>> ENCODE SimpleQueryCommandCodec@2e09e4e1 sql=SET TRANSACTION READ ONLY, autoCommit=false, section=null
[2020-05-18 23:53:30] 详细 io.vertx.db2client.impl.codec.DB2Decoder                <<< DECODE SimpleQueryCommandCodec@2e09e4e1 sql=SET TRANSACTION READ ONLY, autoCommit=false, section=io.vertx.db2client.impl.drda.Section$ImmediateSection@7b071208{packageName=SYSLH200, sectionNumber=385, cursorName=SQL_CURLH200C} (123 bytes)

io.vertx.db2client.DB2Exception: The SQL syntax provided was invalid

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB2 doesn't support setting a transaction as read only via SQL. Instead, attributes like read-only and isolation level are typically specified as "query attributes" which can be appended to the SQL. For example:

  SELECT DEPTNO, DEPTNAME, MGRNO
    FROM DEPT
    WHERE ADMRDEPT ='A00'
    FOR READ ONLY WITH RS

@Ignore
@Test
public void testStartReadOnlyTransaction(TestContext ctx) {
// FIXME
super.testStartReadOnlyTransaction(ctx);
}

@Override
@Ignore
@Test
public void testWithReadOnlyTransactionStart(TestContext ctx) {
// FIXME
super.testWithReadOnlyTransactionStart(ctx);
}
}
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on some quick experiments I ran locally this is not supported by DB2. Instead I think we need to use syntax like this:
https://www.ibm.com/support/knowledgecenter/SSEPGG_11.5.0/com.ibm.db2.luw.sql.ref.doc/doc/r0010944.html

Before we add code like this, it would be good to have some more functional tests in place

}

@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);
}
}
32 changes: 32 additions & 0 deletions vertx-mssql-client/src/main/java/examples/SqlClientExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
45 changes: 33 additions & 12 deletions vertx-mysql-client/src/main/java/examples/SqlClientExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
Loading