This is an automated email from the ASF dual-hosted git repository.
vldpyatkov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new fa93db7c748 IGNITE-28649 Support JDBC API for savepoint (#13112)
fa93db7c748 is described below
commit fa93db7c748fd866142003f4a5846a0eb66e738b
Author: Vladislav Pyatkov <[email protected]>
AuthorDate: Tue May 12 23:14:21 2026 +0300
IGNITE-28649 Support JDBC API for savepoint (#13112)
---
docs/_docs/SQL/JDBC/jdbc-driver.adoc | 41 ++++
docs/_docs/SQL/sql-calcite.adoc | 1 +
modules/calcite/pom.xml | 6 -
.../jdbc/JdbcThinConnectionSavepointTest.java | 270 +++++++++++++++++++++
.../apache/ignite/testsuites/JdbcTestSuite.java | 2 +
.../jdbc/thin/JdbcThinConnectionSelfTest.java | 153 +++++++-----
.../internal/jdbc/thin/JdbcThinConnection.java | 125 +++++++++-
.../jdbc/thin/JdbcThinDatabaseMetadata.java | 2 +-
.../ignite/internal/jdbc/thin/JdbcThinTcpIo.java | 9 +
.../internal/processors/odbc/jdbc/JdbcRequest.java | 8 +
.../processors/odbc/jdbc/JdbcRequestHandler.java | 84 +++++++
.../internal/processors/odbc/jdbc/JdbcResult.java | 8 +
.../processors/odbc/jdbc/JdbcThinFeature.java | 5 +-
.../odbc/jdbc/JdbcTxSavepointRequest.java | 102 ++++++++
.../odbc/jdbc/JdbcTxSavepointResult.java | 81 +++++++
15 files changed, 830 insertions(+), 67 deletions(-)
diff --git a/docs/_docs/SQL/JDBC/jdbc-driver.adoc
b/docs/_docs/SQL/JDBC/jdbc-driver.adoc
index 207ab57b1a2..fb966934609 100644
--- a/docs/_docs/SQL/JDBC/jdbc-driver.adoc
+++ b/docs/_docs/SQL/JDBC/jdbc-driver.adoc
@@ -555,6 +555,47 @@ 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.
+If auto-commit is left enabled, JDBC savepoint API calls requiring an explicit
transaction will fail with 'SQLException'.
+
+[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;
+ }
+}
+----
+
+SQL savepoint commands can be used, 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 f10b1a6724c..da65bfa1d68 100644
--- a/docs/_docs/SQL/sql-calcite.adoc
+++ b/docs/_docs/SQL/sql-calcite.adoc
@@ -192,6 +192,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 9b25987a685..538997e7241 100644
--- a/modules/calcite/pom.xml
+++ b/modules/calcite/pom.xml
@@ -236,12 +236,6 @@
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>ignite-clients</artifactId>
- <scope>test</scope>
- </dependency>
-
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinConnectionSavepointTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinConnectionSavepointTest.java
new file mode 100644
index 00000000000..25073ab812a
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcThinConnectionSavepointTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.query.calcite.jdbc;
+
+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;
+
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+
+/** Savepoint tests for thin JDBC connection. */
+public class JdbcThinConnectionSavepointTest extends AbstractJdbcTest {
+ /** */
+ private static final String TBL = "SAVEPOINT_TEST_TABLE";
+
+ /** JDBC URL. */
+ private static final String SAVEPOINT_URL = URL + "?queryEngine=" +
CalciteQueryEngineConfiguration.ENGINE_NAME +
+ "&transactionConcurrency=PESSIMISTIC";
+
+ /** {@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(2);
+ }
+
+ /** {@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);
+
+ assertThrows(log, () -> {
+ conn.rollback(sp);
+
+ return null;
+ }, SQLException.class, "Savepoint has been released.");
+
+ 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(SAVEPOINT_URL);
+ }
+
+ /**
+ * @param conn Connection.
+ * @param sql SQL.
+ */
+ private void execute(Connection conn, String sql) throws SQLException {
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute(sql);
+ }
+ }
+
+ /**
+ * @param conn Connection.
+ * @param exp Expected values as column pairs.
+ */
+ private void assertQuery(Connection conn, Object... exp) throws
SQLException {
+ List<List<Object>> rows = executeQuery(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/calcite/src/test/java/org/apache/ignite/testsuites/JdbcTestSuite.java
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/JdbcTestSuite.java
index 0ca27232c9d..b69f3cf8347 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/JdbcTestSuite.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/JdbcTestSuite.java
@@ -23,6 +23,7 @@ import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcLocalFlagTes
import org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcQueryTest;
import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcSetClientInfoCacheInterceptorTest;
import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcSetClientInfoTest;
+import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcThinConnectionSavepointTest;
import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcThinTransactionalSelfTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -34,6 +35,7 @@ import org.junit.runners.Suite;
@Suite.SuiteClasses({
JdbcQueryTest.class,
JdbcCrossEngineTest.class,
+ JdbcThinConnectionSavepointTest.class,
JdbcThinTransactionalSelfTest.class,
JdbcSetClientInfoTest.class,
JdbcSetClientInfoCacheInterceptorTest.class,
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 a697ffa4e25..086afcafeab 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
@@ -734,7 +734,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
assert !conn.isValid(2) : "Connection must be closed";
- assertThrows(log, new Callable<Object>() {
+ assertThrows(log, new Callable<>() {
@Override public Object call() throws Exception {
conn.isValid(-2);
@@ -797,7 +797,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
}
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createStatement(type, concur);
}
@@ -856,7 +856,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
}
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws
Exception {
return conn.createStatement(type, concur,
holdabililty);
}
@@ -888,7 +888,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// null query text
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(null);
}
@@ -938,7 +938,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// null query text
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws
Exception {
return conn.prepareStatement(null, type,
concur);
}
@@ -951,7 +951,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
}
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(sqlText, type,
concur);
}
@@ -1003,7 +1003,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// null query text
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws
Exception {
return conn.prepareStatement(null,
type, concur, holdabililty);
}
@@ -1016,7 +1016,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
}
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws
Exception {
return conn.prepareStatement(sqlText,
type, concur, holdabililty);
}
@@ -1050,7 +1050,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
final String sqlText = "insert into test (val) values (?)";
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(sqlText,
RETURN_GENERATED_KEYS);
}
@@ -1060,7 +1060,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(sqlText,
NO_GENERATED_KEYS);
}
@@ -1070,7 +1070,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(sqlText, new int[] {1});
}
@@ -1080,7 +1080,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareStatement(sqlText, new String[]
{"ID"});
}
@@ -1100,7 +1100,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
final String sqlText = "exec test()";
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareCall(sqlText);
}
@@ -1110,7 +1110,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareCall(sqlText, TYPE_FORWARD_ONLY,
CONCUR_READ_ONLY);
}
@@ -1120,7 +1120,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.prepareCall(sqlText, TYPE_FORWARD_ONLY,
CONCUR_READ_ONLY, HOLD_CURSORS_OVER_COMMIT);
@@ -1140,7 +1140,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// null query text
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.nativeSQL(null);
}
@@ -1197,7 +1197,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Should not be called in auto-commit mode
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.commit();
@@ -1212,7 +1212,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Should not be called in auto-commit mode
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.commit();
@@ -1242,7 +1242,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Should not be called in auto-commit mode
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.rollback();
@@ -1349,7 +1349,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Invalid parameter value
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@SuppressWarnings("MagicConstant")
@Override public Object call() throws Exception {
conn.setTransactionIsolation(-1);
@@ -1433,7 +1433,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
public void testGetSetTypeMap() throws Exception {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.getTypeMap();
}
@@ -1443,7 +1443,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setTypeMap(new HashMap<String, Class<?>>());
@@ -1458,7 +1458,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Exception when called on closed connection
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.getTypeMap();
}
@@ -1469,7 +1469,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Exception when called on closed connection
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setTypeMap(new HashMap<String, Class<?>>());
@@ -1499,7 +1499,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Invalid constant
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setHoldability(-1);
@@ -1513,7 +1513,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
conn.close();
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.getHoldability();
}
@@ -1523,7 +1523,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
);
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setHoldability(HOLD_CURSORS_OVER_COMMIT);
@@ -1542,11 +1542,11 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
@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,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setSavepoint();
@@ -1573,11 +1573,11 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
@Test
public void testSetSavepointName() throws Exception {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
- assert !conn.getMetaData().supportsSavepoints();
+ assert conn.getMetaData().supportsSavepoints();
// Invalid arg
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setSavepoint(null);
@@ -1592,7 +1592,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Disallowed in auto-commit mode
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setSavepoint(name);
@@ -1613,17 +1613,54 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
}
}
+ /**
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testSavepointsDisabledFeature() throws Exception {
+ try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp +
+ "&disabledFeatures=savepoints")) {
+ assertFalse(conn.getMetaData().supportsSavepoints());
+
+ conn.setAutoCommit(false);
+
+ assertThrows(log,
+ new Callable<>() {
+ @Override public Object call() throws Exception {
+ conn.setSavepoint();
+
+ return null;
+ }
+ },
+ SQLFeatureNotSupportedException.class,
+ "Savepoints are not supported."
+ );
+
+ assertThrows(log,
+ new Callable<>() {
+ @Override public Object call() throws Exception {
+ conn.setSavepoint("savepoint");
+
+ return null;
+ }
+ },
+ SQLFeatureNotSupportedException.class,
+ "Savepoints are not supported."
+ );
+ }
+ }
+
/**
* @throws Exception If failed.
*/
@Test
public void testRollbackSavePoint() throws Exception {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
- assert !conn.getMetaData().supportsSavepoints();
+ assert conn.getMetaData().supportsSavepoints();
// Invalid arg
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.rollback(null);
@@ -1638,7 +1675,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Disallowed in auto-commit mode
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.rollback(savepoint);
@@ -1678,11 +1715,11 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
@Test
public void testReleaseSavepoint() throws Exception {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
- assert !conn.getMetaData().supportsSavepoints();
+ assert conn.getMetaData().supportsSavepoints();
// Invalid arg
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.releaseSavepoint(null);
@@ -1695,11 +1732,17 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
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();
@@ -1723,7 +1766,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
conn.close();
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createClob();
}
@@ -1746,7 +1789,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
conn.close();
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createBlob();
}
@@ -1765,7 +1808,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Unsupported
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createNClob();
}
@@ -1777,7 +1820,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
conn.close();
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createNClob();
}
@@ -1796,7 +1839,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Unsupported
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createSQLXML();
}
@@ -1808,7 +1851,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
conn.close();
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createSQLXML();
}
@@ -1845,7 +1888,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
});
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setClientInfo(name, val);
@@ -1884,7 +1927,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
});
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setClientInfo(props);
@@ -1906,7 +1949,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
// Invalid typename
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.createArrayOf(null, null);
@@ -1943,7 +1986,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
// Invalid typename
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
return conn.createStruct(null, null);
}
@@ -2014,7 +2057,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
try (Connection conn =
DriverManager.getConnection(urlWithPartitionAwarenessProp)) {
//Invalid executor
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.abort(null);
@@ -2048,7 +2091,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
//Invalid timeout
assertThrows(log,
- new Callable<Object>() {
+ new Callable<>() {
@Override public Object call() throws Exception {
conn.setNetworkTimeout(executor, -1);
@@ -2083,7 +2126,7 @@ public class JdbcThinConnectionSelfTest extends
JdbcThinAbstractSelfTest {
*/
@Test
public void testSslClientAndPlainServer() {
- Throwable e = assertThrows(log, new Callable<Object>() {
+ Throwable e = assertThrows(log, new Callable<>() {
@Override public Object call() throws Exception {
DriverManager.getConnection(urlWithPartitionAwarenessProp +
"&sslMode=require" +
"&sslClientCertificateKeyStoreUrl=" + CLI_KEY_STORE_PATH +
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 e3771824b5e..619057cd6ce 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.JdbcResultWithIo;
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;
@@ -373,6 +378,11 @@ public class JdbcThinConnection implements Connection {
return isTxAwareQueriesSupported;
}
+ /** @return {@code True} if savepoints supported by the server, {@code
false} otherwise. */
+ boolean savepointsSupportedByServer() {
+ return defaultIo().isSavepointsSupported();
+ }
+
/** @return {@code True} if certain isolation level supported by the
server, {@code false} otherwise. */
boolean isolationLevelSupported(int level) throws SQLException {
if (level == TRANSACTION_NONE)
@@ -810,7 +820,11 @@ public class JdbcThinConnection implements Connection {
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 +837,9 @@ public class JdbcThinConnection implements Connection {
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 +852,7 @@ public class JdbcThinConnection implements Connection {
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 +862,9 @@ public class JdbcThinConnection implements Connection {
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 +1084,52 @@ public class JdbcThinConnection implements Connection {
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 (!savepointsSupportedByServer())
+ throw new SQLFeatureNotSupportedException("Savepoints are not
supported.");
+
+ 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 +2767,59 @@ public class JdbcThinConnection implements Connection {
}
}
+ /** 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 3758e9625a3..f9f681893b1 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 conn.savepointsSupportedByServer();
}
/** {@inheritDoc} */
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinTcpIo.java
b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinTcpIo.java
index 9c3327600cb..970f9102e8e 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinTcpIo.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinTcpIo.java
@@ -722,6 +722,15 @@ public class JdbcThinTcpIo {
return protoCtx.isFeatureSupported(JdbcThinFeature.TX_AWARE_QUERIES);
}
+ /**
+ * Whether transaction savepoint operations are supported by the server or
not.
+ *
+ * @return {@code true} if transaction savepoint operations supported,
{@code false} otherwise.
+ */
+ boolean isSavepointsSupported() {
+ return protoCtx.isFeatureSupported(JdbcThinFeature.SAVEPOINTS);
+ }
+
/**
* @param isolation Transaction isolation level.
* @return {@code True} if transaction isolation mode supported by the
server, {@code false} otherwise.
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 81f8e56ad76..9d25ac4e27e 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 class JdbcRequest extends ClientListenerRequestNoId
implements JdbcRawBin
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 451dfdcc79a..b999242274b 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_EX
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 @@ public class JdbcRequestHandler implements
ClientListenerRequestHandler, ClientT
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 @@ public class JdbcRequestHandler implements
ClientListenerRequestHandler, ClientT
}
}
+ /**
+ * 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 390b4890c9d..401663738bf 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 class JdbcResult implements JdbcRawBinarylizable {
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/JdbcThinFeature.java
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcThinFeature.java
index f531b72bdee..f635e92cb1d 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcThinFeature.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcThinFeature.java
@@ -43,7 +43,10 @@ public enum JdbcThinFeature implements ThinProtocolFeature {
CLIENT_INFO(5),
/** Execute local queries. */
- LOCAL_QUERIES(6);
+ LOCAL_QUERIES(6),
+
+ /** Transaction savepoint operations. */
+ SAVEPOINTS(7);
/** */
private static final EnumSet<JdbcThinFeature> ALL_FEATURES_AS_ENUM_SET =
EnumSet.allOf(JdbcThinFeature.class);
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 00000000000..744a9ded778
--- /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 00000000000..26fead78ad6
--- /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);
+ }
+}