diff --git a/docs/_docs/SQL/JDBC/jdbc-driver.adoc b/docs/_docs/SQL/JDBC/jdbc-driver.adoc index 207ab57b1a2db..b8946694a03b1 100644 --- a/docs/_docs/SQL/JDBC/jdbc-driver.adoc +++ b/docs/_docs/SQL/JDBC/jdbc-driver.adoc @@ -555,6 +555,46 @@ In addition to generic DataSource properties, `IgniteJdbcThinDataSource` support Refer to the link:{javadoc_base_url}/org/apache/ignite/IgniteJdbcThinDataSource.html[JavaDocs] for more details. +== Transaction Savepoints + +JDBC Thin Driver supports the standard JDBC savepoint API for explicit transactions: + +* `Connection.setSavepoint()` +* `Connection.setSavepoint(String name)` +* `Connection.rollback(Savepoint savepoint)` +* `Connection.releaseSavepoint(Savepoint savepoint)` + +Savepoints are available for JDBC connections that use the Calcite-based SQL engine and explicit `PESSIMISTIC` transactions. +Disable auto-commit before creating a savepoint. + +[source,java] +---- +try (Connection conn = DriverManager.getConnection( + "jdbc:ignite:thin://127.0.0.1?transactionConcurrency=PESSIMISTIC")) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("INSERT INTO Person(id, name) VALUES (1, 'John')"); + + Savepoint savepoint = conn.setSavepoint("before_update"); + + stmt.executeUpdate("UPDATE Person SET name = 'Jane' WHERE id = 1"); + + conn.rollback(savepoint); + conn.releaseSavepoint(savepoint); + conn.commit(); + } + catch (Throwable t) { + conn.rollback(); + + throw t; + } +} +---- + +You can also use SQL savepoint commands, such as `SAVEPOINT` and `ROLLBACK TO SAVEPOINT`, from JDBC statements. +See link:sql-reference/transactions[Transactions, window=_blank] for SQL syntax and usage details. + == Examples To start processing the data located in the cluster, you need to create a JDBC Connection object via one of the methods below: diff --git a/docs/_docs/SQL/sql-calcite.adoc b/docs/_docs/SQL/sql-calcite.adoc index 504080d9e9b19..2dead7850270c 100644 --- a/docs/_docs/SQL/sql-calcite.adoc +++ b/docs/_docs/SQL/sql-calcite.adoc @@ -136,6 +136,7 @@ In most cases, statement syntax is compliant with the old SQL engine. But there === Transactions The Calcite-based SQL engine supports SQL savepoint commands for explicit transactions. See link:sql-reference/transactions[Transactions, window=_blank] for syntax and usage details. +JDBC connections can also use the standard JDBC savepoint API. See link:SQL/JDBC/jdbc-driver#transaction-savepoints[JDBC Transaction Savepoints, window=_blank] for details. === Supported Functions diff --git a/modules/calcite/pom.xml b/modules/calcite/pom.xml index 9b25987a685f7..538997e72418d 100644 --- a/modules/calcite/pom.xml +++ b/modules/calcite/pom.xml @@ -236,12 +236,6 @@ test - - ${project.groupId} - ignite-clients - test - - org.mockito mockito-core diff --git a/modules/clients/pom.xml b/modules/clients/pom.xml index 851c763fd3228..08d0798a66006 100644 --- a/modules/clients/pom.xml +++ b/modules/clients/pom.xml @@ -73,6 +73,12 @@ test + + ${project.groupId} + ignite-calcite + test + + ${project.groupId} ignite-log4j2 diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java index bc9e274523c8e..fb04357e82a1d 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcDriverTestSuite.java @@ -43,6 +43,7 @@ import org.apache.ignite.jdbc.thin.JdbcThinConnectionMultipleAddressesTest; import org.apache.ignite.jdbc.thin.JdbcThinConnectionPropertiesTest; import org.apache.ignite.jdbc.thin.JdbcThinConnectionSSLTest; +import org.apache.ignite.jdbc.thin.JdbcThinConnectionSavepointTest; import org.apache.ignite.jdbc.thin.JdbcThinConnectionSelfTest; import org.apache.ignite.jdbc.thin.JdbcThinConnectionTimeoutSelfTest; import org.apache.ignite.jdbc.thin.JdbcThinDataPageScanPropertySelfTest; @@ -146,6 +147,7 @@ // New thin JDBC JdbcThinConnectionSelfTest.class, + JdbcThinConnectionSavepointTest.class, JdbcThinConnectionMultipleAddressesTest.class, JdbcThinTcpIoTest.class, JdbcThinConnectionAdditionalSecurityTest.class, diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcThinDriverPartitionAwarenessTestSuite.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcThinDriverPartitionAwarenessTestSuite.java index 69722306e26d1..43b952a9c5b53 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcThinDriverPartitionAwarenessTestSuite.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/suite/IgniteJdbcThinDriverPartitionAwarenessTestSuite.java @@ -18,6 +18,7 @@ package org.apache.ignite.jdbc.suite; import org.apache.ignite.jdbc.thin.JdbcThinAbstractSelfTest; +import org.apache.ignite.jdbc.thin.JdbcThinConnectionSavepointTest; import org.apache.ignite.jdbc.thin.JdbcThinConnectionSelfTest; import org.apache.ignite.jdbc.thin.JdbcThinPartitionAwarenessReconnectionAndFailoverSelfTest; import org.apache.ignite.jdbc.thin.JdbcThinPartitionAwarenessSelfTest; @@ -34,6 +35,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ JdbcThinConnectionSelfTest.class, + JdbcThinConnectionSavepointTest.class, JdbcThinTcpIoTest.class, JdbcThinStatementSelfTest.class, JdbcThinPartitionAwarenessSelfTest.class, diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSavepointTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSavepointTest.java new file mode 100644 index 0000000000000..98750a8dde00f --- /dev/null +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSavepointTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.jdbc.thin; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; +import org.apache.ignite.calcite.CalciteQueryEngineConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.configuration.SqlConfiguration; +import org.apache.ignite.configuration.TransactionConfiguration; +import org.junit.Test; + +/** Savepoint tests for thin JDBC connection. */ +public class JdbcThinConnectionSavepointTest extends JdbcThinAbstractSelfTest { + /** */ + private static final String TBL = "SAVEPOINT_TEST_TABLE"; + + /** URL. */ + private String url = partitionAwareness ? + "jdbc:ignite:thin://127.0.0.1:10800..10802" : + "jdbc:ignite:thin://127.0.0.1"; + + /** Nodes count. */ + private int nodesCnt = partitionAwareness ? 4 : 2; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + return super.getConfiguration(igniteInstanceName) + .setTransactionConfiguration(new TransactionConfiguration() + .setTxAwareQueriesEnabled(true)) + .setSqlConfiguration(new SqlConfiguration() + .setQueryEnginesConfiguration(new CalciteQueryEngineConfiguration())); + } + + /** {@inheritDoc} */ + @Override protected void beforeTestsStarted() throws Exception { + super.beforeTestsStarted(); + + startGridsMultiThreaded(nodesCnt); + } + + /** {@inheritDoc} */ + @Override protected void afterTestsStopped() throws Exception { + stopAllGrids(); + + super.afterTestsStopped(); + } + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + super.beforeTest(); + + try (Connection conn = connection()) { + execute(conn, "DROP TABLE IF EXISTS " + TBL); + execute(conn, "CREATE TABLE " + TBL + "(ID INT PRIMARY KEY, VAL VARCHAR) WITH atomicity=transactional"); + } + } + + /** */ + @Test + public void testJdbcSavepointApiRollsBackSqlDmlChanges() throws Exception { + try (Connection conn = connection()) { + assertTrue(conn.getMetaData().supportsSavepoints()); + + conn.setAutoCommit(false); + + try { + execute(conn, "INSERT INTO " + TBL + " VALUES (1, 'before_sp1')"); + + Savepoint sp1 = conn.setSavepoint("sp1"); + + execute(conn, "UPDATE " + TBL + " SET VAL = 'after_sp1' WHERE ID = 1"); + execute(conn, "INSERT INTO " + TBL + " VALUES (2, 'after_sp1')"); + + Savepoint sp2 = conn.setSavepoint("sp2"); + + execute(conn, "DELETE FROM " + TBL + " WHERE ID = 1"); + execute(conn, "INSERT INTO " + TBL + " VALUES (3, 'after_sp2')"); + + assertQuery(conn, 2, "after_sp1", 3, "after_sp2"); + + conn.rollback(sp2); + + assertQuery(conn, 1, "after_sp1", 2, "after_sp1"); + + conn.releaseSavepoint(sp2); + conn.rollback(sp1); + + assertQuery(conn, 1, "before_sp1"); + + conn.releaseSavepoint(sp1); + conn.commit(); + } + catch (Throwable t) { + conn.rollback(); + + throw t; + } + } + + try (Connection conn = connection()) { + assertQuery(conn, 1, "before_sp1"); + } + } + + /** */ + @Test + public void testJdbcSavepointCanStartTransactionBeforeSqlDml() throws Exception { + try (Connection conn = connection()) { + conn.setAutoCommit(false); + + try { + Savepoint sp1 = conn.setSavepoint("sp1"); + + execute(conn, "INSERT INTO " + TBL + " VALUES (1, 'after_sp1')"); + + assertQuery(conn, 1, "after_sp1"); + + conn.rollback(sp1); + conn.commit(); + } + catch (Throwable t) { + conn.rollback(); + + throw t; + } + } + + try (Connection conn = connection()) { + assertQuery(conn); + } + } + + /** */ + @Test + public void testJdbcUnnamedSavepointApiRollsBackSqlDmlChanges() throws Exception { + try (Connection conn = connection()) { + conn.setAutoCommit(false); + + try { + execute(conn, "INSERT INTO " + TBL + " VALUES (1, 'before_sp')"); + + Savepoint sp = conn.setSavepoint(); + + execute(conn, "UPDATE " + TBL + " SET VAL = 'after_sp' WHERE ID = 1"); + execute(conn, "INSERT INTO " + TBL + " VALUES (2, 'after_sp')"); + + assertQuery(conn, 1, "after_sp", 2, "after_sp"); + + conn.rollback(sp); + + assertQuery(conn, 1, "before_sp"); + + conn.releaseSavepoint(sp); + conn.commit(); + } + catch (Throwable t) { + conn.rollback(); + + throw t; + } + } + + try (Connection conn = connection()) { + assertQuery(conn, 1, "before_sp"); + } + } + + /** */ + @Test + public void testSqlDmlChangesCanBeRolledBackToSavepointUsingJdbc() throws Exception { + try (Connection conn = connection(); Statement stmt = conn.createStatement()) { + conn.setAutoCommit(false); + + try { + stmt.executeUpdate("INSERT INTO " + TBL + " VALUES (1, 'before_sp1')"); + + stmt.execute("SAVEPOINT sp1"); + + stmt.executeUpdate("UPDATE " + TBL + " SET VAL = 'after_sp1' WHERE ID = 1"); + stmt.executeUpdate("INSERT INTO " + TBL + " VALUES (2, 'after_sp1')"); + + stmt.execute("SAVEPOINT sp2"); + + stmt.executeUpdate("DELETE FROM " + TBL + " WHERE ID = 1"); + stmt.executeUpdate("INSERT INTO " + TBL + " VALUES (3, 'after_sp2')"); + + assertQuery(conn, 2, "after_sp1", 3, "after_sp2"); + + stmt.execute("ROLLBACK TO SAVEPOINT sp2"); + + assertQuery(conn, 1, "after_sp1", 2, "after_sp1"); + + stmt.execute("ROLLBACK TO SAVEPOINT sp1"); + + Savepoint sp = conn.setSavepoint("sp1"); + conn.rollback(sp); + conn.releaseSavepoint(sp); + + assertQuery(conn, 1, "before_sp1"); + + conn.commit(); + } + catch (Throwable t) { + conn.rollback(); + + throw t; + } + } + + try (Connection conn = connection()) { + assertQuery(conn, 1, "before_sp1"); + } + } + + /** + * @return Connection. + */ + private Connection connection() throws SQLException { + return DriverManager.getConnection(url + "?partitionAwareness=" + partitionAwareness + + "&transactionConcurrency=PESSIMISTIC"); + } + + /** + * @param conn Connection. + * @param exp Expected values as column pairs. + */ + private void assertQuery(Connection conn, Object... exp) throws SQLException { + List> rows = execute(conn, "SELECT ID, VAL FROM " + TBL + " ORDER BY ID"); + + assertEquals(exp.length / 2, rows.size()); + + for (int i = 0; i < exp.length; i += 2) + assertEqualsCollections(Arrays.asList(exp[i], exp[i + 1]), rows.get(i / 2)); + } +} diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java index a697ffa4e2541..c15aaf6892042 100644 --- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java +++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinConnectionSelfTest.java @@ -1542,7 +1542,7 @@ public void testGetSetHoldability() throws Exception { @Test public void testSetSavepoint() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - assert !conn.getMetaData().supportsSavepoints(); + assert conn.getMetaData().supportsSavepoints(); // Disallowed in auto-commit mode assertThrows(log, @@ -1573,7 +1573,7 @@ public void testSetSavepoint() throws Exception { @Test public void testSetSavepointName() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - assert !conn.getMetaData().supportsSavepoints(); + assert conn.getMetaData().supportsSavepoints(); // Invalid arg assertThrows(log, @@ -1619,7 +1619,7 @@ public void testSetSavepointName() throws Exception { @Test public void testRollbackSavePoint() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - assert !conn.getMetaData().supportsSavepoints(); + assert conn.getMetaData().supportsSavepoints(); // Invalid arg assertThrows(log, @@ -1678,7 +1678,7 @@ public void testDisabledFeatures() throws Exception { @Test public void testReleaseSavepoint() throws Exception { try (Connection conn = DriverManager.getConnection(urlWithPartitionAwarenessProp)) { - assert !conn.getMetaData().supportsSavepoints(); + assert conn.getMetaData().supportsSavepoints(); // Invalid arg assertThrows(log, @@ -1695,11 +1695,17 @@ public void testReleaseSavepoint() throws Exception { final Savepoint savepoint = getFakeSavepoint(); - checkNotSupported(new RunnableX() { - @Override public void runx() throws Exception { - conn.releaseSavepoint(savepoint); - } - }); + assertThrows(log, + new Callable() { + @Override public Object call() throws Exception { + conn.releaseSavepoint(savepoint); + + return null; + } + }, + SQLException.class, + "Invalid savepoint" + ); conn.close(); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java index e3771824b5e93..265ac8542b1e9 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java @@ -113,6 +113,8 @@ import org.apache.ignite.internal.processors.odbc.jdbc.JdbcSetTxParametersRequest; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcStatementType; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcTxEndRequest; +import org.apache.ignite.internal.processors.odbc.jdbc.JdbcTxSavepointRequest; +import org.apache.ignite.internal.processors.odbc.jdbc.JdbcTxSavepointResult; import org.apache.ignite.internal.processors.odbc.jdbc.JdbcUpdateBinarySchemaResult; import org.apache.ignite.internal.sql.command.SqlCommand; import org.apache.ignite.internal.sql.command.SqlSetStreamingCommand; @@ -182,6 +184,9 @@ public class JdbcThinConnection implements Connection { /** No retries. */ public static final int NO_RETRIES = 0; + /** Savepoint name generator. */ + private static final AtomicLong SAVEPOINT_ID_GEN = new AtomicLong(); + /** Default isolation level. */ public static final int DFLT_ISOLATION = TRANSACTION_READ_COMMITTED; @@ -810,7 +815,11 @@ private void checkCursorOptions(int resSetType, int resSetConcurrency, int resSe if (autoCommit) throw new SQLException("Savepoint cannot be set in auto-commit mode."); - throw new SQLFeatureNotSupportedException("Savepoints are not supported."); + String name = "JDBC_SAVEPOINT_" + SAVEPOINT_ID_GEN.incrementAndGet(); + + savepoint(JdbcTxSavepointRequest.SAVEPOINT, name); + + return new JdbcThinSavepoint(name, false); } /** {@inheritDoc} */ @@ -823,7 +832,9 @@ private void checkCursorOptions(int resSetType, int resSetConcurrency, int resSe if (autoCommit) throw new SQLException("Savepoint cannot be set in auto-commit mode."); - throw new SQLFeatureNotSupportedException("Savepoints are not supported."); + savepoint(JdbcTxSavepointRequest.SAVEPOINT, name); + + return new JdbcThinSavepoint(name, true); } /** {@inheritDoc} */ @@ -836,7 +847,7 @@ private void checkCursorOptions(int resSetType, int resSetConcurrency, int resSe if (autoCommit) throw new SQLException("Auto-commit mode."); - throw new SQLFeatureNotSupportedException("Savepoints are not supported."); + savepoint(JdbcTxSavepointRequest.ROLLBACK_TO_SAVEPOINT, savepointName(savepoint)); } /** {@inheritDoc} */ @@ -846,7 +857,9 @@ private void checkCursorOptions(int resSetType, int resSetConcurrency, int resSe if (savepoint == null) throw new SQLException("Savepoint cannot be null."); - throw new SQLFeatureNotSupportedException("Savepoints are not supported."); + savepoint(JdbcTxSavepointRequest.RELEASE_SAVEPOINT, savepointName(savepoint)); + + ((JdbcThinSavepoint)savepoint).release(); } /** {@inheritDoc} */ @@ -1066,6 +1079,49 @@ public int txId() { return txCtx == null ? NONE_TX : txCtx.txId; } + /** + * Execute savepoint operation. + * + * @param op Operation. + * @param name Savepoint name. + * @throws SQLException If failed. + */ + private void savepoint(byte op, String name) throws SQLException { + if (!txEnabledForConnection()) { + logTransactionWarning(); + + throw new SQLFeatureNotSupportedException("Savepoints are not supported."); + } + + if (txCtx == null && op != JdbcTxSavepointRequest.SAVEPOINT) + throw new SQLException("Transaction not found"); + + JdbcResultWithIo res = sendRequest(new JdbcTxSavepointRequest(txId(), op, name), null, + txCtx == null ? null : txCtx.txIo); + + JdbcTxSavepointResult savepointRes = res.response(); + + if (txCtx == null) + txCtx = new TxContext(res.cliIo(), savepointRes.txId()); + else if (txCtx.txId != savepointRes.txId()) { + throw new IllegalStateException("Unexpected transaction id for savepoint operation [" + + "txCtx.txId=" + txCtx.txId + + ", res.txId=" + savepointRes.txId() + ']'); + } + } + + /** + * @param savepoint Savepoint. + * @return Savepoint name. + * @throws SQLException If savepoint is invalid. + */ + private static String savepointName(Savepoint savepoint) throws SQLException { + if (!(savepoint instanceof JdbcThinSavepoint)) + throw new SQLException("Invalid savepoint."); + + return ((JdbcThinSavepoint)savepoint).name(); + } + /** * Ensures that connection is not closed. * @@ -2703,6 +2759,59 @@ public void remove(JdbcThinStatement stmt) { } } + /** JDBC thin savepoint. */ + private static class JdbcThinSavepoint implements Savepoint { + /** Savepoint name. */ + private final String name; + + /** Named savepoint flag. */ + private final boolean named; + + /** Released flag. */ + private boolean released; + + /** + * @param name Savepoint name used by Ignite transaction. + * @param named Whether savepoint was created as named JDBC savepoint. + */ + private JdbcThinSavepoint(String name, boolean named) { + this.name = name; + this.named = named; + } + + /** @return Savepoint name used by Ignite transaction. */ + private String name() throws SQLException { + if (released) + throw new SQLException("Savepoint has been released."); + + return name; + } + + /** Mark savepoint as released. */ + private void release() { + released = true; + } + + /** {@inheritDoc} */ + @Override public int getSavepointId() throws SQLException { + if (named) + throw new SQLException("Named savepoint does not have an id."); + + if (released) + throw new SQLException("Savepoint has been released."); + + return (int)Long.parseLong(name.substring("JDBC_SAVEPOINT_".length())); + } + + /** {@inheritDoc} */ + @Override public String getSavepointName() throws SQLException { + if (!named) + throw new SQLException("Unnamed savepoint does not have a name."); + + return name(); + } + } + /** */ public static TransactionIsolation isolation(int jdbcIsolation) throws SQLException { switch (jdbcIsolation) { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java index 3758e9625a3ec..48490eb7cf3d8 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java @@ -1212,7 +1212,7 @@ public class JdbcThinDatabaseMetadata implements DatabaseMetaData { /** {@inheritDoc} */ @Override public boolean supportsSavepoints() throws SQLException { - return false; + return true; } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequest.java index 81f8e56ad766e..9d25ac4e27ee2 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequest.java @@ -94,6 +94,9 @@ public class JdbcRequest extends ClientListenerRequestNoId implements JdbcRawBin /** Finish transaction request. */ public static final byte TX_END = 22; + /** Savepoint operation request. */ + public static final byte TX_SAVEPOINT = 23; + /** Request Id generator. */ private static final AtomicLong REQ_ID_GENERATOR = new AtomicLong(); @@ -265,6 +268,11 @@ public static JdbcRequest readRequest( break; + case TX_SAVEPOINT: + req = new JdbcTxSavepointRequest(); + + break; + default: throw new IgniteException("Unknown SQL listener request ID: [request ID=" + reqType + ']'); } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java index 451dfdcc79a49..b999242274b3d 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java @@ -115,6 +115,7 @@ import static org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest.QRY_FETCH; import static org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest.QRY_META; import static org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest.TX_END; +import static org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest.TX_SAVEPOINT; import static org.apache.ignite.internal.processors.odbc.jdbc.JdbcRequest.TX_SET_PARAMS; /** @@ -396,6 +397,10 @@ JdbcResponse doHandle(JdbcRequest req) { resp = endTransaction((JdbcTxEndRequest)req); break; + case TX_SAVEPOINT: + resp = savepoint((JdbcTxSavepointRequest)req); + break; + default: resp = new JdbcResponse(IgniteQueryErrorCode.UNSUPPORTED_OPERATION, "Unsupported JDBC request [req=" + req + ']'); @@ -1473,6 +1478,85 @@ private JdbcResponse endTransaction(JdbcTxEndRequest req) { } } + /** + * Execute transaction savepoint operation. + * + * @param req Request. + * @return Resulting {@link JdbcResponse}. + */ + private JdbcResponse savepoint(JdbcTxSavepointRequest req) { + int txId = req.txId(); + boolean txStarted = false; + + try { + if (txId == NONE_TX) { + if (req.operation() != JdbcTxSavepointRequest.SAVEPOINT) + throw transactionNotFoundException(); + + txId = startClientTransaction( + connCtx, + cliCtx.concurrency(), + cliCtx.isolation(), + cliCtx.transactionTimeout(), + cliCtx.transactionLabel(), + cliCtx.applicationAttributes() + ); + + txStarted = true; + } + + ClientTxContext txCtx = connCtx.txContext(txId); + + if (txCtx == null) + throw transactionNotFoundException(); + + txCtx.acquire(true); + + try { + switch (req.operation()) { + case JdbcTxSavepointRequest.SAVEPOINT: + txCtx.tx().savepoint(req.name(), false); + + break; + + case JdbcTxSavepointRequest.ROLLBACK_TO_SAVEPOINT: + txCtx.tx().rollbackToSavepoint(req.name()); + + break; + + case JdbcTxSavepointRequest.RELEASE_SAVEPOINT: + txCtx.tx().releaseSavepoint(req.name()); + + break; + + default: + throw new IgniteSQLException("Unsupported savepoint operation: " + req.operation(), + IgniteQueryErrorCode.UNSUPPORTED_OPERATION); + } + } + finally { + txCtx.release(true); + } + + return resultToResonse(new JdbcTxSavepointResult(req.requestId(), txId)); + } + catch (Exception e) { + if (txStarted) { + try { + endTxAsync(connCtx, txId, false).get(); + } + catch (Exception e0) { + e.addSuppressed(e0); + } + } + + U.error(log, "Failed to execute transaction savepoint operation [reqId=" + req.requestId() + + ", req=" + req + ']', e); + + return exceptionToResult(e); + } + } + /** * Create {@link JdbcResponse} bearing appropriate Ignite specific result code if possible * from given {@link Exception}. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcResult.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcResult.java index 390b4890c9dd0..401663738bfb6 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcResult.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcResult.java @@ -93,6 +93,9 @@ public class JdbcResult implements JdbcRawBinarylizable { /** End transaction response. */ static final byte TX_END = 24; + /** Savepoint operation response. */ + static final byte TX_SAVEPOINT = 25; + /** Success status. */ private byte type; @@ -243,6 +246,11 @@ public static JdbcResult readResult( break; + case TX_SAVEPOINT: + res = new JdbcTxSavepointResult(); + + break; + default: throw new IgniteException("Unknown SQL listener request ID: [request ID=" + resId + ']'); } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointRequest.java new file mode 100644 index 0000000000000..744a9ded77882 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointRequest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.odbc.jdbc; + +import org.apache.ignite.binary.BinaryObjectException; +import org.apache.ignite.internal.binary.BinaryReaderEx; +import org.apache.ignite.internal.binary.BinaryWriterEx; +import org.apache.ignite.internal.util.typedef.internal.S; + +/** JDBC transaction savepoint request. */ +public class JdbcTxSavepointRequest extends JdbcRequest { + /** Create savepoint operation. */ + public static final byte SAVEPOINT = 0; + + /** Rollback to savepoint operation. */ + public static final byte ROLLBACK_TO_SAVEPOINT = 1; + + /** Release savepoint operation. */ + public static final byte RELEASE_SAVEPOINT = 2; + + /** Transaction id. */ + private int txId; + + /** Operation. */ + private byte op; + + /** Savepoint name. */ + private String name; + + /** Default constructor is used for deserialization. */ + public JdbcTxSavepointRequest() { + super(TX_SAVEPOINT); + } + + /** + * @param txId Transaction id. + * @param op Operation. + * @param name Savepoint name. + */ + public JdbcTxSavepointRequest(int txId, byte op, String name) { + this(); + + this.txId = txId; + this.op = op; + this.name = name; + } + + /** @return Transaction id. */ + public int txId() { + return txId; + } + + /** @return Operation. */ + public byte operation() { + return op; + } + + /** @return Savepoint name. */ + public String name() { + return name; + } + + /** {@inheritDoc} */ + @Override public void writeBinary(BinaryWriterEx writer, JdbcProtocolContext protoCtx) + throws BinaryObjectException { + super.writeBinary(writer, protoCtx); + + writer.writeInt(txId); + writer.writeByte(op); + writer.writeString(name); + } + + /** {@inheritDoc} */ + @Override public void readBinary(BinaryReaderEx reader, JdbcProtocolContext protoCtx) + throws BinaryObjectException { + super.readBinary(reader, protoCtx); + + txId = reader.readInt(); + op = reader.readByte(); + name = reader.readString(); + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(JdbcTxSavepointRequest.class, this); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointResult.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointResult.java new file mode 100644 index 0000000000000..26fead78ad64a --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcTxSavepointResult.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.odbc.jdbc; + +import org.apache.ignite.binary.BinaryObjectException; +import org.apache.ignite.internal.binary.BinaryReaderEx; +import org.apache.ignite.internal.binary.BinaryWriterEx; +import org.apache.ignite.internal.util.typedef.internal.S; + +/** JDBC transaction savepoint result. */ +public class JdbcTxSavepointResult extends JdbcResult { + /** ID of initial request. */ + private long reqId; + + /** Transaction id. */ + private int txId; + + /** Default constructor for deserialization purpose. */ + public JdbcTxSavepointResult() { + super(TX_SAVEPOINT); + } + + /** + * @param reqId ID of initial request. + * @param txId Transaction id. + */ + public JdbcTxSavepointResult(long reqId, int txId) { + this(); + + this.reqId = reqId; + this.txId = txId; + } + + /** {@inheritDoc} */ + @Override public void writeBinary(BinaryWriterEx writer, JdbcProtocolContext protoCtx) + throws BinaryObjectException { + super.writeBinary(writer, protoCtx); + + writer.writeLong(reqId); + writer.writeInt(txId); + } + + /** {@inheritDoc} */ + @Override public void readBinary(BinaryReaderEx reader, JdbcProtocolContext protoCtx) + throws BinaryObjectException { + super.readBinary(reader, protoCtx); + + reqId = reader.readLong(); + txId = reader.readInt(); + } + + /** @return Request id. */ + public long reqId() { + return reqId; + } + + /** @return Transaction id. */ + public int txId() { + return txId; + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(JdbcTxSavepointResult.class, this); + } +}