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);
+    }
+}

Reply via email to