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