This is an automated email from the ASF dual-hosted git repository.

ppa pushed a commit to branch jdbc_over_thin_sql
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/jdbc_over_thin_sql by this 
push:
     new 31be73b1720 IGNITE-26142 Jdbc. Support multi-statement execution in 
(Prepared)Statement.execute() using thin client SQL API (#6806)
31be73b1720 is described below

commit 31be73b1720fa618794f45aaef128a93fd27716b
Author: Max Zhuravkov <[email protected]>
AuthorDate: Thu Oct 23 09:31:36 2025 +0300

    IGNITE-26142 Jdbc. Support multi-statement execution in 
(Prepared)Statement.execute() using thin client SQL API (#6806)
---
 .../cli/commands/sql/ItSqlCommandTest.java         |  22 ++--
 .../cli/commands/sql/ItSqlMultistatementTest.java  |   6 -
 .../cli/commands/sql/ItSqlReplCommandTest.java     |  10 +-
 .../sql/ClientSqlCursorNextResultRequest.java      |  24 +++-
 .../internal/jdbc/ItJdbcMetadataSelfTest.java      |  29 ++---
 .../jdbc2/ItJdbcClusterPerIntegrationTest.java     | 124 +++++++++++++++++++++
 .../apache/ignite/jdbc/AbstractJdbcSelfTest.java   |   9 +-
 .../apache/ignite/jdbc/ItJdbcBatchSelfTest.java    |  12 +-
 .../ignite/jdbc/ItJdbcMultiStatementSelfTest.java  |  40 +++++--
 .../apache/ignite/jdbc/ItJdbcQueryMetricsTest.java |   4 -
 .../ignite/jdbc/ItJdbcStatementCancelSelfTest.java |   1 -
 .../ignite/jdbc/ItJdbcStatementSelfTest.java       |  10 +-
 .../ignite/internal/jdbc2/ClientSyncResultSet.java |  46 ++++++++
 .../internal/jdbc2/ClientSyncResultSetImpl.java    |  89 +++++++++++++++
 .../ignite/internal/jdbc2/JdbcConnection2.java     |  11 +-
 .../internal/jdbc2/JdbcExceptionMapperUtil.java    |  56 ++++++++++
 .../internal/jdbc2/JdbcPreparedStatement2.java     |   9 +-
 .../ignite/internal/jdbc2/JdbcResultSet.java       |  64 ++++++-----
 .../ignite/internal/jdbc2/JdbcStatement2.java      |  98 ++++++++--------
 .../ignite/internal/jdbc2/ResultSetWrapper.java    |  99 ++++++++++++++++
 .../internal/jdbc2/JdbcResultSet2SelfTest.java     |  66 ++++++-----
 .../internal/jdbc2/JdbcStatement2SelfTest.java     |   4 +-
 .../client/ItThinClientMultistatementSqlTest.java  |  19 +++-
 23 files changed, 660 insertions(+), 192 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
index 7226e48e2a5..1c691db8c40 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.cli.commands.sql;
 import static 
org.apache.ignite.internal.cli.core.exception.handler.SqlExceptionHandler.CLIENT_CONNECTION_FAILED_MESSAGE;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
@@ -155,7 +154,6 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
     @Test
     @DisplayName("Should execute multiline sql script from file")
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void multilineScript() {
         String filePath = getClass().getResource("/multiline.sql").getPath();
         execute("sql", "--file", filePath, "--jdbc-url", JDBC_URL);
@@ -177,13 +175,13 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void exceptionHandler() {
         execute("sql", "SELECT 1/0;", "--jdbc-url", JDBC_URL);
 
         assertAll(
                 this::assertOutputIsEmpty,
-                () -> assertErrOutputContains("SQL query execution error"),
+                // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                // () -> assertErrOutputContains("SQL query execution error"),
                 () -> assertErrOutputContains("Division by zero"),
                 () -> assertErrOutputDoesNotContain("Unknown error")
         );
@@ -192,7 +190,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
         assertAll(
                 this::assertOutputIsEmpty,
-                () -> assertErrOutputContains("SQL query execution error"),
+                // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                // () -> assertErrOutputContains("SQL query execution error"),
                 () -> assertErrOutputContains("Object 'NOTEXISTEDTABLE' not 
found"),
                 () -> assertErrOutputDoesNotContain("Unknown error")
         );
@@ -200,7 +199,6 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
     @Test
     @DisplayName("An error should be displayed indicating that the script 
transaction was not completed by the script.")
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void scriptTxNotFinishedByScript() {
         String expectedError = "Transaction block doesn't have a COMMIT 
statement at the end.";
 
@@ -209,7 +207,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
             assertAll(
                     this::assertOutputIsEmpty,
-                    () -> assertErrOutputContains("SQL query execution error"),
+                    // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                    // () -> assertErrOutputContains("SQL query execution 
error"),
                     () -> assertErrOutputContains(expectedError),
                     () -> assertErrOutputDoesNotContain("Unknown error")
             );
@@ -220,7 +219,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
             assertAll(
                     this::assertOutputIsEmpty,
-                    () -> assertErrOutputContains("SQL query execution error"),
+                    // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                    // () -> assertErrOutputContains("SQL query execution 
error"),
                     () -> assertErrOutputContains(expectedError),
                     () -> assertErrOutputDoesNotContain("Unknown error")
             );
@@ -231,7 +231,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
             assertAll(
                     this::assertOutputIsEmpty,
-                    () -> assertErrOutputContains("SQL query execution error"),
+                    // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                    // () -> assertErrOutputContains("SQL query execution 
error"),
                     () -> assertErrOutputContains(expectedError),
                     () -> assertErrOutputDoesNotContain("Unknown error")
             );
@@ -242,7 +243,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
 
             assertAll(
                     this::assertOutputIsEmpty,
-                    () -> assertErrOutputContains("SQL query execution error"),
+                    // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                    // () -> assertErrOutputContains("SQL query execution 
error"),
                     () -> assertErrOutputContains(expectedError),
                     () -> assertErrOutputDoesNotContain("Unknown error")
             );
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlMultistatementTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlMultistatementTest.java
index 057c23bf3cd..a194cc26c2c 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlMultistatementTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlMultistatementTest.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.cli.commands.sql;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -37,7 +36,6 @@ public class ItSqlMultistatementTest extends 
CliSqlCommandTestBase {
      * Test for multiple insert queries. It ensures that the output contains 
the number of updated rows for each query.
      */
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void sequentialUpdates() {
         String testQuery = "insert into mytable(id) values (1);insert into 
mytable(id) values (2), (3)";
         String expectedOutput = "Updated 1 rows.\n"
@@ -53,7 +51,6 @@ public class ItSqlMultistatementTest extends 
CliSqlCommandTestBase {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void sequentialSelects() {
         String testQuery = "select * from mytable order by id; insert into 
mytable(id) values(3); select * from mytable order by id;";
         String expectedOutput =
@@ -87,7 +84,6 @@ public class ItSqlMultistatementTest extends 
CliSqlCommandTestBase {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void deleteAfterInserts() {
         String testQuery = "insert into mytable(id) values (1);insert into 
mytable(id) values (2), (3); delete from mytable;";
         String expectedOutput = "Updated 1 rows.\n"
@@ -104,7 +100,6 @@ public class ItSqlMultistatementTest extends 
CliSqlCommandTestBase {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void updateAfterInserts() {
         String testQuery = "insert into mytable(id) values (1);insert into 
mytable(id) values (2), (3); update mytable set Name = 'Name1';";
         String expectedOutput = "Updated 1 rows.\n"
@@ -121,7 +116,6 @@ public class ItSqlMultistatementTest extends 
CliSqlCommandTestBase {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void sequentialCreateTable() {
         String testQuery = "create table mytable1(id int primary key); create 
table mytable2(id int primary key)";
         String expectedOutput = "Updated 0 rows.\n"
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlReplCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlReplCommandTest.java
index 4f33ca79a18..4791dbea68a 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlReplCommandTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlReplCommandTest.java
@@ -23,7 +23,6 @@ import io.micronaut.context.annotation.Bean;
 import io.micronaut.context.annotation.Replaces;
 import org.apache.ignite.internal.cli.CliIntegrationTest;
 import org.apache.ignite.internal.cli.core.repl.executor.ReplExecutorProvider;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
@@ -64,11 +63,11 @@ class ItSqlReplCommandTest extends CliIntegrationTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void multilineCommand() {
         execute("CREATE TABLE MULTILINE_TABLE(K INT PRIMARY KEY); \n INSERT 
INTO MULTILINE_TABLE VALUES(1);", "--jdbc-url", JDBC_URL);
 
         assertAll(
+                // TODO https://issues.apache.org/jira/browse/IGNITE-26790
                 // The output from CREATE TABLE is: Updated 0 rows.
                 () -> assertOutputContains("Updated 0 rows."),
                 this::assertErrOutputIsEmpty
@@ -100,13 +99,13 @@ class ItSqlReplCommandTest extends CliIntegrationTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void exceptionHandler() {
         execute("SELECT 1/0;", "--jdbc-url", JDBC_URL);
 
         assertAll(
                 this::assertOutputIsEmpty,
-                () -> assertErrOutputContains("SQL query execution error"),
+                // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                // () -> assertErrOutputContains("SQL query execution error"),
                 () -> assertErrOutputContains("Division by zero"),
                 () -> assertErrOutputDoesNotContain("Unknown error")
         );
@@ -115,7 +114,8 @@ class ItSqlReplCommandTest extends CliIntegrationTest {
 
         assertAll(
                 this::assertOutputIsEmpty,
-                () -> assertErrOutputContains("SQL query execution error"),
+                // TODO https://issues.apache.org/jira/browse/IGNITE-26790
+                // () -> assertErrOutputContains("SQL query execution error"),
                 () -> assertErrOutputContains("Object 'NOTEXISTEDTABLE' not 
found"),
                 () -> assertErrOutputDoesNotContain("Unknown error")
         );
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java
index 5e8752fae65..613ea270992 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCursorNextResultRequest.java
@@ -27,6 +27,7 @@ import 
org.apache.ignite.client.handler.requests.sql.ClientSqlCommon.CursorWithP
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.sql.api.AsyncResultSetImpl;
+import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
 import org.apache.ignite.sql.SqlRow;
 
 /**
@@ -50,7 +51,7 @@ public class ClientSqlCursorNextResultRequest {
         CursorWithPageSize cursorWithPageSize = 
resource.get(CursorWithPageSize.class);
         int pageSize = cursorWithPageSize.pageSize();
 
-        return cursorWithPageSize.cursorFuture()
+        CompletableFuture<ResponseWriter> f = cursorWithPageSize.cursorFuture()
                 .thenComposeAsync(cur -> cur.requestNextAsync(pageSize)
                         .thenApply(batchRes -> new AsyncResultSetImpl<SqlRow>(
                                         cur,
@@ -60,5 +61,26 @@ public class ClientSqlCursorNextResultRequest {
                         ).thenCompose(asyncResultSet ->
                                 ClientSqlCommon.writeResultSetAsync(resources, 
asyncResultSet, metrics, pageSize, false, false, true)
                         ).thenApply(rsWriter -> rsWriter), operationExecutor);
+
+        f.whenCompleteAsync((r, t) -> {
+            if (t != null) {
+                cursorWithPageSize.cursorFuture().thenAccept(cur -> 
closeRemainingCursors(cur, false, operationExecutor));
+            }
+        }, operationExecutor);
+
+        return f;
+    }
+
+    private static void closeRemainingCursors(AsyncSqlCursor<?> cursor, 
boolean closeCursor, Executor operationExecutor) {
+        if (cursor.hasNextResult()) {
+            cursor.nextResult().whenCompleteAsync((c, err) -> {
+                if (c != null) {
+                    cursor.closeAsync();
+                    closeRemainingCursors(c, true, operationExecutor);
+                }
+            }, operationExecutor);
+        } else if (closeCursor) {
+            cursor.closeAsync();
+        }
     }
 }
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc/ItJdbcMetadataSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc/ItJdbcMetadataSelfTest.java
index 14ccdce26fc..2c95ec8e1c7 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc/ItJdbcMetadataSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc/ItJdbcMetadataSelfTest.java
@@ -52,21 +52,21 @@ public class ItJdbcMetadataSelfTest extends 
AbstractJdbcSelfTest {
     /** Creates tables. */
     @BeforeAll
     public static void createTables() throws SQLException {
-        // TODO https://issues.apache.org/jira/browse/IGNITE-26142 convert 
these statements back into a script 
         try (Statement stmt = conn.createStatement()) {
-            stmt.execute("CREATE SCHEMA IF NOT EXISTS PUBLIC;");
-            stmt.execute("CREATE SCHEMA IF NOT EXISTS META;");
-            stmt.execute("CREATE SCHEMA IF NOT EXISTS USER2;");
-            stmt.execute("CREATE SCHEMA IF NOT EXISTS \"user0\";");
-            stmt.execute("CREATE SCHEMA IF NOT EXISTS USER1;");
-            stmt.execute("CREATE TABLE person(name VARCHAR(32), age INT, orgid 
INT PRIMARY KEY);");
-            stmt.execute("CREATE TABLE organization(id INT PRIMARY KEY, name 
VARCHAR, bigdata DECIMAL(20, 10));");
-            stmt.execute("CREATE TABLE user1.table1(id INT PRIMARY KEY);");
-            stmt.execute("CREATE TABLE user2.\"table2\"(id INT PRIMARY KEY);");
-            stmt.execute("CREATE TABLE \"user0\".\"table0\"(\"id\" INT PRIMARY 
KEY);");
-            stmt.execute("CREATE TABLE \"user0\".table0(id INT PRIMARY KEY);");
-            stmt.execute("INSERT INTO person (orgid, name, age) VALUES (1, 
'111', 111);");
-            stmt.execute("INSERT INTO organization (id, name, bigdata) VALUES 
(1, 'AAA', 10);");
+            stmt.execute("CREATE SCHEMA IF NOT EXISTS PUBLIC;"
+                    + "CREATE SCHEMA IF NOT EXISTS META;"
+                    + "CREATE SCHEMA IF NOT EXISTS USER2;"
+                    + "CREATE SCHEMA IF NOT EXISTS \"user0\";"
+                    + "CREATE SCHEMA IF NOT EXISTS USER1;"
+                    + "CREATE TABLE person(name VARCHAR(32), age INT, orgid 
INT PRIMARY KEY);"
+                    + "CREATE TABLE organization(id INT PRIMARY KEY, name 
VARCHAR, bigdata DECIMAL(20, 10));"
+                    + "CREATE TABLE user1.table1(id INT PRIMARY KEY);"
+                    + "CREATE TABLE user2.\"table2\"(id INT PRIMARY KEY);"
+                    + "CREATE TABLE \"user0\".\"table0\"(\"id\" INT PRIMARY 
KEY);"
+                    + "CREATE TABLE \"user0\".table0(id INT PRIMARY KEY);"
+                    + "INSERT INTO person (orgid, name, age) VALUES (1, '111', 
111);"
+                    + "INSERT INTO organization (id, name, bigdata) VALUES (1, 
'AAA', 10);"
+            );
         }
     }
 
@@ -161,6 +161,7 @@ public class ItJdbcMetadataSelfTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26145";)
     public void testResultSetMetaDataColumns() throws Exception {
         createMetaTable();
 
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc2/ItJdbcClusterPerIntegrationTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc2/ItJdbcClusterPerIntegrationTest.java
new file mode 100644
index 00000000000..06d37f12872
--- /dev/null
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/internal/jdbc2/ItJdbcClusterPerIntegrationTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.jdbc2;
+
+import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.List;
+import org.apache.ignite.internal.ClusterPerTestIntegrationTest;
+import org.apache.ignite.jdbc.ItJdbcStatementSelfTest;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Jdbc tests that require a new cluster for each test.
+ */
+public class ItJdbcClusterPerIntegrationTest extends 
ClusterPerTestIntegrationTest {
+
+    private static final List<String> STATEMENTS = List.of(
+            "SELECT 1; SELECT 2/0; SELECT 3",
+            "SELECT * FROM SYSTEM_RANGE(1, 1500); SELECT 2/0; SELECT 3",
+            "SELECT 1; SELECT 2/0; SELECT * FROM SYSTEM_RANGE(3, 1500)"
+    );
+
+    private static final String JDBC_URL = 
"jdbc:ignite:thin://127.0.0.1:10800";
+
+    @Override
+    protected int initialNodes() {
+        return 1;
+    }
+
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26789";)
+    public void noScriptResourcesAfterExecutingFailingScript() throws 
Exception {
+        for (String statement : STATEMENTS) {
+            log.info("Run statement: {}", statement);
+
+            try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
+                Statement stmt = conn.createStatement();
+                stmt.execute(statement);
+                // Connection should close the statement
+            }
+            // Fails with IGN-CMN-65535 Failed to find resource with id: XYZ
+
+            expectNoResources();
+        }
+    }
+
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26789";)
+    public void noScriptResourcesAfterExecutingFailingScript2() throws 
Exception {
+        for (String statement : STATEMENTS) {
+            log.info("Run statement: {}", statement);
+
+            try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
+                try (Statement stmt = conn.createStatement()) {
+                    stmt.execute(statement);
+                }
+                // Fails with IGN-CMN-65535 Failed to find resource with id: 
XYZ
+
+                // All resources should be released
+                expectNoResources();
+            }
+        }
+    }
+
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26789";)
+    public void noResourcesScriptAfterClientTerminates() throws Exception {
+        for (String statement : STATEMENTS) {
+            log.info("Run statement: {}", statement);
+
+            Connection conn = DriverManager.getConnection(JDBC_URL);
+            JdbcConnection2 jdbcConnection = 
conn.unwrap(JdbcConnection2.class);
+
+            Statement stmt = conn.createStatement();
+            // Do not close the statement, closing the client should release 
its resources.
+            stmt.execute(statement);
+
+            // Close the client
+            jdbcConnection.closeClient();
+
+            expectNoResources();
+        }
+    }
+
+    @Test
+    public void noStatementResourcesAfterClientTerminates() throws Exception {
+        Connection conn = DriverManager.getConnection(JDBC_URL);
+        JdbcConnection2 jdbcConnection = conn.unwrap(JdbcConnection2.class);
+
+        Statement stmt = conn.createStatement();
+        // Do not close the statement, closing the client should release its 
resources.
+        stmt.executeQuery("SELECT * FROM SYSTEM_RANGE(1, 15000)");
+
+        // Close the client
+        jdbcConnection.closeClient();
+
+        expectNoResources();
+    }
+
+    private void expectNoResources() throws InterruptedException {
+        assertTrue(waitForCondition(() -> 
ItJdbcStatementSelfTest.openResources(cluster) == 0, 5_000));
+        assertTrue(waitForCondition(() -> 
ItJdbcStatementSelfTest.openCursors(cluster) == 0, 5_000));
+    }
+}
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java
index 87237f7fb18..5470ea102f6 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/AbstractJdbcSelfTest.java
@@ -25,6 +25,7 @@ import java.sql.DriverManager;
 import java.sql.SQLFeatureNotSupportedException;
 import java.sql.Statement;
 import java.util.Map;
+import org.apache.ignite.internal.Cluster;
 import org.apache.ignite.internal.ClusterPerClassIntegrationTest;
 import org.apache.ignite.internal.app.IgniteImpl;
 import org.apache.ignite.internal.manager.IgniteComponent;
@@ -138,8 +139,8 @@ public class AbstractJdbcSelfTest extends 
ClusterPerClassIntegrationTest {
     }
 
     /** Return a size of stored resources. Reflection based implementation, 
need to be refactored. */
-    int openResources() {
-        IgniteImpl ignite = unwrapIgniteImpl(CLUSTER.node(0));
+    public static int openResources(Cluster cluster) {
+        IgniteImpl ignite = unwrapIgniteImpl(cluster.node(0));
         IgniteComponent cliHnd = IgniteTestUtils.getFieldValue(ignite, 
"clientHandlerModule");
         Object clientInboundHandler = IgniteTestUtils.getFieldValue(cliHnd, 
"handler");
         Object rsrc = IgniteTestUtils.getFieldValue(clientInboundHandler, 
"resources");
@@ -148,8 +149,8 @@ public class AbstractJdbcSelfTest extends 
ClusterPerClassIntegrationTest {
     }
 
     /** Returns a size of opened cursors. */
-    int openCursors() {
-        IgniteImpl ignite = unwrapIgniteImpl(CLUSTER.node(0));
+    public static int openCursors(Cluster cluster) {
+        IgniteImpl ignite = unwrapIgniteImpl(cluster.node(0));
         SqlQueryProcessor queryProcessor = (SqlQueryProcessor) 
ignite.queryEngine();
         return queryProcessor.openedCursors();
     }
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
index 43d1e520fa5..a0f517a4197 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcBatchSelfTest.java
@@ -43,7 +43,6 @@ import java.time.LocalTime;
 import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import org.apache.ignite.internal.TestWrappers;
@@ -117,7 +116,7 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
             statement.executeUpdate(SQL_DELETE);
         }
 
-        resourcesBefore = openResources();
+        resourcesBefore = openResources(CLUSTER);
     }
 
     /** {@inheritDoc} */
@@ -138,7 +137,7 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
         assertEquals(0, countOfPendingTransactions);
 
         Awaitility.await().timeout(5, TimeUnit.SECONDS).untilAsserted(() -> {
-            assertThat(openResources() - resourcesBefore, is(0));
+            assertThat(openResources(CLUSTER) - resourcesBefore, is(0));
         });
     }
 
@@ -811,8 +810,9 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
         }
 
         // Each statement in a batch is executed separately, and timeout is 
applied to each statement.
-        {
-            int timeoutMillis = ThreadLocalRandom.current().nextInt(1, 5);
+        // Retry until timeout exception is thrown.
+        Awaitility.await().untilAsserted(() -> {
+            int timeoutMillis = 1;
             igniteStmt.timeout(timeoutMillis);
 
             for (int i = 0; i < 3; i++) {
@@ -823,7 +823,7 @@ public class ItJdbcBatchSelfTest extends 
AbstractJdbcSelfTest {
 
             assertThrowsSqlException(SQLException.class,
                     "Query timeout", igniteStmt::executeBatch);
-        }
+        });
 
         {
             // Disable timeout
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java
index 54326c2f5a5..86db5d79615 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcMultiStatementSelfTest.java
@@ -47,7 +47,6 @@ import org.junit.jupiter.params.provider.ValueSource;
 /**
  * Tests for queries containing multiple sql statements, separated by ";".
  */
-@Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
 public class ItJdbcMultiStatementSelfTest extends AbstractJdbcSelfTest {
     /**
      * Setup tables.
@@ -70,17 +69,17 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
 
     @AfterEach
     void tearDown() throws Exception {
-        int openCursorResources = openResources();
+        int openCursorResources = openResources(CLUSTER);
         // connection + not closed result set
-        assertTrue(openResources() <= 2, "Open cursors: " + 
openCursorResources);
+        assertTrue(openResources(CLUSTER) <= 2, "Open cursors: " + 
openCursorResources);
 
         stmt.close();
 
-        openCursorResources = openResources();
+        openCursorResources = openResources(CLUSTER);
 
         // only connection context or 0 if already closed.
-        assertTrue(openResources() <= 1, "Open cursors: " + 
openCursorResources);
-        assertTrue(waitForCondition(() -> openCursors() == 0, 5_000));
+        assertTrue(openResources(CLUSTER) <= 1, "Open cursors: " + 
openCursorResources);
+        assertTrue(waitForCondition(() -> openCursors(CLUSTER) == 0, 5_000));
     }
 
     @Test
@@ -94,7 +93,9 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
         // pk violation exception
         // TODO: https://issues.apache.org/jira/browse/IGNITE-21133
         stmt.execute("START TRANSACTION; INSERT INTO TEST_TX VALUES (1, 1, 
'1'); COMMIT");
-        assertThrowsSqlException("Failed to fetch query results", () -> 
stmt.execute("SELECT COUNT(*) FROM TEST_TX"));
+        assertEquals(0, stmt.getUpdateCount());
+        assertThrowsSqlException("PK unique constraint is violated", () -> 
stmt.getMoreResults());
+
         stmt.execute("SELECT COUNT(*) FROM TEST_TX");
         try (ResultSet rs = stmt.getResultSet()) {
             assertTrue(rs.next());
@@ -145,18 +146,27 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
     public void testSimpleQueryError() throws Exception {
         boolean res = stmt.execute("SELECT 1; SELECT 1/0; SELECT 2");
         assertTrue(res);
-        assertThrowsSqlException("Failed to fetch query results", () -> 
stmt.getMoreResults());
+        assertThrowsSqlException("Division by zero", () -> 
stmt.getMoreResults());
         // Next after exception.
-        assertFalse(stmt.getMoreResults());
+        // assertFalse(stmt.getMoreResults());
+        // Do not move past the first result.
+        assertThrowsSqlException("Division by zero", () -> 
stmt.getMoreResults());
 
         stmt.closeOnCompletion();
     }
 
+    @Test
+    public void testSimpleQueryErrorMustReleaseServerResources() throws 
Exception {
+        // The script fails, the user does not retrieve any result sets.
+        stmt.execute("SELECT 1; SELECT 2/0; SELECT 3");
+        // But the resources must be released in after test callbacks.
+    }
+
     @Test
     public void testSimpleQueryErrorCloseRs() throws Exception {
-        stmt.execute("SELECT 1; SELECT 1/0; SELECT 2");
+        stmt.execute("SELECT 1; SELECT 2/0; SELECT 2");
         ResultSet rs = stmt.getResultSet();
-        assertThrowsSqlException("Failed to fetch query results", () -> 
stmt.getMoreResults());
+        assertThrowsSqlException("Division by zero", () -> 
stmt.getMoreResults());
         stmt.closeOnCompletion();
 
         rs.close();
@@ -214,13 +224,18 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
     @Test
     public void noMoreResultsArePossibleAfterCloseOnCompletion() throws 
Exception {
         stmt.execute("SELECT 1; SELECT 2; SELECT 3");
+
+        ResultSet rs1 = stmt.getResultSet();
         // SELECT 2;
         assertTrue(stmt.getMoreResults());
+        assertTrue(rs1.isClosed());
 
+        ResultSet rs2 = stmt.getResultSet();
         stmt.closeOnCompletion();
 
         // SELECT 3;
         assertTrue(stmt.getMoreResults());
+        assertTrue(rs2.isClosed());
         assertFalse(stmt.isClosed());
 
         // no more results, auto close statement
@@ -417,7 +432,7 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
     @Test
     @SuppressWarnings("ThrowableNotThrown")
     public void testAutoCommitFalseTxControlStatementsNotSupported() throws 
Exception {
-        String txErrMsg = "Transaction control statements are not supported 
when autocommit mode is disabled";
+        String txErrMsg = "Transaction control statements are not supported 
when autocommit mode is disabled.";
         conn.setAutoCommit(false);
         assertThrowsSqlException(txErrMsg, () -> stmt.execute("START 
TRANSACTION; SELECT 1; COMMIT"));
         assertThrowsSqlException(txErrMsg, () -> stmt.execute("COMMIT"));
@@ -583,6 +598,7 @@ public class ItJdbcMultiStatementSelfTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26143";)
     public void testResultsFromExecuteBatch() throws Exception {
         stmt.addBatch("INSERT INTO TEST_TX VALUES (7, 25, 'Michel');");
         stmt.addBatch("INSERT INTO TEST_TX VALUES (8, 25, 'Michel');");
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcQueryMetricsTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcQueryMetricsTest.java
index eca7adc20ae..f9a89af17ee 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcQueryMetricsTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcQueryMetricsTest.java
@@ -40,7 +40,6 @@ import org.apache.ignite.internal.metrics.MetricSet;
 import org.apache.ignite.internal.sql.metrics.SqlQueryMetricSource;
 import org.awaitility.Awaitility;
 import org.hamcrest.Matchers;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -58,7 +57,6 @@ public class ItJdbcQueryMetricsTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testScriptErrors() throws SQLException {
         try (var stmt = conn.prepareStatement("SELECT 1; SELECT 1/?;")) {
             stmt.setInt(1, 0);
@@ -93,7 +91,6 @@ public class ItJdbcQueryMetricsTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testScriptCancellation() throws SQLException {
         try (var stmt = conn.prepareStatement("SELECT 1; SELECT 1/?;")) {
             stmt.setInt(1, 0);
@@ -126,7 +123,6 @@ public class ItJdbcQueryMetricsTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testScriptTimeout() {
         Callable<Map<String, Long>> runScript = () -> {
 
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementCancelSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementCancelSelfTest.java
index 936625b3a04..a49b0e07a29 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementCancelSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementCancelSelfTest.java
@@ -77,7 +77,6 @@ public class ItJdbcStatementCancelSelfTest extends 
AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     void cancellationOfMultiStatementQuery() throws Exception {
         stmt.executeUpdate("CREATE TABLE dummy (id INT PRIMARY KEY, val INT)");
         stmt.setFetchSize(1);
diff --git 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementSelfTest.java
 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementSelfTest.java
index a40fa0aec41..100c87c7098 100644
--- 
a/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementSelfTest.java
+++ 
b/modules/jdbc/src/integrationTest/java/org/apache/ignite/jdbc/ItJdbcStatementSelfTest.java
@@ -380,7 +380,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testExecuteQueryMultipleOnlyResultSets() throws Exception {
         assertTrue(conn.getMetaData().supportsMultipleResultSets());
 
@@ -414,7 +413,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testExecuteQueryMultipleOnlyDml() throws Exception {
         Statement stmt0 = conn.createStatement();
 
@@ -449,7 +447,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testExecuteQueryMultipleMixed() throws Exception {
         int stmtCnt = 10;
 
@@ -677,7 +674,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testGetMoreResults() throws Exception {
         assertFalse(stmt.getMoreResults());
 
@@ -787,7 +783,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testStatementTypeMismatchSelectForCachedQuery() throws 
Exception {
         // Put query to cache.
         stmt.executeQuery("select 1;");
@@ -820,7 +815,6 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
     }
 
     @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-26142";)
     public void testOpenCursorsPureQuery() throws Exception {
         stmt.execute("SELECT 1; SELECT 2;");
         ResultSet rs = stmt.getResultSet();
@@ -834,7 +828,7 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
         }
 
         stmt.close();
-        assertTrue(waitForCondition(() -> openCursors() == 0, 5_000));
+        assertTrue(waitForCondition(() -> openCursors(CLUSTER) == 0, 5_000));
     }
 
     @Test
@@ -845,7 +839,7 @@ public class ItJdbcStatementSelfTest extends 
ItJdbcAbstractStatementSelfTest {
         stmt.execute("DROP TABLE T1");
         stmt.getResultSet();
 
-        assertTrue(waitForCondition(() -> openCursors() == 0, 5_000));
+        assertTrue(waitForCondition(() -> openCursors(CLUSTER) == 0, 5_000));
     }
 
     @Test
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSet.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSet.java
new file mode 100644
index 00000000000..a565e073947
--- /dev/null
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSet.java
@@ -0,0 +1,46 @@
+/*
+ * 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.jdbc2;
+
+import java.util.Iterator;
+import java.util.List;
+import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
+import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.sql.SqlRow;
+
+/**
+ * Sync result set.
+ */
+interface ClientSyncResultSet extends Iterator<SqlRow> {
+
+    ResultSetMetadata EMPTY_METADATA = new ResultSetMetadataImpl(List.of());
+
+    ResultSetMetadata metadata();
+
+    boolean hasRowSet();
+
+    long affectedRows();
+
+    boolean wasApplied();
+
+    boolean hasNextResultSet();
+
+    ClientSyncResultSet nextResultSet();
+
+    void close();
+}
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSetImpl.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSetImpl.java
new file mode 100644
index 00000000000..2d1f49e909e
--- /dev/null
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ClientSyncResultSetImpl.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jdbc2;
+
+import org.apache.ignite.internal.client.sql.ClientAsyncResultSet;
+import org.apache.ignite.internal.sql.SyncResultSetAdapter;
+import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.sql.SqlRow;
+
+/**
+ * Implementation of {@link ClientSyncResultSet}.
+ */
+final class ClientSyncResultSetImpl implements ClientSyncResultSet {
+
+    private final ClientAsyncResultSet<SqlRow> rs;
+
+    private final SyncResultSetAdapter<SqlRow> syncRs;
+
+    ClientSyncResultSetImpl(ClientAsyncResultSet<SqlRow> rs) {
+        this.rs = rs;
+        this.syncRs = new SyncResultSetAdapter<>(rs);
+    }
+
+    @Override
+    public ResultSetMetadata metadata() {
+        ResultSetMetadata metadata = syncRs.metadata();
+        return metadata != null ? metadata : EMPTY_METADATA;
+    }
+
+    @Override
+    public boolean hasRowSet() {
+        return syncRs.hasRowSet();
+    }
+
+    @Override
+    public long affectedRows() {
+        return syncRs.affectedRows();
+    }
+
+    @Override
+    public boolean wasApplied() {
+        return syncRs.wasApplied();
+    }
+
+    @Override
+    public boolean hasNextResultSet() {
+        return rs.hasNextResultSet();
+    }
+
+    @Override
+    public ClientSyncResultSet nextResultSet() {
+        if (!rs.hasNextResultSet()) {
+            throw new IllegalStateException("Should not have been called.");
+        }
+
+        ClientAsyncResultSet<SqlRow> nextRs = rs.nextResultSet().join();
+        return new ClientSyncResultSetImpl(nextRs);
+    }
+
+    @Override
+    public void close() {
+        syncRs.close();
+    }
+
+    @Override
+    public boolean hasNext() {
+        return syncRs.hasNext();
+    }
+
+    @Override
+    public SqlRow next() {
+        return syncRs.next();
+    }
+}
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
index 1c0b3bc4741..e353df28164 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
@@ -51,12 +51,12 @@ import org.apache.ignite.internal.jdbc.ConnectionProperties;
 import org.apache.ignite.internal.jdbc.JdbcDatabaseMetadata;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryEventHandler;
 import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
-import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.sql.SqlCommon;
 import org.apache.ignite.sql.IgniteSql;
 import org.apache.ignite.tx.Transaction;
 import org.apache.ignite.tx.TransactionOptions;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
 
 /**
  * {@link Connection} implementation backed by the thin client.
@@ -282,7 +282,7 @@ public class JdbcConnection2 implements Connection {
         try {
             tx.commit();
         } catch (Exception e) {
-            throw new SQLException(COMMIT_REQUEST_FAILED, 
IgniteExceptionMapperUtil.mapToPublicException(e));
+            throw 
JdbcExceptionMapperUtil.mapToJdbcException(COMMIT_REQUEST_FAILED, e);
         }
     }
 
@@ -298,7 +298,7 @@ public class JdbcConnection2 implements Connection {
         try {
             tx.rollback();
         } catch (Exception e) {
-            throw new SQLException(ROLLBACK_REQUEST_FAILED, 
IgniteExceptionMapperUtil.mapToPublicException(e));
+            throw 
JdbcExceptionMapperUtil.mapToJdbcException(ROLLBACK_REQUEST_FAILED, e);
         }
     }
 
@@ -831,6 +831,11 @@ public class JdbcConnection2 implements Connection {
         return properties;
     }
 
+    @TestOnly
+    void closeClient() {
+        igniteClient.close();
+    }
+
     private static void checkCursorOptions(
             int resSetType,
             int resSetConcurrency,
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcExceptionMapperUtil.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcExceptionMapperUtil.java
new file mode 100644
index 00000000000..34c3965ab38
--- /dev/null
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcExceptionMapperUtil.java
@@ -0,0 +1,56 @@
+/*
+ * 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.jdbc2;
+
+import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
+
+import java.sql.SQLException;
+import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
+import org.apache.ignite.lang.ErrorGroups.Sql;
+import org.apache.ignite.lang.IgniteException;
+
+/**
+ * Maps an exception to a {@link SQLException}.
+ */
+final class JdbcExceptionMapperUtil {
+
+    private static final String TX_CONTROL_STATEMENT_WHEN_AUTOCOMMIT_MODE_OFF 
= 
+            "Transaction control statements are not supported when autocommit 
mode is disabled.";
+
+    private JdbcExceptionMapperUtil() {
+
+    }
+
+    static SQLException mapToJdbcException(String message, Exception e) {
+        return new SQLException(message, 
IgniteExceptionMapperUtil.mapToPublicException(unwrapCause(e)));
+    }
+
+    static SQLException mapToJdbcException(Exception e) {
+        Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(unwrapCause(e));
+        String message = cause.getMessage();
+
+        if (cause instanceof IgniteException) {
+            IgniteException ie = (IgniteException) cause;
+            if (ie.code() == Sql.TX_CONTROL_INSIDE_EXTERNAL_TX_ERR) {
+                message = TX_CONTROL_STATEMENT_WHEN_AUTOCOMMIT_MODE_OFF;
+            }
+        }
+
+        return new SQLException(message, cause);
+    }
+}
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement2.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement2.java
index 3e6148a5957..114eb5f5d0f 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement2.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement2.java
@@ -58,7 +58,6 @@ import java.util.UUID;
 import org.apache.ignite.internal.client.sql.ClientSql;
 import org.apache.ignite.internal.jdbc.proto.IgniteQueryErrorCode;
 import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
-import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.util.CollectionUtils;
 import org.apache.ignite.lang.CancelHandle;
 import org.apache.ignite.sql.BatchedArguments;
@@ -179,8 +178,7 @@ public class JdbcPreparedStatement2 extends JdbcStatement2 
implements PreparedSt
                     IgniteQueryErrorCode.UNKNOWN,
                     longsArrayToIntsArrayUnsafe(e.updateCounters()));
         } catch (Exception e) {
-            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
-            throw new SQLException(cause.getMessage(), cause);
+            throw JdbcExceptionMapperUtil.mapToJdbcException(e);
         } finally {
             batchedArgs = null;
         }
@@ -233,7 +231,10 @@ public class JdbcPreparedStatement2 extends JdbcStatement2 
implements PreparedSt
     public boolean execute() throws SQLException {
         execute0(ALL, sql, currentArguments.toArray());
 
-        return isQuery();
+        ResultSetWrapper rs = result;
+        assert rs != null;
+
+        return rs.isQuery();
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
index ad6d4b1d39a..6863ffc40c3 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
@@ -52,13 +52,10 @@ import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
 import java.util.Calendar;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.function.Supplier;
 import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
-import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
-import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
 import org.apache.ignite.internal.util.StringUtils;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
@@ -77,13 +74,11 @@ public class JdbcResultSet implements ResultSet {
 
     private static final String SQL_SPECIFIC_TYPES_ARE_NOT_SUPPORTED = 
"SQL-specific types are not supported.";
 
-    private static final ResultSetMetadata EMPTY_METADATA = new 
ResultSetMetadataImpl(List.of());
-
     private static final BigDecimal MIN_DOUBLE = 
BigDecimal.valueOf(-Double.MAX_VALUE);
 
     private static final BigDecimal MAX_DOUBLE = 
BigDecimal.valueOf(Double.MAX_VALUE);
 
-    private final org.apache.ignite.sql.ResultSet<SqlRow> rs;
+    private final ClientSyncResultSet rs;
 
     private final ResultSetMetadata rsMetadata;
 
@@ -103,7 +98,7 @@ public class JdbcResultSet implements ResultSet {
 
     private int currentPosition;
 
-    private boolean closed;
+    boolean closed;
 
     private boolean wasNull;
 
@@ -111,7 +106,7 @@ public class JdbcResultSet implements ResultSet {
      * Constructor.
      */
     JdbcResultSet(
-            org.apache.ignite.sql.ResultSet<SqlRow> rs,
+            ClientSyncResultSet rs,
             Statement statement,
             Supplier<ZoneId> zoneIdSupplier,
             boolean closeOnCompletion,
@@ -119,9 +114,7 @@ public class JdbcResultSet implements ResultSet {
     ) {
         this.rs = rs;
 
-        ResultSetMetadata metadata = rs.metadata();
-        this.rsMetadata = metadata != null ? metadata : EMPTY_METADATA;
-
+        this.rsMetadata = rs.metadata();
         this.zoneIdSupplier = zoneIdSupplier;
         this.statement = statement;
         this.currentRow = null;
@@ -132,21 +125,36 @@ public class JdbcResultSet implements ResultSet {
         this.maxRows = maxRows;
     }
 
-    int updateCount() {
-        assert !isQuery() : "Should not be called on a query";
-        if (rs.wasApplied() || rs.affectedRows() == -1) {
-            return 0;
-        } else {
-            //noinspection NumericCastThatLosesPrecision
-            return (int) rs.affectedRows();
+    ClientSyncResultSet resultSet() {
+        return rs;
+    }
+
+    @Nullable
+    JdbcResultSet tryNextResultSet() throws SQLException {
+        if (!rs.hasNextResultSet()) {
+            return null;
         }
+
+        ClientSyncResultSet clientResultSet;
+
+        try {
+            clientResultSet = rs.nextResultSet();
+        } catch (Exception e) {
+            throw JdbcExceptionMapperUtil.mapToJdbcException(e);
+        }
+
+        JdbcStatement2 jdbcStatement = statement.unwrap(JdbcStatement2.class);
+        // isCloseOnCompletion throws SQLException if a statement is closed
+        boolean closeOnCompletion = jdbcStatement.closeOnCompletion;
+
+        return new JdbcResultSet(clientResultSet, statement, zoneIdSupplier, 
closeOnCompletion, maxRows);
     }
 
-    boolean isQuery() {
-        return rs.hasRowSet();
+    boolean isCloseOnCompletion() {
+        return closeOnCompletion;
     }
 
-    void closeStatement(boolean close) {
+    void setCloseStatement(boolean close) {
         closeOnCompletion = close;
     }
 
@@ -167,8 +175,7 @@ public class JdbcResultSet implements ResultSet {
             currentPosition += 1;
             return true;
         } catch (Exception e) {
-            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
-            throw new SQLException(cause.getMessage(), cause);
+            throw JdbcExceptionMapperUtil.mapToJdbcException(e);
         }
     }
 
@@ -179,13 +186,15 @@ public class JdbcResultSet implements ResultSet {
         }
         closed = true;
 
+        boolean moreResultSets = rs.hasNextResultSet();
+
         try {
             rs.close();
         } catch (Exception e) {
-            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
-            throw new SQLException(cause.getMessage(), cause);
+            throw JdbcExceptionMapperUtil.mapToJdbcException(e);
         } finally {
-            if (closeOnCompletion) {
+            // Close the statement if this result set is the last one.
+            if (closeOnCompletion && !moreResultSets) {
                 JdbcStatement2 statement2 = 
statement.unwrap(JdbcStatement2.class);
                 statement2.closeIfAllResultsClosed();
             }
@@ -2102,8 +2111,7 @@ public class JdbcResultSet implements ResultSet {
 
             return val;
         } catch (Exception e) {
-            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
-            throw new SQLException("Unable to value for column: " + colIdx, 
cause);
+            throw JdbcExceptionMapperUtil.mapToJdbcException("Unable to value 
for column: " + colIdx, e);
         }
     }
 
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
index ea7656ba575..fa1251189cc 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
@@ -32,17 +32,15 @@ import java.util.EnumSet;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import org.apache.ignite.internal.client.sql.ClientAsyncResultSet;
 import org.apache.ignite.internal.client.sql.ClientSql;
 import org.apache.ignite.internal.client.sql.QueryModifier;
 import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
-import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
-import org.apache.ignite.internal.sql.SyncResultSetAdapter;
 import org.apache.ignite.internal.util.ArrayUtils;
 import org.apache.ignite.lang.CancelHandle;
 import org.apache.ignite.sql.IgniteSql;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.sql.Statement.StatementBuilder;
-import org.apache.ignite.sql.async.AsyncResultSet;
 import org.apache.ignite.table.mapper.Mapper;
 import org.apache.ignite.tx.Transaction;
 import org.jetbrains.annotations.Nullable;
@@ -92,7 +90,7 @@ public class JdbcStatement2 implements Statement {
 
     private final int rsHoldability;
 
-    private volatile @Nullable JdbcResultSet resultSet;
+    protected volatile @Nullable ResultSetWrapper result;
 
     private long queryTimeoutMillis;
 
@@ -102,7 +100,7 @@ public class JdbcStatement2 implements Statement {
 
     private volatile boolean closed;
 
-    private boolean closeOnCompletion;
+    boolean closeOnCompletion;
 
     volatile @Nullable CancelHandle cancelHandle;
 
@@ -123,7 +121,10 @@ public class JdbcStatement2 implements Statement {
     public ResultSet executeQuery(String sql) throws SQLException {
         execute0(QUERY, Objects.requireNonNull(sql), 
ArrayUtils.OBJECT_EMPTY_ARRAY);
 
-        ResultSet rs = getResultSet();
+        ResultSetWrapper currentRs = result;
+        assert currentRs != null;
+
+        ResultSet rs = currentRs.current();
 
         if (rs == null) {
             throw new SQLException("The query isn't SELECT query: " + sql, 
SqlStateCode.PARSING_EXCEPTION);
@@ -132,9 +133,9 @@ public class JdbcStatement2 implements Statement {
         return rs;
     }
 
-    JdbcResultSet createResultSet(org.apache.ignite.sql.ResultSet<SqlRow> 
resultSet) throws SQLException {
-        JdbcConnection2 connection2 = connection.unwrap(JdbcConnection2.class);
-        ZoneId zoneId = connection2.properties().getConnectionTimeZone();
+    JdbcResultSet createResultSet(ClientSyncResultSet resultSet) throws 
SQLException {
+        JdbcConnection2 jdbcConnection = 
connection.unwrap(JdbcConnection2.class);
+        ZoneId zoneId = jdbcConnection.properties().getConnectionTimeZone();
         return new JdbcResultSet(resultSet, this, () -> zoneId, 
closeOnCompletion, maxRows);
     }
 
@@ -157,16 +158,6 @@ public class JdbcStatement2 implements Statement {
         JdbcConnection2 connection2 = connection.unwrap(JdbcConnection2.class);
         Transaction tx = connection2.startTransactionIfNoAutoCommit();
 
-        // TODO https://issues.apache.org/jira/browse/IGNITE-26142 
multistatement.
-        if (sql.indexOf(';') == -1 || sql.indexOf(';') == sql.length() - 1) {
-            queryModifiers.remove(QueryModifier.ALLOW_MULTISTATEMENT);
-        }
-
-        // TODO https://issues.apache.org/jira/browse/IGNITE-26142 
multistatement.
-        if (queryModifiers.contains(QueryModifier.ALLOW_MULTISTATEMENT)) {
-            throw new UnsupportedOperationException("Multi-statements are not 
supported yet.");
-        }
-
         org.apache.ignite.sql.Statement igniteStmt = 
createIgniteStatement(sql);
         ClientSql clientSql = (ClientSql) igniteSql;
 
@@ -174,9 +165,9 @@ public class JdbcStatement2 implements Statement {
         CancelHandle handle = CancelHandle.create();
         cancelHandle = handle;
 
-        AsyncResultSet<SqlRow> clientRs;
+        ClientAsyncResultSet<SqlRow> clientRs;
         try {
-            clientRs = clientSql.executeAsyncInternal(tx,
+            clientRs = (ClientAsyncResultSet<SqlRow>) 
clientSql.executeAsyncInternal(tx,
                     (Mapper<SqlRow>) null,
                     handle.token(),
                     queryModifiers,
@@ -184,12 +175,9 @@ public class JdbcStatement2 implements Statement {
                     args
             ).join();
 
-            SyncResultSetAdapter<SqlRow> syncRs = new 
SyncResultSetAdapter<>(clientRs);
-
-            resultSet = createResultSet(syncRs);
+            result = new ResultSetWrapper(createResultSet(new 
ClientSyncResultSetImpl(clientRs)));
         } catch (Exception e) {
-            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
-            throw new SQLException(cause.getMessage(), cause);
+            throw JdbcExceptionMapperUtil.mapToJdbcException(e);
         }
     }
 
@@ -200,14 +188,15 @@ public class JdbcStatement2 implements Statement {
 
         execute0(DML_OR_DDL, sql, ArrayUtils.OBJECT_EMPTY_ARRAY);
 
-        int rowCount = getUpdateCount();
+        ResultSetWrapper rs = result;
+        assert rs != null;
 
-        if (isQuery()) {
+        if (rs.isQuery()) {
             closeResults();
             throw new SQLException("The query is not DML statement: " + sql);
         }
 
-        return Math.max(rowCount, 0);
+        return rs.updateCount();
     }
 
     /** {@inheritDoc} */
@@ -362,7 +351,10 @@ public class JdbcStatement2 implements Statement {
 
         execute0(QueryModifier.ALL, Objects.requireNonNull(sql), 
ArrayUtils.OBJECT_EMPTY_ARRAY);
 
-        return isQuery();
+        ResultSetWrapper rs = result;
+        assert rs != null;
+
+        return rs.isQuery();
     }
 
     /** {@inheritDoc} */
@@ -411,7 +403,12 @@ public class JdbcStatement2 implements Statement {
     public @Nullable ResultSet getResultSet() throws SQLException {
         ensureNotClosed();
 
-        return isQuery() ? resultSet : null;
+        ResultSetWrapper rs = result;
+        if (rs != null && rs.isQuery()) {
+            return rs.current();
+        } else {
+            return null;
+        }
     }
 
     /** {@inheritDoc} */
@@ -419,8 +416,8 @@ public class JdbcStatement2 implements Statement {
     public int getUpdateCount() throws SQLException {
         ensureNotClosed();
 
-        JdbcResultSet rs = resultSet;
-        if (rs == null || rs.isQuery()) {
+        ResultSetWrapper rs = result;
+        if (rs == null) {
             return -1;
         } else {
             return rs.updateCount();
@@ -440,15 +437,20 @@ public class JdbcStatement2 implements Statement {
 
         switch (current) {
             case CLOSE_CURRENT_RESULT:
-
-                JdbcResultSet currentRs = resultSet;
-                if (currentRs == null) {
+                ResultSetWrapper currentResult = result;
+                if (currentResult == null) {
                     return false;
                 }
 
-                resultSet = null;
-                currentRs.close();
-                return false;
+                boolean moreResults = currentResult.nextResultSet();
+                if (!moreResults) {
+                    // next() closes the remaining result set if necessary
+                    result = null;
+
+                    return false;
+                } else {
+                    return currentResult.isQuery();
+                }
 
             case CLOSE_ALL_RESULTS:
             case KEEP_CURRENT_RESULT:
@@ -623,9 +625,9 @@ public class JdbcStatement2 implements Statement {
 
         closeOnCompletion = true;
 
-        JdbcResultSet rs = resultSet;
+        ResultSetWrapper rs = result;
         if (rs != null) {
-            rs.closeStatement(true);
+            rs.setCloseStatement(true);
         }
     }
 
@@ -659,16 +661,6 @@ public class JdbcStatement2 implements Statement {
         this.queryTimeoutMillis = timeoutMillis;
     }
 
-    protected boolean isQuery() {
-        // This method is called after statement is executed, so the reference 
points to a correct result set.
-        // The statement is not expected to be used from multiple threads, so 
this reference points to a correct result set.
-        // getResultSet() performs its own result set checks.
-        JdbcResultSet rs = resultSet;
-        assert rs != null;
-
-        return rs.isQuery();
-    }
-
     org.apache.ignite.sql.Statement createIgniteStatement(String sql) throws 
SQLException {
         StatementBuilder builder = igniteSql.statementBuilder()
                 .query(sql)
@@ -695,11 +687,11 @@ public class JdbcStatement2 implements Statement {
     }
 
     private void closeResults() throws SQLException {
-        JdbcResultSet rs = resultSet;
+        ResultSetWrapper rs = result;
+        result = null;
         if (rs != null) {
             rs.close();
         }
-        resultSet = null;
     }
 
     /**
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ResultSetWrapper.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ResultSetWrapper.java
new file mode 100644
index 00000000000..fc600917da9
--- /dev/null
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/ResultSetWrapper.java
@@ -0,0 +1,99 @@
+/*
+ * 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.jdbc2;
+
+import java.sql.SQLException;
+
+final class ResultSetWrapper {
+
+    private JdbcResultSet resultSet;
+
+    ResultSetWrapper(JdbcResultSet first) {
+        resultSet = first;
+    }
+
+    void setCloseStatement(boolean b) {
+        resultSet.setCloseStatement(b);
+    }
+
+    JdbcResultSet current() {
+        return resultSet;
+    }
+
+    boolean isQuery() {
+        ClientSyncResultSet rs = resultSet.resultSet();
+
+        return rs.hasRowSet();
+    }
+
+    int updateCount() {
+        JdbcResultSet rs = resultSet;
+
+        ClientSyncResultSet clientResultSet = rs.resultSet();
+        if (clientResultSet.hasRowSet()) {
+            return -1;
+        } else if (clientResultSet.affectedRows() == -1) {
+            // DDL or control statements
+            return 0;
+        } else {
+            return (int) clientResultSet.affectedRows();
+        }
+    }
+
+    boolean nextResultSet() throws SQLException {
+        JdbcResultSet current = resultSet;
+        if (current == null) {
+            return false;
+        }
+
+        try {
+            JdbcResultSet nextRs = current.tryNextResultSet();
+            boolean hasNext = nextRs != null;
+
+            resultSet = nextRs;
+
+            return hasNext;
+        } finally {
+            current.close();
+        }
+    }
+
+    void close() throws SQLException {
+        JdbcResultSet current = resultSet;
+        if (current == null || current.closed) {
+            return;
+        }
+        resultSet = null;
+
+        JdbcResultSet rs = current;
+
+        do {
+            // TODO: https://issues.apache.org/jira/browse/IGNITE-26789 Close 
properly.
+            // w/o iteration over all cursors, the cursors that are not 
retrieved hung in the void
+            // and are never released.
+            try {
+                rs = rs.tryNextResultSet();
+            } catch (SQLException ignore) {
+                // This is an execution related exception, ignore it. 
+                break;
+            }
+        } while (rs != null);
+
+        current.close();
+    }
+}
diff --git 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
index 01bfc0cda3e..54d52830544 100644
--- 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
+++ 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
@@ -134,18 +134,18 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
     }
 
     @Test
-    @SuppressWarnings("unchecked")
     public void nextExceptionIsWrapped() {
         // ClientResultSet hasNext() throws
         {
             Statement statement = Mockito.mock(Statement.class);
 
-            org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+            ClientSyncResultSet clientRs = 
Mockito.mock(ClientSyncResultSet.class);
+            
when(clientRs.metadata()).thenReturn(ClientSyncResultSet.EMPTY_METADATA);
 
             RuntimeException cause = new RuntimeException("Some error");
-            when(igniteRs.hasNext()).thenThrow(cause);
+            when(clientRs.hasNext()).thenThrow(cause);
 
-            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
+            ResultSet rs = new JdbcResultSet(clientRs, statement, 
ZoneId::systemDefault, false, 0);
 
             SQLException err = assertThrows(SQLException.class, rs::next);
             assertEquals("Some error", err.getMessage());
@@ -157,13 +157,14 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         {
             Statement statement = Mockito.mock(Statement.class);
 
-            org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+            ClientSyncResultSet clientRs = 
Mockito.mock(ClientSyncResultSet.class);
+            
when(clientRs.metadata()).thenReturn(ClientSyncResultSet.EMPTY_METADATA);
 
             RuntimeException cause = new RuntimeException("Some error");
-            when(igniteRs.hasNext()).thenReturn(true);
-            when(igniteRs.next()).thenThrow(cause);
+            when(clientRs.hasNext()).thenReturn(true);
+            when(clientRs.next()).thenThrow(cause);
 
-            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
+            ResultSet rs = new JdbcResultSet(clientRs, statement, 
ZoneId::systemDefault, false, 0);
 
             SQLException err = assertThrows(SQLException.class, rs::next);
             assertEquals("Some error", err.getMessage());
@@ -173,40 +174,40 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
     }
 
     @Test
-    @SuppressWarnings("unchecked")
     public void closeClosesResultSet() throws SQLException {
         Statement statement = Mockito.mock(Statement.class);
 
-        org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
-        when(igniteRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of()));
+        ClientSyncResultSet clientRs = Mockito.mock(ClientSyncResultSet.class);
+        
when(clientRs.metadata()).thenReturn(ClientSyncResultSet.EMPTY_METADATA);
 
         JdbcStatement2 statement2 = Mockito.mock(JdbcStatement2.class);
         when(statement.unwrap(JdbcStatement2.class)).thenReturn(statement2);
 
-        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, true, 0);
+        ResultSet rs = new JdbcResultSet(clientRs, statement, 
ZoneId::systemDefault, true, 0);
 
         rs.close();
         rs.close();
 
-        verify(igniteRs, times(1)).close();
-        verify(igniteRs, times(1)).metadata();
+        verify(clientRs, times(1)).close();
+        verify(clientRs, times(1)).metadata();
         verify(statement2, times(1)).closeIfAllResultsClosed();
-        verifyNoMoreInteractions(igniteRs, statement2);
+        verify(clientRs, times(1)).hasNextResultSet();
+        verifyNoMoreInteractions(clientRs, statement2);
     }
 
     @ParameterizedTest
     @ValueSource(booleans = {true, false})
-    @SuppressWarnings("unchecked")
     public void closeExceptionIsWrapped(boolean closeOnCompletion) throws 
SQLException {
         JdbcStatement2 statement = Mockito.mock(JdbcStatement2.class);
         when(statement.unwrap(JdbcStatement2.class)).thenReturn(statement);
 
-        org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+        ClientSyncResultSet clientRs = Mockito.mock(ClientSyncResultSet.class);
+        
when(clientRs.metadata()).thenReturn(ClientSyncResultSet.EMPTY_METADATA);
 
         RuntimeException cause = new RuntimeException("Some error");
-        doAnswer(new ThrowsException(cause)).when(igniteRs).close();
+        doAnswer(new ThrowsException(cause)).when(clientRs).close();
 
-        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, closeOnCompletion, 0);
+        ResultSet rs = new JdbcResultSet(clientRs, statement, 
ZoneId::systemDefault, closeOnCompletion, 0);
 
         SQLException err = assertThrows(SQLException.class, rs::close);
         assertEquals("Some error", err.getMessage());
@@ -218,19 +219,19 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
     public void getValueExceptionIsWrapped() throws SQLException {
         Statement statement = Mockito.mock(Statement.class);
 
-        org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+        ClientSyncResultSet clientRs = Mockito.mock(ClientSyncResultSet.class);
         SqlRow row = Mockito.mock(SqlRow.class);
 
         ColumnMetadataImpl column = new ColumnMetadataImpl("C", 
ColumnType.INT32, 0, 0, false, null);
 
-        when(igniteRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of(column)));
-        when(igniteRs.hasNext()).thenReturn(true);
-        when(igniteRs.next()).thenReturn(row);
+        when(clientRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of(column)));
+        when(clientRs.hasNext()).thenReturn(true);
+        when(clientRs.next()).thenReturn(row);
 
         RuntimeException cause = new RuntimeException("Corrupted value");
         when(row.value(0)).thenThrow(cause);
 
-        JdbcResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
+        JdbcResultSet rs = new JdbcResultSet(clientRs, statement, 
ZoneId::systemDefault, false, 0);
         assertTrue(rs.next());
 
         SQLException err = assertThrows(SQLException.class, () -> 
rs.getValue(1));
@@ -291,7 +292,6 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         return createResultSet(statement, zoneId, cols, rows, 0);
     }
 
-    @SuppressWarnings("unchecked")
     private static ResultSet createResultSet(
             Statement statement,
             @SuppressWarnings("unused")
@@ -311,8 +311,8 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
 
         // ResultSet has no metadata
         if (cols.isEmpty() && rows.isEmpty()) {
-            org.apache.ignite.sql.ResultSet<SqlRow> rs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
-            when(rs.metadata()).thenReturn(null);
+            ClientSyncResultSet rs = Mockito.mock(ClientSyncResultSet.class);
+            when(rs.metadata()).thenReturn(ClientSyncResultSet.EMPTY_METADATA);
 
             return new JdbcResultSet(rs, statement, zoneIdSupplier, false, 0);
         }
@@ -332,7 +332,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         return new JdbcResultSet(new ResultSetStub(apiMeta, rows), statement, 
zoneIdSupplier, false, maxRows);
     }
 
-    private static class ResultSetStub implements 
org.apache.ignite.sql.ResultSet<SqlRow> {
+    private static class ResultSetStub implements ClientSyncResultSet {
         private final ResultSetMetadata meta;
         private final Iterator<List<Object>> it;
         private @Nullable List<Object> current;
@@ -363,6 +363,16 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
             throw new IllegalStateException("Should not be called");
         }
 
+        @Override
+        public boolean hasNextResultSet() {
+            return false;
+        }
+
+        @Override
+        public ClientSyncResultSet nextResultSet() {
+            throw new IllegalStateException("Should not be called");
+        }
+
         @Override
         public void close() {
             // Does nothing, checked separately.
diff --git 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
index 0f1f8b87070..9b820028415 100644
--- 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
+++ 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
@@ -37,7 +37,6 @@ import 
org.apache.ignite.internal.jdbc.ConnectionPropertiesImpl;
 import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
 import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.apache.ignite.sql.IgniteSql;
-import org.apache.ignite.sql.SqlRow;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.function.Executable;
 import org.mockito.Mockito;
@@ -379,8 +378,7 @@ public class JdbcStatement2SelfTest extends 
BaseIgniteAbstractTest {
         try (Statement stmt = createStatement()) {
             JdbcStatement2 jdbc = stmt.unwrap(JdbcStatement2.class);
 
-            @SuppressWarnings("unchecked")
-            org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+            ClientSyncResultSet igniteRs = 
Mockito.mock(ClientSyncResultSet.class);
             when(igniteRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of()));
 
             {
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMultistatementSqlTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMultistatementSqlTest.java
index bfb3ab34645..32e93d14e77 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMultistatementSqlTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMultistatementSqlTest.java
@@ -418,6 +418,21 @@ public class ItThinClientMultistatementSqlTest extends 
ItAbstractThinClientTest
         });
     }
 
+    @Test
+    public void executeScriptWithErrors() {
+        client().sql().executeScript("SELECT 1; SELECT 2/0; SELECT 3;");
+    }
+
+    @Test
+    public void iterateOverScriptWithErrors() {
+        ClientAsyncResultSet<SqlRow> rs = runSql("SELECT 1; SELECT 2/0; SELECT 
3; SELECT 4;");
+        try {
+            rs.nextResultSet().join();
+        } catch (Exception ignore) {
+            // Resources should not leak after this error.
+        }
+    }
+
     private void expectRowsCount(@Nullable Transaction tx, String table, long 
expectedCount) {
         try (ResultSet<SqlRow> rs = client().sql().execute(tx, "SELECT 
COUNT(*) FROM " + table)) {
             assertThat(rs.next().longValue(0), is(expectedCount));
@@ -444,7 +459,7 @@ public class ItThinClientMultistatementSqlTest extends 
ItAbstractThinClientTest
         return count;
     }
 
-    private ClientAsyncResultSet<SqlRow> runSql(String query, Object ... args) 
{
+    private ClientAsyncResultSet<SqlRow> runSql(String query, Object... args) {
         return runSql(null, null, null, query, args);
     }
 
@@ -474,7 +489,7 @@ public class ItThinClientMultistatementSqlTest extends 
ItAbstractThinClientTest
         executeSql(null, sql, args);
     }
 
-    private void executeSql(@Nullable Transaction tx, String sql, Object ... 
args) {
+    private void executeSql(@Nullable Transaction tx, String sql, Object... 
args) {
         fetchAllResults(runSql(tx, null, null, sql, args))
                 .forEach(rs -> await(rs.closeAsync()));
     }

Reply via email to