You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ko...@apache.org on 2022/01/14 15:15:12 UTC

[ignite-3] branch main updated: IGNITE-15648 Added JDBC integration tests (#522)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5894611  IGNITE-15648 Added JDBC integration tests (#522)
5894611 is described below

commit 58946113a1977333343d79607da3951041edef08
Author: Vladimir Ermakov <85...@users.noreply.github.com>
AuthorDate: Fri Jan 14 18:15:05 2022 +0300

    IGNITE-15648 Added JDBC integration tests (#522)
---
 .../proto/query/event/BatchExecuteResult.java      |   3 +-
 .../client/handler/JdbcQueryEventHandlerImpl.java  |   4 +-
 .../ignite/internal/jdbc/JdbcConnection.java       |  28 +-
 .../runner/app/jdbc/AbstractJdbcSelfTest.java      |  69 ++-
 .../app/jdbc/ItJdbcAbstractStatementSelfTest.java  |  55 ++
 .../app/jdbc/ItJdbcComplexDmlDdlSelfTest.java      | 396 +++++++++++++
 .../app/jdbc/ItJdbcComplexQuerySelfTest.java       | 231 ++++++++
 .../runner/app/jdbc/ItJdbcConnectionSelfTest.java  |  88 +--
 .../app/jdbc/ItJdbcDeleteStatementSelfTest.java    |  68 +++
 .../app/jdbc/ItJdbcErrorsAbstractSelfTest.java     | 632 +++++++++++++++++++++
 .../runner/app/jdbc/ItJdbcErrorsSelfTest.java      | 120 ++++
 .../app/jdbc/ItJdbcInsertStatementSelfTest.java    | 214 +++++++
 .../runner/app/jdbc/ItJdbcJoinsSelfTest.java       | 164 ++++++
 .../jdbc/ItJdbcMetadataPrimaryKeysSelfTest.java    | 105 ++++
 .../runner/app/jdbc/ItJdbcMetadataSelfTest.java    | 318 +++++------
 .../app/jdbc/ItJdbcMultiStatementSelfTest.java     | 187 ++++++
 .../runner/app/jdbc/ItJdbcResultSetSelfTest.java   | 238 ++++----
 .../app/jdbc/ItJdbcSelectAfterAlterTable.java      |  91 +++
 .../app/jdbc/ItJdbcStatementCancelSelfTest.java    | 157 +++++
 .../runner/app/jdbc/ItJdbcStatementSelfTest.java   | 163 +++---
 .../app/jdbc/ItJdbcUpdateStatementSelfTest.java    |  69 +++
 21 files changed, 2914 insertions(+), 486 deletions(-)

diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/BatchExecuteResult.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/BatchExecuteResult.java
index 5568c4f..e3dbc75 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/BatchExecuteResult.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/BatchExecuteResult.java
@@ -21,6 +21,7 @@ import java.util.Objects;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
 import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.internal.util.ArrayUtils;
 
 /**
  * JDBC batch execute result.
@@ -64,7 +65,7 @@ public class BatchExecuteResult extends Response {
      * @return Update count for DML queries.
      */
     public int[] updateCounts() {
-        return updateCnts;
+        return updateCnts == null ? ArrayUtils.INT_EMPTY_ARRAY : updateCnts;
     }
 
     /** {@inheritDoc} */
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
index f8681b9..6c9b170 100644
--- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
@@ -290,14 +290,14 @@ public class JdbcQueryEventHandlerImpl implements JdbcQueryEventHandler {
             case QUERY:
                 return new QuerySingleResult(cursorId, fetch, !hasNext);
             case DML:
-            case DDL: {
                 if (!validateDmlResult(fetch, hasNext)) {
                     return new QuerySingleResult(Response.STATUS_FAILED,
                             "Unexpected result for DML query [" + req.sqlQuery() + "].");
                 }
 
                 return new QuerySingleResult(cursorId, (Long) fetch.get(0).get(0));
-            }
+            case DDL:
+                return new QuerySingleResult(cursorId, 0);
             default:
                 return new QuerySingleResult(UNSUPPORTED_OPERATION,
                         "Query type [" + cur.queryType() + "] is not supported yet.");
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
index 63ee238..857dd2a 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
@@ -21,6 +21,7 @@ import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT;
 import static java.sql.ResultSet.CONCUR_READ_ONLY;
 import static java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT;
 import static java.sql.ResultSet.TYPE_FORWARD_ONLY;
+import static org.apache.ignite.client.proto.query.SqlStateCode.CLIENT_CONNECTION_FAILED;
 import static org.apache.ignite.client.proto.query.SqlStateCode.CONNECTION_CLOSED;
 
 import java.sql.Array;
@@ -134,7 +135,7 @@ public class JdbcConnection implements Connection {
      *
      * @param props Connection properties.
      */
-    public JdbcConnection(ConnectionProperties props) {
+    public JdbcConnection(ConnectionProperties props) throws SQLException {
         this.connProps = props;
         autoCommit = true;
 
@@ -148,14 +149,19 @@ public class JdbcConnection implements Connection {
         long reconnectThrottlingPeriod = connProps.getReconnectThrottlingPeriod();
         int reconnectThrottlingRetries = connProps.getReconnectThrottlingRetries();
 
-        client = ((TcpIgniteClient) IgniteClient
-                .builder()
-                .addresses(addrs)
-                .connectTimeout(netTimeout)
-                .retryLimit(retryLimit)
-                .reconnectThrottlingPeriod(reconnectThrottlingPeriod)
-                .reconnectThrottlingRetries(reconnectThrottlingRetries)
-                .build());
+        try {
+            client = ((TcpIgniteClient) IgniteClient
+                    .builder()
+                    .addresses(addrs)
+                    .connectTimeout(netTimeout)
+                    .retryLimit(retryLimit)
+                    .reconnectThrottlingPeriod(reconnectThrottlingPeriod)
+                    .reconnectThrottlingRetries(reconnectThrottlingRetries)
+                    .build());
+
+        } catch (Exception e) {
+            throw new SQLException("Failed to connect to server", CLIENT_CONNECTION_FAILED, e);
+        }
 
         this.handler = new JdbcClientQueryEventHandler(client);
 
@@ -228,7 +234,9 @@ public class JdbcConnection implements Connection {
 
         checkCursorOptions(resSetType, resSetConcurrency);
 
-        Objects.requireNonNull(sql);
+        if (sql == null) {
+            throw new SQLException("SQL string cannot be null.");
+        }
 
         JdbcPreparedStatement stmt = new JdbcPreparedStatement(this, sql, resSetHoldability, schema);
 
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/AbstractJdbcSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/AbstractJdbcSelfTest.java
index cdb3e40..9e1ba8c 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/AbstractJdbcSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/AbstractJdbcSelfTest.java
@@ -21,14 +21,22 @@ import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeN
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Statement;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Stream;
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgnitionManager;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.lang.IgniteLogger;
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.function.Executable;
 import org.junit.jupiter.api.io.TempDir;
@@ -38,23 +46,36 @@ import org.junit.jupiter.api.io.TempDir;
  */
 public class AbstractJdbcSelfTest {
     /** URL. */
-    protected static final String URL = "jdbc:ignite:thin://127.0.1.1:10800";
+    protected static final String URL = "jdbc:ignite:thin://127.0.0.1:10800";
 
     /** Cluster nodes. */
     protected static final List<Ignite> clusterNodes = new ArrayList<>();
 
+    /** Connection. */
+    protected static Connection conn;
+
+    /** Statement. */
+    protected Statement stmt;
+
+    /** Logger. */
+    protected IgniteLogger log;
+
     /**
      * Creates a cluster of three nodes.
      *
      * @param temp Temporal directory.
      */
     @BeforeAll
-    public static void beforeAll(@TempDir Path temp, TestInfo testInfo) {
+    public static void beforeAll(@TempDir Path temp, TestInfo testInfo) throws SQLException {
         String nodeName = testNodeName(testInfo, 47500);
 
         String configStr = "node.metastorageNodes: [ \"" + nodeName + "\" ]";
 
         clusterNodes.add(IgnitionManager.start(nodeName, configStr, temp.resolve(nodeName)));
+
+        conn = DriverManager.getConnection(URL);
+
+        conn.setSchema("PUBLIC");
     }
 
     /**
@@ -64,13 +85,51 @@ public class AbstractJdbcSelfTest {
      */
     @AfterAll
     public static void afterAll() throws Exception {
-        for (Ignite clusterNode : clusterNodes) {
-            clusterNode.close();
-        }
+        IgniteUtils.closeAll(
+                Stream.concat(
+                        Stream.of(conn != null && !conn.isClosed() ? conn : (AutoCloseable) () -> {/* NO-OP */}),
+                        clusterNodes.stream()
+                )
+        );
 
+        conn = null;
         clusterNodes.clear();
     }
 
+    @BeforeEach
+    protected void beforeTest() throws Exception {
+        stmt = conn.createStatement();
+
+        assert stmt != null;
+        assert !stmt.isClosed();
+    }
+
+    @AfterEach
+    protected void afterTest() throws Exception {
+        if (stmt != null) {
+            stmt.close();
+
+            assert stmt.isClosed();
+        }
+    }
+
+    /**
+     * Constructor.
+     */
+    @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
+    protected AbstractJdbcSelfTest() {
+        log = IgniteLogger.forClass(getClass());
+    }
+
+    /**
+     * Returns logger.
+     *
+     * @return Logger.
+     */
+    protected IgniteLogger logger() {
+        return log;
+    }
+
     /**
      * Checks that the function throws SQLException about a closed result set.
      *
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcAbstractStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcAbstractStatementSelfTest.java
new file mode 100644
index 0000000..31e33ac
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcAbstractStatementSelfTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.runner.app.jdbc;
+
+import java.sql.Statement;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Abstract jdbc test.
+ */
+public abstract class ItJdbcAbstractStatementSelfTest extends AbstractJdbcSelfTest {
+    /** SQL query to create a table. */
+    private static final String CREATE_TABLE_SQL = "CREATE TABLE public.person(id INTEGER PRIMARY KEY, sid VARCHAR,"
+            + " firstname VARCHAR NOT NULL, lastname VARCHAR NOT NULL, age INTEGER NOT NULL)";
+
+    /** SQL query to populate table. */
+    private static final String ITEMS_SQL = "INSERT INTO public.person(sid, id, firstname, lastname, age) VALUES "
+            + "('p1', 1, 'John', 'White', 25), "
+            + "('p2', 2, 'Joe', 'Black', 35), "
+            + "('p3', 3, 'Mike', 'Green', 40)";
+
+    /** SQL query to clear table. */
+    private static final String DROP_SQL = "DELETE FROM public.person;";
+
+    @BeforeEach
+    public void refillTable() throws Exception {
+        try (Statement s = conn.createStatement()) {
+            s.executeUpdate(DROP_SQL);
+            s.executeUpdate(ITEMS_SQL);
+        }
+    }
+
+    @BeforeAll
+    public static void createTable() throws Exception {
+        try (Statement s = conn.createStatement()) {
+            s.execute(CREATE_TABLE_SQL);
+        }
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexDmlDdlSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexDmlDdlSelfTest.java
new file mode 100644
index 0000000..fc08e48
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexDmlDdlSelfTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Base class for complex SQL tests based on JDBC driver.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-16207")
+public class ItJdbcComplexDmlDdlSelfTest extends AbstractJdbcSelfTest {
+    /** Names of companies to use. */
+    private static final List<String> COMPANIES = Arrays.asList("ASF", "GNU", "BSD");
+
+    /** Cities to use. */
+    private static final List<String> CITIES = Arrays.asList("St. Petersburg", "Boston", "Berkeley", "London");
+
+    /**
+     * Test CRD operations.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCreateSelectDrop() throws Exception {
+        sql(new UpdateChecker(0),
+                "CREATE TABLE person_t (ID int, NAME varchar, AGE int, COMPANY varchar, CITY varchar, "
+                        + "primary key (ID, NAME, CITY))");
+
+        sql(new UpdateChecker(0), "CREATE TABLE city (name varchar, population int, primary key (name))");
+
+        sql(new UpdateChecker(3),
+                "INSERT INTO city (name, population) values(?, ?), (?, ?), (?, ?)",
+                "St. Petersburg", 6000000,
+                "Boston", 2000000,
+                "London", 8000000
+        );
+
+        sql(new ResultColumnChecker("id", "name", "age", "comp"),
+                "SELECT id, name, age, company as comp FROM person_t where id < 50");
+
+        for (int i = 0; i < 100; i++) {
+            sql(new UpdateChecker(1),
+                    "INSERT INTO person_t (id, name, age, company, city) values (?, ?, ?, ?, ?)",
+                    i,
+                    "Person " + i,
+                    20 + (i % 10),
+                    COMPANIES.get(i % COMPANIES.size()),
+                    CITIES.get(i % CITIES.size())
+            );
+        }
+
+        final int[] cnt = {0};
+
+        sql(new ResultPredicateChecker((Object[] objs) -> {
+            int id = ((Integer) objs[0]);
+
+            if (id >= 50) {
+                return false;
+            }
+
+            if (20 + (id % 10) != ((Integer) objs[2])) {
+                return false;
+            }
+
+            if (!("Person " + id).equals(objs[1])) {
+                return false;
+            }
+
+            ++cnt[0];
+
+            return true;
+        }), "SELECT id, name, age FROM person_t where id < 50");
+
+        assertEquals(cnt[0], 50, "Invalid rows count");
+
+        // Berkeley is not present in City table, although 25 people have it specified as their city.
+        sql(new ResultChecker(new Object[][] {{75L}}),
+                "SELECT COUNT(*) from person_t p inner join City c on p.city = c.name");
+
+        sql(new UpdateChecker(34),
+                "UPDATE person_t SET company = 'New Company', age = CASE WHEN MOD(id, 2) <> 0 THEN age + 5 ELSE "
+                    + "age + 1 END WHERE company = 'ASF'");
+
+        cnt[0] = 0;
+
+        sql(new ResultPredicateChecker((Object[] objs) -> {
+            int id = ((Integer) objs[0]);
+            int age = ((Integer) objs[2]);
+
+            if (id % 2 == 0) {
+                if (age != 20 + (id % 10) + 1) {
+                    return false;
+                }
+            } else {
+                if (age != 20 + (id % 10) + 5) {
+                    return false;
+                }
+            }
+
+            ++cnt[0];
+
+            return true;
+        }), "SELECT * FROM person_t where company = 'New Company'");
+
+        assertEquals(cnt[0], 34, "Invalid rows count");
+
+        sql(new UpdateChecker(0), "DROP TABLE city");
+        sql(new UpdateChecker(0), "DROP TABLE person_t");
+    }
+
+    /**
+     * Run sql statement with arguments and check results.
+     *
+     * @param checker Query result's checker.
+     * @param sql     SQL statement to execute.
+     * @param args    Arguments.
+     * @throws SQLException If failed.
+     */
+    protected void sql(SingleStatementChecker checker, String sql, Object... args) throws SQLException {
+        Statement stmt = null;
+
+        try {
+            if (args.length > 0) {
+                stmt = conn.prepareStatement(sql);
+
+                PreparedStatement pstmt = (PreparedStatement) stmt;
+
+                for (int i = 0; i < args.length; ++i) {
+                    pstmt.setObject(i + 1, args[i]);
+                }
+
+                pstmt.execute();
+            } else {
+                stmt = conn.createStatement();
+
+                stmt.execute(sql);
+            }
+
+            checkResults(stmt, checker);
+        } finally {
+            if (stmt != null) {
+                stmt.close();
+            }
+        }
+    }
+
+    /**
+     * Check query results with provided checker.
+     *
+     * @param stmt Statement.
+     * @param checker Checker.
+     * @throws SQLException If failed.
+     */
+    private void checkResults(Statement stmt, SingleStatementChecker checker) throws SQLException {
+        ResultSet rs = stmt.getResultSet();
+
+        if (rs != null) {
+            checker.check(rs);
+        } else {
+            int updCnt = stmt.getUpdateCount();
+
+            assert updCnt != -1 : "Invalid results. Result set is null and update count is -1";
+
+            checker.check(updCnt);
+        }
+    }
+
+    interface SingleStatementChecker {
+        /**
+         * Called when query produces results.
+         *
+         * @param rs Result set.
+         * @throws SQLException On error.
+         */
+        void check(ResultSet rs) throws SQLException;
+
+        /**
+         * Called when query produces any update.
+         *
+         * @param updateCount Update count.
+         */
+        void check(int updateCount);
+    }
+
+    static class UpdateChecker implements SingleStatementChecker {
+        /** Expected update count. */
+        private final int expUpdCnt;
+
+        /**
+         * Constructor.
+         *
+         * @param expUpdCnt Expected Update count.
+         */
+        UpdateChecker(int expUpdCnt) {
+            this.expUpdCnt = expUpdCnt;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(ResultSet rs) {
+            fail("Update results are expected. [rs=" + rs + ']');
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(int updateCount) {
+            assertEquals(expUpdCnt, updateCount);
+        }
+    }
+
+    static class ResultChecker implements SingleStatementChecker {
+        /** Expected update count. */
+        private final Set<Row> expRs = new HashSet<>();
+
+        /**
+         * Constructor.
+         *
+         * @param expRs Expected result set.
+         */
+        ResultChecker(Object[][] expRs) {
+            for (Object[] row : expRs) {
+                this.expRs.add(new Row(row));
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(ResultSet rs) throws SQLException {
+            int cols = rs.getMetaData().getColumnCount();
+
+            while (rs.next()) {
+                Object[] rowObjs = new Object[cols];
+
+                for (int i = 0; i < cols; ++i) {
+                    rowObjs[i] = rs.getObject(i + 1);
+                }
+
+                Row row = new Row(rowObjs);
+                var rmv = expRs.remove(row);
+
+                assert rmv : "Invalid row. [row=" + row + ", remainedRows="
+                    + printRemainedExpectedResult() + ']';
+            }
+
+            assert expRs.isEmpty() : "Expected results has rows that aren't contained at the result set. [remainedRows="
+                + printRemainedExpectedResult() + ']';
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(int updateCount) {
+            fail("Results set is expected. [updateCount=" + updateCount + ']');
+        }
+
+        private String printRemainedExpectedResult() {
+            StringBuilder sb = new StringBuilder();
+
+            for (Row r : expRs) {
+                sb.append('\n').append(r.toString());
+            }
+
+            return sb.toString();
+        }
+    }
+
+    static class ResultColumnChecker extends ResultChecker {
+        /** Expected column names. */
+        private final String[] expColLabels;
+
+        /**
+         * Checker column names for rmpty results.
+         *
+         * @param expColLabels Expected column names.
+         */
+        ResultColumnChecker(String... expColLabels) {
+            super(new Object[][]{});
+
+            this.expColLabels = expColLabels;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(ResultSet rs) throws SQLException {
+            ResultSetMetaData meta = rs.getMetaData();
+
+            int cols = meta.getColumnCount();
+
+            assertEquals(cols, expColLabels.length, "Invalid columns count: [expected=" + expColLabels.length
+                    + ", actual=" + cols + ']');
+
+            for (int i = 0; i < cols; ++i) {
+                assertTrue(expColLabels[i].equalsIgnoreCase(meta.getColumnLabel(i + 1)), expColLabels[i] + ":" + meta.getColumnName(i + 1));
+            }
+
+            super.check(rs);
+        }
+    }
+
+    static class ResultPredicateChecker implements SingleStatementChecker {
+        /** Row predicate. */
+        private final Function<Object[], Boolean> rowPredicate;
+
+        /**
+         * Constructor.
+         *
+         * @param rowPredicate Row predicate to check result set.
+         */
+        ResultPredicateChecker(Function<Object[], Boolean> rowPredicate) {
+            this.rowPredicate = rowPredicate;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(ResultSet rs) throws SQLException {
+            int cols = rs.getMetaData().getColumnCount();
+
+            while (rs.next()) {
+                Object[] rowObjs = new Object[cols];
+
+                for (int i = 0; i < cols; ++i) {
+                    rowObjs[i] = rs.getObject(i + 1);
+                }
+
+                assert rowPredicate.apply(rowObjs) : "Invalid row. [row=" + Arrays.toString(rowObjs) + ']';
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check(int updateCount) {
+            fail("Results set is expected. [updateCount=" + updateCount + ']');
+        }
+    }
+
+    private static class Row {
+        /** Row. */
+        private final Object[] row;
+
+        /**
+         * Conctructor.
+         *
+         * @param row Data row.
+         */
+        private Row(Object[] row) {
+            this.row = row;
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            Row row1 = (Row) o;
+
+            return Arrays.equals(row, row1.row);
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Arrays.hashCode(row);
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return Arrays.toString(row);
+        }
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexQuerySelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexQuerySelfTest.java
new file mode 100644
index 0000000..a36b6b8
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcComplexQuerySelfTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for complex queries (joins, etc.).
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcComplexQuerySelfTest extends AbstractJdbcSelfTest {
+    @BeforeAll
+    public static void createTable() throws Exception {
+        try (Statement s = conn.createStatement()) {
+            s.execute("DROP TABLE IF EXISTS public.person");
+            s.execute("CREATE TABLE public.person (id INTEGER PRIMARY KEY, orgid INTEGER, "
+                    + "name VARCHAR NOT NULL, age INTEGER NOT NULL)");
+
+            s.execute("DROP TABLE IF EXISTS public.org");
+            s.execute("CREATE TABLE public.org (id INTEGER PRIMARY KEY, name VARCHAR NOT NULL)");
+
+            s.executeUpdate("INSERT INTO public.person(orgid, id, name, age) VALUES "
+                    + "(1, 1, 'John White', 25), "
+                    + "(1, 2, 'Joe Black', 35), "
+                    + "(2, 3, 'Mike Green', 40)");
+            s.executeUpdate("INSERT INTO public.org(id, name) VALUES "
+                    + "(1, 'A'), "
+                    + "(2, 'B')");
+        }
+    }
+
+    /**
+     * Join test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testJoin() throws Exception {
+        ResultSet rs = stmt.executeQuery(
+                "select p.id, p.name, o.name as orgName from PUBLIC.Person p, PUBLIC.Org o where p.orgId = o.id");
+
+        assertNotNull(rs);
+
+        int cnt = 0;
+
+        while (rs.next()) {
+            int id = rs.getInt("id");
+
+            if (id == 1) {
+                assertEquals("John White", rs.getString("name"));
+                assertEquals("A", rs.getString("orgName"));
+            } else if (id == 2) {
+                assertEquals("Joe Black", rs.getString("name"));
+                assertEquals("A", rs.getString("orgName"));
+            } else if (id == 3) {
+                assertEquals("Mike Green", rs.getString("name"));
+                assertEquals("B", rs.getString("orgName"));
+            } else {
+                fail("Wrong ID: " + id);
+            }
+
+            cnt++;
+        }
+
+        assertEquals(3, cnt);
+    }
+
+    /**
+     * Join without alias test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testJoinWithoutAlias() throws Exception {
+        ResultSet rs = stmt.executeQuery(
+                "select p.id, p.name, o.name from PUBLIC.Person p, PUBLIC.Org o where p.orgId = o.id");
+
+        assertNotNull(rs);
+
+        int cnt = 0;
+
+        while (rs.next()) {
+            int id = rs.getInt(1);
+
+            if (id == 1) {
+                assertEquals("John White", rs.getString("name"));
+                assertEquals("John White", rs.getString(2));
+                assertEquals("A", rs.getString(3));
+            } else if (id == 2) {
+                assertEquals("Joe Black", rs.getString("name"));
+                assertEquals("Joe Black", rs.getString(2));
+                assertEquals("A", rs.getString(3));
+            } else if (id == 3) {
+                assertEquals("Mike Green", rs.getString("name"));
+                assertEquals("Mike Green", rs.getString(2));
+                assertEquals("B", rs.getString(3));
+            } else {
+                fail("Wrong ID: " + id);
+            }
+
+            cnt++;
+        }
+
+        assertEquals(3, cnt);
+    }
+
+    /**
+     * In function test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testIn() throws Exception {
+        ResultSet rs = stmt.executeQuery("select name from PUBLIC.Person where age in (25, 35)");
+
+        assertNotNull(rs);
+
+        int cnt = 0;
+
+        while (rs.next()) {
+            assertThat(rs.getString("name"), anyOf(is("John White"), is("Joe Black")));
+
+            cnt++;
+        }
+
+        assertEquals(2, cnt);
+    }
+
+    /**
+     * Between func test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testBetween() throws Exception {
+        ResultSet rs = stmt.executeQuery("select name from PUBLIC.Person where age between 24 and 36");
+
+        assertNotNull(rs);
+
+        int cnt = 0;
+
+        while (rs.next()) {
+            assertThat(rs.getString("name"), anyOf(is("John White"), is("Joe Black")));
+
+            cnt++;
+        }
+
+        assertEquals(2, cnt);
+    }
+
+    /**
+     * Calculated value test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCalculatedValue() throws Exception {
+        ResultSet rs = stmt.executeQuery("select age * 2 from PUBLIC.Person");
+
+        assertNotNull(rs);
+
+        int cnt = 0;
+
+        while (rs.next()) {
+            assertThat(rs.getInt(1), anyOf(is(50), is(70), is(80)));
+
+            cnt++;
+        }
+
+        assertEquals(3, cnt);
+    }
+
+    /**
+     * Wrong argument type test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testWrongArgumentType() throws Exception {
+        try (ResultSet rs = stmt.executeQuery("select * from PUBLIC.Org where name = '2'")) {
+            assertFalse(rs.next());
+        }
+
+        // Check non-indexed field.
+        assertThrows(SQLException.class, () -> {
+            try (ResultSet rs = stmt.executeQuery("select * from PUBLIC.Org where name = 2")) {
+                assertFalse(rs.next());
+            }
+        });
+
+        // Check indexed field.
+        try (ResultSet rs = stmt.executeQuery("select * from PUBLIC.Person where name = '2'")) {
+            assertFalse(rs.next());
+        }
+
+        assertThrows(SQLException.class, () -> {
+            try (ResultSet rs = stmt.executeQuery("select * from PUBLIC.Person where name = 2")) {
+                assertFalse(rs.next());
+            }
+        });
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcConnectionSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcConnectionSelfTest.java
index 4c2357f..0a8be3c 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcConnectionSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcConnectionSelfTest.java
@@ -62,6 +62,7 @@ import org.junit.jupiter.api.Test;
  * Connection test.
  */
 @SuppressWarnings("ThrowableNotThrown")
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
 public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     /**
      * Test JDBC loading via ServiceLoader.
@@ -85,7 +86,7 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     @SuppressWarnings({"EmptyTryBlock", "unused"})
     @Test
     public void testDefaults() throws Exception {
-        var url = "jdbc:ignite:thin://127.0.1.1:10800";
+        var url = "jdbc:ignite:thin://127.0.0.1:10800";
 
         try (Connection conn = DriverManager.getConnection(url)) {
             // No-op.
@@ -97,8 +98,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     @SuppressWarnings({"EmptyTryBlock", "unused"})
+    @Disabled
     @Test
-    @Disabled("ITDS-1887")
     public void testDefaultsIpv6() throws Exception {
         var url = "jdbc:ignite:thin://[::1]:10800";
 
@@ -224,12 +225,11 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO  IGNITE-15188.
+     * Test create statement.
      *
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testCreateStatement2() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             int[] rsTypes = new int[]{TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE};
@@ -266,12 +266,11 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO  IGNITE-15188.
+     * Test create statement.
      *
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testCreateStatement3() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             int[] rsTypes = new int[]{ TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE };
@@ -318,8 +317,9 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         try (Connection conn = DriverManager.getConnection(URL)) {
             // null query text
             assertThrows(
-                    NullPointerException.class,
-                    () -> conn.prepareStatement(null)
+                    SQLException.class,
+                    () -> conn.prepareStatement(null),
+                    "SQL string cannot be null"
             );
 
             final String sqlText = "select * from test where param = ?";
@@ -336,12 +336,11 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO  IGNITE-15188.
+     * Test prepare statement.
      *
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testPrepareStatement3() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             final String sqlText = "select * from test where param = ?";
@@ -385,12 +384,11 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO  IGNITE-15188.
+     * Test prepare statement.
      *
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testPrepareStatement4() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             final String sqlText = "select * from test where param = ?";
@@ -515,13 +513,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO Enable when transactions are ready.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testGetSetAutoCommit() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             boolean ac0 = conn.getAutoCommit();
@@ -582,13 +575,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * Enable when transactions are ready.
-     *
-     * @throws Exception if failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testBeginFailsWhenMvccIsDisabled() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             conn.createStatement().execute("BEGIN");
@@ -599,13 +587,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * Enable when transactions are ready.
-     *
-     * @throws Exception if failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testCommitIgnoredWhenMvccIsDisabled() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             conn.setAutoCommit(false);
@@ -616,13 +599,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         // assert no exception
     }
 
-    /**
-     * Enable when transactions are ready.
-     *
-     * @throws Exception if failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testRollbackIgnoredWhenMvccIsDisabled() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             conn.setAutoCommit(false);
@@ -635,12 +613,11 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO  IGNITE-15188.
+     * Test get metadata.
      *
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testGetMetaData() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             DatabaseMetaData meta = conn.getMetaData();
@@ -667,13 +644,7 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO  IGNITE-15188.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
     public void testGetSetCatalog() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             assertFalse(conn.getMetaData().supportsCatalogsInDataManipulation());
@@ -788,7 +759,6 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testGetSetHoldability() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             // default value
@@ -824,13 +794,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO  IGNITE-15188.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testSetSavepoint() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             assertFalse(conn.getMetaData().supportsSavepoints());
@@ -848,13 +813,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO  IGNITE-15188.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testSetSavepointName() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             assertFalse(conn.getMetaData().supportsSavepoints());
@@ -881,13 +841,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO  IGNITE-15188.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testRollbackSavePoint() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             assertFalse(conn.getMetaData().supportsSavepoints());
@@ -914,13 +869,8 @@ public class ItJdbcConnectionSelfTest extends AbstractJdbcSelfTest {
         }
     }
 
-    /**
-     * TODO  IGNITE-15188.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15087")
     public void testReleaseSavepoint() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL)) {
             assertFalse(conn.getMetaData().supportsSavepoints());
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcDeleteStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcDeleteStatementSelfTest.java
new file mode 100644
index 0000000..786ab71
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcDeleteStatementSelfTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.SQLException;
+import org.apache.ignite.table.KeyValueView;
+import org.apache.ignite.table.Tuple;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Delete functional statement self test.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcDeleteStatementSelfTest extends ItJdbcAbstractStatementSelfTest {
+    /**
+     * Execute delete query test.
+     *
+     * @throws SQLException If failed.
+     */
+    @Test
+    public void testExecute() throws SQLException {
+        stmt.execute("delete from PUBLIC.PERSON where substring(SID, 2, 1)::int % 2 = 0");
+
+        KeyValueView<Tuple, Tuple> kvView = clusterNodes.get(0).tables().table("PUBLIC.PERSON").keyValueView();
+
+        assertFalse(kvView.contains(null, Tuple.create().set("ID", 2)));
+        assertTrue(kvView.contains(null, Tuple.create().set("ID", 1)));
+        assertTrue(kvView.contains(null, Tuple.create().set("ID", 3)));
+    }
+
+    /**
+     * Execute delete update test.
+     *
+     * @throws SQLException If failed.
+     */
+    @Test
+    public void testExecuteUpdate() throws SQLException {
+        int res = stmt.executeUpdate("delete from PUBLIC.PERSON where substring(SID, 2, 1)::int % 2 = 0");
+
+        assertEquals(1, res);
+
+        KeyValueView<Tuple, Tuple> kvView = clusterNodes.get(0).tables().table("PUBLIC.PERSON").keyValueView();
+
+        assertFalse(kvView.contains(null, Tuple.create().set("ID", 2)));
+        assertTrue(kvView.contains(null, Tuple.create().set("ID", 1)));
+        assertTrue(kvView.contains(null, Tuple.create().set("ID", 3)));
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsAbstractSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsAbstractSelfTest.java
new file mode 100644
index 0000000..55aff1e
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsAbstractSelfTest.java
@@ -0,0 +1,632 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.apache.ignite.client.proto.query.SqlStateCode.CONNECTION_CLOSED;
+import static org.apache.ignite.client.proto.query.SqlStateCode.CONVERSION_FAILED;
+import static org.apache.ignite.client.proto.query.SqlStateCode.INVALID_CURSOR_STATE;
+import static org.apache.ignite.client.proto.query.SqlStateCode.NULL_VALUE;
+import static org.apache.ignite.client.proto.query.SqlStateCode.PARSING_EXCEPTION;
+import static org.apache.ignite.client.proto.query.SqlStateCode.UNSUPPORTED_OPERATION;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.net.URL;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.List;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test SQLSTATE codes propagation with (any) Ignite JDBC driver.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public abstract class ItJdbcErrorsAbstractSelfTest extends AbstractJdbcSelfTest {
+    /**
+     * Test that parsing-related error codes get propagated to Ignite SQL exceptions.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testParsingErrors() {
+        checkErrorState("gibberish", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"GIBBERISH[*] \"");
+    }
+
+    /**
+     * Test that error codes from tables related DDL operations get propagated to Ignite SQL
+     * exceptions.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testTableErrors() {
+        checkErrorState("DROP TABLE \"PUBLIC\".missing", PARSING_EXCEPTION, "Table doesn't exist: MISSING");
+    }
+
+    /**
+     * Test that error codes from indexes related DDL operations get propagated to Ignite SQL
+     * exceptions.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testIndexErrors() {
+        checkErrorState("DROP INDEX \"PUBLIC\".missing", PARSING_EXCEPTION, "Index doesn't exist: MISSING");
+    }
+
+    /**
+     * Test that error codes from DML operations get propagated to Ignite SQL exceptions.
+     *
+     * @throws SQLException if failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDmlErrors() throws SQLException {
+        stmt.execute("CREATE TABLE INTEGER(KEY INT PRIMARY KEY, VAL INT)");
+
+        try {
+            checkErrorState("INSERT INTO INTEGER(key, val) values(1, null)", NULL_VALUE,
+                    "Value for INSERT, COPY, MERGE, or UPDATE must not be null");
+
+            checkErrorState("INSERT INTO INTEGER(key, val) values(1, 'zzz')", CONVERSION_FAILED,
+                    "Value conversion failed [column=_VAL, from=java.lang.String, to=java.lang.Integer]");
+        } finally {
+            stmt.execute("DROP TABLE INTEGER;");
+        }
+    }
+
+    /**
+     * Test error code for the case when user attempts to refer a future currently unsupported.
+     *
+     * @throws SQLException if failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testUnsupportedSql() throws SQLException {
+        stmt.execute("CREATE TABLE INTEGER(KEY INT PRIMARY KEY, VAL INT)");
+
+        try {
+            checkErrorState("ALTER TABLE INTEGER MODIFY COLUMN KEY CHAR", UNSUPPORTED_OPERATION,
+                    "ALTER COLUMN is not supported");
+        } finally {
+            stmt.execute("DROP TABLE INTEGER;");
+        }
+    }
+
+    /**
+     * Test error code for the case when user attempts to use a closed connection.
+     *
+     * @throws SQLException if failed.
+     */
+    @Test
+    public void testConnectionClosed() throws SQLException {
+        Connection conn = getConnection();
+        DatabaseMetaData meta = conn.getMetaData();
+
+        conn.close();
+
+        checkErrorState(() -> conn.prepareStatement("SELECT 1"), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> conn.createStatement(), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> conn.getMetaData(), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> meta.getIndexInfo(null, null, null, false, false), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> meta.getColumns(null, null, null, null), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> meta.getPrimaryKeys(null, null, null), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> meta.getSchemas(null, null), CONNECTION_CLOSED, "Connection is closed.");
+        checkErrorState(() -> meta.getTables(null, null, null, null), CONNECTION_CLOSED, "Connection is closed.");
+    }
+
+    /**
+     * Test error code for the case when user attempts to use a closed result set.
+     */
+    @Test
+    public void testResultSetClosed() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 1")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.close();
+
+                rs.getInt(1);
+            }
+        }, INVALID_CURSOR_STATE, "Result set is closed");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code int} value from column whose
+     * value can't be converted to an {@code int}.
+     */
+    @Test
+    public void testInvalidIntFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getInt(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to int");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code long} value from column whose
+     * value can't be converted to an {@code long}.
+     */
+    @Test
+    public void testInvalidLongFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getLong(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to long");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code float} value from column whose
+     * value can't be converted to an {@code float}.
+     */
+    @Test
+    public void testInvalidFloatFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getFloat(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to float");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code double} value from column whose
+     * value can't be converted to an {@code double}.
+     */
+    @Test
+    public void testInvalidDoubleFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getDouble(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to double");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code byte} value from column whose
+     * value can't be converted to an {@code byte}.
+     */
+    @Test
+    public void testInvalidByteFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getByte(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to byte");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code short} value from column whose
+     * value can't be converted to an {@code short}.
+     */
+    @Test
+    public void testInvalidShortFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getShort(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to short");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code BigDecimal} value from column
+     * whose value can't be converted to an {@code BigDecimal}.
+     */
+    @Test
+    public void testInvalidBigDecimalFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getBigDecimal(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to BigDecimal");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code boolean} value from column
+     * whose value can't be converted to an {@code boolean}.
+     */
+    @Test
+    public void testInvalidBooleanFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getBoolean(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to boolean");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@code boolean} value from column
+     * whose value can't be converted to an {@code boolean}.
+     */
+    @Test
+    public void testInvalidObjectFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getObject(1, List.class);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to java.util.List");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@link Date} value from column whose
+     * value can't be converted to a {@link Date}.
+     */
+    @Test
+    public void testInvalidDateFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getDate(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to date");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@link Time} value from column whose
+     * value can't be converted to a {@link Time}.
+     */
+    @Test
+    public void testInvalidTimeFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getTime(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to time");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@link Timestamp} value from column
+     * whose value can't be converted to a {@link Timestamp}.
+     */
+    @Test
+    public void testInvalidTimestampFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getTimestamp(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to timestamp");
+    }
+
+    /**
+     * Test error code for the case when user attempts to get {@link URL} value from column whose
+     * value can't be converted to a {@link URL}.
+     */
+    @Test
+    public void testInvalidUrlFormat() {
+        checkErrorState(() -> {
+            try (PreparedStatement stmt = conn.prepareStatement("SELECT 'zzz'")) {
+                ResultSet rs = stmt.executeQuery();
+
+                rs.next();
+
+                rs.getURL(1);
+            }
+        }, CONVERSION_FAILED, "Cannot convert to URL");
+    }
+
+    /**
+     * Check error code for the case null value is inserted into table field declared as NOT NULL.
+     *
+     * @throws SQLException if failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testNotNullViolation() throws SQLException {
+        stmt.execute("CREATE TABLE public.nulltest(id INT PRIMARY KEY, name CHAR NOT NULL)");
+
+        try {
+            checkErrorState(() -> stmt.execute("INSERT INTO public.nulltest(id, name) VALUES (1, NULLIF('a', 'a'))"),
+                    NULL_VALUE, "Null value is not allowed for column 'NAME'");
+        } finally {
+            stmt.execute("DROP TABLE public.nulltest");
+        }
+    }
+
+    /**
+     * Checks wrong table name select error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testSelectWrongTable() {
+        checkSqlErrorMessage("select from wrong", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+    }
+
+    /**
+     * Checks wrong column name select error message.
+     *
+     * @throws SQLException If failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testSelectWrongColumnName() throws SQLException {
+        stmt.execute("CREATE TABLE public.test(id INT PRIMARY KEY, name CHAR NOT NULL)");
+
+        try {
+            checkSqlErrorMessage("select wrong from public.test", PARSING_EXCEPTION,
+                    "Failed to parse query. Column \"WRONG\" not found");
+        } finally {
+            stmt.execute("DROP TABLE public.test");
+        }
+    }
+
+    /**
+     * Checks wrong syntax select error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testSelectWrongSyntax() {
+        checkSqlErrorMessage("select from test where", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"SELECT FROM TEST WHERE[*]");
+    }
+
+    /**
+     * Checks wrong table name DML error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDmlWrongTable() {
+        checkSqlErrorMessage("insert into wrong (id, val) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+
+        checkSqlErrorMessage("merge into wrong (id, val) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+
+        checkSqlErrorMessage("update wrong set val = 'val3' where id = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+
+        checkSqlErrorMessage("delete from wrong where id = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+    }
+
+    /**
+     * Checks wrong column name DML error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDmlWrongColumnName() {
+        checkSqlErrorMessage("insert into test (id, wrong) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Column \"WRONG\" not found");
+
+        checkSqlErrorMessage("merge into test (id, wrong) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Column \"WRONG\" not found");
+
+        checkSqlErrorMessage("update test set wrong = 'val3' where id = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Column \"WRONG\" not found");
+
+        checkSqlErrorMessage("delete from test where wrong = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Column \"WRONG\" not found");
+    }
+
+    /**
+     * Checks wrong syntax DML error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDmlWrongSyntax() {
+        checkSqlErrorMessage("insert test (id, val) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"INSERT TEST[*] (ID, VAL)");
+
+        checkSqlErrorMessage("merge test (id, val) values (3, 'val3')", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"MERGE TEST[*] (ID, VAL)");
+
+        checkSqlErrorMessage("update test val = 'val3' where id = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"UPDATE TEST VAL =[*] 'val3' WHERE ID = 2");
+
+        checkSqlErrorMessage("delete from test 1where id = 2", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"DELETE FROM TEST 1[*]WHERE ID = 2 ");
+    }
+
+    /**
+     * Checks wrong table name DDL error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDdlWrongTable() {
+        checkSqlErrorMessage("create table test (id int primary key, val varchar)", PARSING_EXCEPTION,
+                "Table already exists: TEST");
+
+        checkSqlErrorMessage("drop table wrong", PARSING_EXCEPTION,
+                "Table doesn't exist: WRONG");
+
+        checkSqlErrorMessage("create index idx1 on wrong (val)", PARSING_EXCEPTION,
+                "Table doesn't exist: WRONG");
+
+        checkSqlErrorMessage("drop index wrong", PARSING_EXCEPTION,
+                "Index doesn't exist: WRONG");
+
+        checkSqlErrorMessage("alter table wrong drop column val", PARSING_EXCEPTION,
+                "Failed to parse query. Table \"WRONG\" not found");
+    }
+
+    /**
+     * Checks wrong column name DDL error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDdlWrongColumnName() {
+        checkSqlErrorMessage("alter table test drop column wrong", PARSING_EXCEPTION,
+                "Failed to parse query. Column \"WRONG\" not found");
+
+        checkSqlErrorMessage("create index idx1 on test (wrong)", PARSING_EXCEPTION,
+                "Column doesn't exist: WRONG");
+
+        checkSqlErrorMessage("create table test(id integer primary key, AgE integer, AGe integer)",
+                PARSING_EXCEPTION,
+                "Duplicate column name: AGE");
+
+        checkSqlErrorMessage(
+                "create table test(\"id\" integer primary key, \"age\" integer, \"age\" integer)",
+                PARSING_EXCEPTION,
+                "Duplicate column name: age");
+
+        checkSqlErrorMessage("create table test(id integer primary key, age integer, age varchar)",
+                PARSING_EXCEPTION,
+                "Duplicate column name: AGE");
+    }
+
+    /**
+     * Checks wrong syntax DDL error message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testDdlWrongSyntax() {
+        checkSqlErrorMessage("create table test2 (id int wrong key, val varchar)", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"CREATE TABLE TEST2 (ID INT WRONG[*]");
+
+        checkSqlErrorMessage("drop table test on", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"DROP TABLE TEST ON[*]");
+
+        checkSqlErrorMessage("create index idx1 test (val)", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"CREATE INDEX IDX1 TEST[*]");
+
+        checkSqlErrorMessage("drop index", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"DROP INDEX [*]");
+
+        checkSqlErrorMessage("alter table test drop column", PARSING_EXCEPTION,
+                "Failed to parse query. Syntax error in SQL statement \"ALTER TABLE TEST DROP COLUMN [*]");
+    }
+
+    /**
+     * Gets the connection.
+     *
+     * @return Connection to execute statements on.
+     * @throws SQLException if failed.
+     */
+    protected abstract Connection getConnection() throws SQLException;
+
+    /**
+     * Test that running given SQL statement yields expected SQLSTATE code.
+     *
+     * @param sql      Statement.
+     * @param expState Expected SQLSTATE code.
+     * @param expMsg   Expected message.
+     */
+    private void checkErrorState(final String sql, String expState, String expMsg) {
+        checkErrorState(() -> {
+            try (final PreparedStatement stmt = conn.prepareStatement(sql)) {
+                stmt.execute();
+            }
+        }, expState, expMsg);
+    }
+
+    /**
+     * Test that running given closure yields expected SQLSTATE code.
+     *
+     * @param clo      Closure.
+     * @param expState Expected SQLSTATE code.
+     * @param expMsg   Expected message.
+     */
+    protected void checkErrorState(final RunnableX clo, String expState, String expMsg) {
+        SQLException ex = assertThrows(SQLException.class, clo::run, expMsg);
+        assertThat(ex.getMessage(), containsString(expMsg));
+
+        assertEquals(expState, ex.getSQLState(), ex.getMessage());
+    }
+
+    /**
+     * Check SQL exception message and error code.
+     *
+     * @param sql      Query string.
+     * @param expState Error code.
+     * @param expMsg   Error message.
+     */
+    private void checkSqlErrorMessage(final String sql, String expState, String expMsg) {
+        checkErrorState(() -> {
+            stmt.executeUpdate("DROP TABLE IF EXISTS wrong");
+            stmt.executeUpdate("DROP TABLE IF EXISTS test");
+
+            stmt.executeUpdate("CREATE TABLE test (id INT PRIMARY KEY, val VARCHAR)");
+
+            stmt.executeUpdate("INSERT INTO test (id, val) VALUES (1, 'val1')");
+            stmt.executeUpdate("INSERT INTO test (id, val) VALUES (2, 'val2')");
+
+            stmt.execute(sql);
+
+            fail("Exception is expected");
+        }, expState, expMsg);
+    }
+
+    /**
+     * A runnable that can throw an SQLException.
+     */
+    public interface RunnableX {
+        /**
+         * Runs this runnable.
+         */
+        void run() throws SQLException;
+    }
+}
\ No newline at end of file
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsSelfTest.java
new file mode 100644
index 0000000..9c18827
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcErrorsSelfTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.apache.ignite.client.proto.query.SqlStateCode.CLIENT_CONNECTION_FAILED;
+import static org.apache.ignite.client.proto.query.SqlStateCode.INVALID_TRANSACTION_LEVEL;
+import static org.apache.ignite.client.proto.query.SqlStateCode.PARSING_EXCEPTION;
+import static org.apache.ignite.client.proto.query.SqlStateCode.UNSUPPORTED_OPERATION;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.sql.BatchUpdateException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test SQLSTATE codes propagation with thin client driver.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcErrorsSelfTest extends ItJdbcErrorsAbstractSelfTest {
+    /** {@inheritDoc} */
+    @Override protected Connection getConnection() throws SQLException {
+        return DriverManager.getConnection(URL);
+    }
+
+    /**
+     * Test error code for the case when connection string is fine but client can't reach server
+     * due to <b>communication problems</b> (not due to clear misconfiguration).
+     */
+    @Test
+    public void testConnectionError() {
+        checkErrorState(() -> DriverManager.getConnection("jdbc:ignite:thin://unknown.host?connectionTimeout=1000"),
+                CLIENT_CONNECTION_FAILED, "Failed to connect to server");
+    }
+
+    /**
+     * Test error code for the case when connection string is a mess.
+     */
+    @Test
+    public void testInvalidConnectionStringFormat() {
+        checkErrorState(() -> DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:1000000"),
+                CLIENT_CONNECTION_FAILED, "port range contains invalid port 1000000");
+    }
+
+    /**
+     * Test error code for the case when user attempts to set an invalid isolation level to a connection.
+     */
+    @SuppressWarnings("MagicConstant")
+    @Test
+    public void testInvalidIsolationLevel() {
+        checkErrorState(() -> conn.setTransactionIsolation(1000),
+                INVALID_TRANSACTION_LEVEL, "Invalid transaction isolation level.");
+    }
+
+    /**
+     * Test error code for the case when error is caused on batch execution.
+     *
+     * @throws SQLException if failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16201")
+    public void testBatchUpdateException() throws SQLException {
+        try {
+            stmt.executeUpdate("CREATE TABLE test2 (id int primary key, val varchar)");
+
+            stmt.addBatch("insert into test2 (id, val) values (1, 'val1')");
+            stmt.addBatch("insert into test2 (id, val) values (2, 'val2')");
+            stmt.addBatch("insert into test2 (id1, val1) values (3, 'val3')");
+
+            stmt.executeBatch();
+
+            fail("BatchUpdateException is expected");
+        } catch (BatchUpdateException e) {
+            assertEquals(3, e.getUpdateCounts().length);
+
+            assertArrayEquals(new int[] {1, 1, Statement.EXECUTE_FAILED}, e.getUpdateCounts());
+
+            assertEquals(PARSING_EXCEPTION, e.getSQLState());
+
+            assertTrue(e.getMessage() != null
+                            && e.getMessage().contains("Failed to parse query. Column \"ID1\" not found"),
+                    "Unexpected error message: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Check that unsupported explain of update operation causes Exception on the driver side with correct code and
+     * message.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15247")
+    public void testExplainUpdatesUnsupported() {
+        checkErrorState(() -> {
+            stmt.executeUpdate("CREATE TABLE TEST_EXPLAIN (ID INT PRIMARY KEY, VAL INT)");
+
+            stmt.executeUpdate("EXPLAIN INSERT INTO TEST_EXPLAIN VALUES (1, 2)");
+        }, UNSUPPORTED_OPERATION, "Explains of update queries are not supported.");
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcInsertStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcInsertStatementSelfTest.java
new file mode 100644
index 0000000..0dc6098
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcInsertStatementSelfTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Statement test.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcInsertStatementSelfTest extends ItJdbcAbstractStatementSelfTest {
+    /** SQL SELECT query for verification. */
+    private static final String SQL_SELECT = "select sid, id, firstName, lastName, age from PUBLIC.PERSON";
+
+    /** SQL query. */
+    private static final String SQL = "insert into PUBLIC.PERSON(sid, id, firstName, lastName, age) values "
+            + "('p1', 1, 'John', 'White', 25), "
+            + "('p2', 2, 'Joe', 'Black', 35), "
+            + "('p3', 3, 'Mike', 'Green', 40)";
+
+    /** SQL query. */
+    private static final String SQL_PREPARED = "insert into PUBLIC.PERSON(sid, id, firstName, lastName, age) values "
+            + "(?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)";
+
+    /** Arguments for prepared statement. */
+    private final Object[][] args = new Object[][] {
+            {"p1", 1, "John", "White", 25},
+            {"p3", 3, "Mike", "Green", 40},
+            {"p2", 2, "Joe", "Black", 35}
+    };
+
+    /** SQL query to populate cache. */
+    private static final String DROP_SQL = "DELETE FROM PUBLIC.PERSON;";
+
+    /** Prepared statement. */
+    private PreparedStatement prepStmt;
+
+    @BeforeEach
+    @Override
+    public void refillTable() throws Exception {
+        stmt.execute(DROP_SQL);
+
+        prepStmt = conn.prepareStatement(SQL_PREPARED);
+
+        assertNotNull(prepStmt);
+        assertFalse(prepStmt.isClosed());
+
+        int paramCnt = 1;
+
+        for (Object[] arg : args) {
+            prepStmt.setString(paramCnt++, (String) arg[0]);
+            prepStmt.setInt(paramCnt++, (Integer) arg[1]);
+            prepStmt.setString(paramCnt++, (String) arg[2]);
+            prepStmt.setString(paramCnt++, (String) arg[3]);
+            prepStmt.setInt(paramCnt++, (Integer) arg[4]);
+        }
+    }
+
+    @AfterEach
+    @Override public void afterTest() throws Exception {
+        super.afterTest();
+
+        if (prepStmt != null && !prepStmt.isClosed()) {
+            prepStmt.close();
+
+            assertTrue(prepStmt.isClosed());
+        }
+    }
+
+    private void doCheck() throws Exception {
+        assertTrue(stmt.execute(SQL_SELECT));
+
+        ResultSet rs = stmt.getResultSet();
+
+        assertNotNull(rs);
+
+        while (rs.next()) {
+            int id = rs.getInt("id");
+
+            switch (id) {
+                case 1:
+                    assertEquals("p1", rs.getString("sid"));
+                    assertEquals("John", rs.getString("firstName"));
+                    assertEquals("White", rs.getString("lastName"));
+                    assertEquals(25, rs.getInt("age"));
+                    break;
+
+                case 2:
+                    assertEquals("p2", rs.getString("sid"));
+                    assertEquals("Joe", rs.getString("firstName"));
+                    assertEquals("Black", rs.getString("lastName"));
+                    assertEquals(35, rs.getInt("age"));
+                    break;
+
+                case 3:
+                    assertEquals("p3", rs.getString("sid"));
+                    assertEquals("Mike", rs.getString("firstName"));
+                    assertEquals("Green", rs.getString("lastName"));
+                    assertEquals(40, rs.getInt("age"));
+                    break;
+
+                case 4:
+                    assertEquals("p4", rs.getString("sid"));
+                    assertEquals("Leah", rs.getString("firstName"));
+                    assertEquals("Grey", rs.getString("lastName"));
+                    assertEquals(22, rs.getInt("age"));
+                    break;
+
+                default:
+                    fail("Invalid ID: " + id);
+            }
+        }
+    }
+
+    /**
+     * Execute update test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testExecuteUpdate() throws Exception {
+        assertEquals(3, stmt.executeUpdate(SQL));
+
+        doCheck();
+    }
+
+    /**
+     * Prepared update execute test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPreparedExecuteUpdate() throws Exception {
+        assertEquals(3, prepStmt.executeUpdate());
+
+        doCheck();
+    }
+
+    /**
+     * Test execute.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testExecute() throws Exception {
+        assertFalse(stmt.execute(SQL));
+
+        doCheck();
+    }
+
+    /**
+     * Test prepared execute.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPreparedExecute() throws Exception {
+        assertFalse(prepStmt.execute());
+
+        doCheck();
+    }
+
+    /**
+     * Test duplicated keys.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testDuplicateKeys() throws Exception {
+        String sql = "insert into PUBLIC.PERSON(sid, id, firstName, lastName, age) values('p1', 1, 'John', 'White', 25)";
+
+        assertFalse(stmt.execute(sql));
+
+        assertThrows(SQLException.class, () -> stmt.execute(SQL), "Failed to INSERT some keys because they are already in cache.");
+
+        stmt.execute("select count(*) from PUBLIC.PERSON;");
+
+        ResultSet resultSet = stmt.getResultSet();
+
+        assertTrue(resultSet.next());
+
+        assertEquals(3, resultSet.getInt(1));
+
+        doCheck();
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcJoinsSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcJoinsSelfTest.java
new file mode 100644
index 0000000..7416362
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcJoinsSelfTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for complex queries with joins.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcJoinsSelfTest extends AbstractJdbcSelfTest {
+    /**
+     * Check distributed OUTER join of 3 tables (T1 -> T2 -> T3) returns correct result for non-collocated data.
+     *
+     * <ul>
+     *     <li>Create tables.</li>
+     *     <li>Put data into tables.</li>
+     *     <li>Check query with distributedJoin=true returns correct results.</li>
+     * </ul>
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16219")
+    public void testJoin() throws Exception {
+        stmt.executeUpdate("CREATE TABLE PUBLIC.PERSON"
+                + " (ID INT, NAME VARCHAR(64), AGE INT, CITY_ID DOUBLE, PRIMARY KEY (NAME));");
+        stmt.executeUpdate("CREATE TABLE PUBLIC.MEDICAL_INFO"
+                + " (ID INT, NAME VARCHAR(64), AGE INT, BLOOD_GROUP VARCHAR(64), PRIMARY KEY (ID));");
+        stmt.executeUpdate("CREATE TABLE PUBLIC.BLOOD_GROUP_INFO_PJ"
+                + " (ID INT, BLOOD_GROUP VARCHAR(64), UNIVERSAL_DONOR VARCHAR(64), PRIMARY KEY (ID));");
+        stmt.executeUpdate("CREATE TABLE PUBLIC.BLOOD_GROUP_INFO_P"
+                + " (ID INT, BLOOD_GROUP VARCHAR(64), UNIVERSAL_DONOR VARCHAR(64), PRIMARY KEY (BLOOD_GROUP));");
+
+        populateData();
+
+        checkQueries();
+    }
+
+    /**
+     * Start queries and check query results.
+     *
+     */
+    private void checkQueries() throws SQLException {
+        String res1;
+        String res2;
+
+        // Join on non-primary key.
+        try (final ResultSet resultSet = stmt.executeQuery("SELECT person.id, person.name,"
+                    + " medical_info.blood_group, blood_group_info_PJ.universal_donor FROM person "
+                    + "LEFT JOIN medical_info ON medical_info.name = person.name "
+                    + "LEFT JOIN blood_group_info_PJ ON blood_group_info_PJ.blood_group "
+                    + "= medical_info.blood_group;")) {
+
+            res1 = queryResultAsString(resultSet);
+        }
+
+        // Join on primary key.
+        try (final ResultSet resultSet = stmt.executeQuery("SELECT person.id, person.name,"
+                + " medical_info.blood_group, blood_group_info_P.universal_donor FROM person "
+                + "LEFT JOIN medical_info ON medical_info.name = person.name "
+                + "LEFT JOIN blood_group_info_P ON blood_group_info_P.blood_group "
+                + "= medical_info.blood_group;")) {
+
+            res2 = queryResultAsString(resultSet);
+        }
+
+        log.info("Query1 result: \n" + res1);
+        log.info("Query2 result: \n" + res2);
+
+        String expOut = "2001,Shravya,null,null\n"
+                + "2002,Kiran,O+,O+A+B+AB+\n"
+                + "2003,Harika,AB+,AB+\n"
+                + "2004,Srinivas,null,null\n"
+                + "2005,Madhavi,A+,A+AB+\n"
+                + "2006,Deeps,null,null\n"
+                + "2007,Hope,null,null\n";
+
+        assertEquals(expOut, res1, "Wrong result");
+        assertEquals(expOut, res2, "Wrong result");
+    }
+
+    /**
+     * Convert query result to string.
+     *
+     * @param res Query result set.
+     * @return String representation.
+     */
+    private String queryResultAsString(ResultSet res) throws SQLException {
+        List<String> results = new ArrayList<>();
+
+        while (res.next()) {
+            String row = String.valueOf(res.getLong(1))
+                    + ',' + res.getString(2)
+                    + ',' + res.getString(3)
+                    + ',' + res.getString(4);
+
+            results.add(row);
+        }
+
+        results.sort(String::compareTo);
+
+        StringBuilder sb = new StringBuilder();
+
+        for (String result : results) {
+            sb.append(result).append('\n');
+        }
+
+        return sb.toString();
+    }
+
+    private void populateData() throws SQLException {
+        stmt.executeUpdate(" INSERT INTO PUBLIC.PERSON (ID,NAME,AGE,CITY_ID) VALUES "
+                + "(2001,'Shravya',25,1.1), "
+                + "(2002,'Kiran',26,1.1), "
+                + "(2003,'Harika',26,2.4), "
+                + "(2004,'Srinivas',24,3.2), "
+                + "(2005,'Madhavi',23,3.2), "
+                + "(2006,'Deeps',28,1.2), "
+                + "(2007,'Hope',27,1.2);");
+
+        stmt.executeUpdate("INSERT INTO PUBLIC.MEDICAL_INFO (id,name,age,blood_group) VALUES "
+                + "(2001,'Madhavi',23,'A+'), "
+                + "(2002,'Diggi',27,'B+'), "
+                + "(2003,'Kiran',26,'O+'), "
+                + "(2004,'Harika',26,'AB+');");
+
+        stmt.executeUpdate("INSERT INTO PUBLIC.BLOOD_GROUP_INFO_PJ (id,blood_group,universal_donor) VALUES "
+                + "(2001,'A+','A+AB+'), "
+                + "(2002,'O+','O+A+B+AB+'), "
+                + "(2003,'B+','B+AB+'), "
+                + "(2004,'AB+','AB+'), "
+                + "(2005,'O-','EveryOne');");
+
+        stmt.executeUpdate("INSERT INTO PUBLIC.BLOOD_GROUP_INFO_P (id,blood_group,universal_donor) VALUES "
+                + "(2001,'A+','A+AB+'), "
+                + "(2002,'O+','O+A+B+AB+'), "
+                + "(2003,'B+','B+AB+'), "
+                + "(2004,'AB+','AB+'), "
+                + "(2005,'O-','EveryOne');");
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataPrimaryKeysSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataPrimaryKeysSelfTest.java
new file mode 100644
index 0000000..6ee86c7
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataPrimaryKeysSelfTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that primary keys in the metadata are valid.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcMetadataPrimaryKeysSelfTest extends AbstractJdbcSelfTest {
+    /** COLUMN_NAME column index in the metadata table. */
+    private static final int COL_NAME_IDX = 4;
+
+    /** {@inheritDoc} */
+    @AfterEach
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        executeUpdate("DROP TABLE IF EXISTS PUBLIC.TEST;");
+    }
+
+    /**
+     * Checks for PK that contains single field.
+     */
+    @Test
+    public void testSingleKey() throws Exception {
+        executeUpdate("CREATE TABLE PUBLIC.TEST (ID INT PRIMARY KEY, NAME VARCHAR);");
+
+        checkPkFields("TEST", "ID");
+    }
+
+    /**
+     * Checks for composite (so implicitly wrapped) primary key.
+     */
+    @Test
+    public void testCompositeKey() throws Exception {
+        executeUpdate("CREATE TABLE PUBLIC.TEST ("
+                + "ID INT, "
+                + "SEC_ID INT, "
+                + "NAME VARCHAR, "
+                + "PRIMARY KEY (ID, SEC_ID));");
+
+        checkPkFields("TEST", "ID", "SEC_ID");
+    }
+
+    /**
+     * Execute update sql operation using new connection.
+     *
+     * @param sql update SQL query.
+     * @return update count.
+     * @throws SQLException on error.
+     */
+    private int executeUpdate(String sql) throws SQLException {
+        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+            return stmt.executeUpdate();
+        }
+    }
+
+    /**
+     * Checks that field names in the metadata matches specified expected fields.
+     *
+     * @param tabName part of the sql query after CREATE TABLE TESTER.
+     * @param expPkFields Expected primary key fields.
+     */
+    private void checkPkFields(String tabName, String... expPkFields) throws Exception {
+        DatabaseMetaData md = conn.getMetaData();
+
+        ResultSet rs = md.getPrimaryKeys(conn.getCatalog(), null, tabName);
+
+        List<String> colNames = new ArrayList<>();
+
+        while (rs.next()) {
+            colNames.add(rs.getString(COL_NAME_IDX));
+        }
+
+        assertEquals(Arrays.asList(expPkFields), colNames, "Field names in the primary key are not correct");
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataSelfTest.java
index c4396a0..b0691e3 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMetadataSelfTest.java
@@ -42,6 +42,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
+import org.apache.ignite.Ignite;
 import org.apache.ignite.internal.client.proto.ProtocolVersion;
 import org.apache.ignite.internal.schema.configuration.SchemaConfigurationConverter;
 import org.apache.ignite.schema.SchemaBuilders;
@@ -56,10 +57,8 @@ import org.junit.jupiter.api.Test;
 /**
  * Metadata tests.
  */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
 public class ItJdbcMetadataSelfTest extends AbstractJdbcSelfTest {
-    /** URL. */
-    protected static final String URL = "jdbc:ignite:thin://127.0.1.1:10800";
-
     /** Creates tables. */
     @BeforeAll
     public static void createTables() {
@@ -127,13 +126,12 @@ public class ItJdbcMetadataSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-15507")
     public void testDecimalAndDateTypeMetaData() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            Statement stmt = conn.createStatement();
+        createMetaTable();
 
-            ResultSet rs = stmt.executeQuery(
-                    "select t.decimal, t.date from \"metaTest\".MetaTest as t");
+        try {
+            ResultSet rs = stmt.executeQuery("SELECT t.DECIMAL_COL, t.DATE FROM PUBLIC.METATEST t;");
 
             assertNotNull(rs);
 
@@ -144,125 +142,139 @@ public class ItJdbcMetadataSelfTest extends AbstractJdbcSelfTest {
             assertEquals(2, meta.getColumnCount());
 
             assertEquals("METATEST", meta.getTableName(1).toUpperCase());
-            assertEquals("DECIMAL", meta.getColumnName(1).toUpperCase());
-            assertEquals("DECIMAL", meta.getColumnLabel(1).toUpperCase());
+            assertEquals("DECIMAL_COL", meta.getColumnName(1).toUpperCase());
+            assertEquals("DECIMAL_COL", meta.getColumnLabel(1).toUpperCase());
             assertEquals(DECIMAL, meta.getColumnType(1));
             assertEquals(meta.getColumnTypeName(1), "DECIMAL");
             assertEquals(meta.getColumnClassName(1), "java.math.BigDecimal");
 
             assertEquals("METATEST", meta.getTableName(2).toUpperCase());
-            assertEquals("DATE", meta.getColumnName(2).toUpperCase());
-            assertEquals("DATE", meta.getColumnLabel(2).toUpperCase());
+            assertEquals("DATE_COL", meta.getColumnName(2).toUpperCase());
+            assertEquals("DATE_COL", meta.getColumnLabel(2).toUpperCase());
             assertEquals(DATE, meta.getColumnType(2));
             assertEquals(meta.getColumnTypeName(2), "DATE");
             assertEquals(meta.getColumnClassName(2), "java.sql.Date");
+        } finally {
+            stmt.execute("DROP TABLE METATEST;");
         }
     }
 
+    private void createMetaTable() {
+        Ignite ignite = clusterNodes.get(0);
+
+        TableDefinition metaTableDef = SchemaBuilders.tableBuilder("PUBLIC", "METATEST")
+                .columns(
+                        SchemaBuilders.column("DECIMAL_COL", ColumnType.decimalOf()).build(),
+                        SchemaBuilders.column("DATE_COL", ColumnType.DATE).build(),
+                        SchemaBuilders.column("ID", ColumnType.INT32).asNullable(false).build())
+                .withPrimaryKey("ID")
+                .build();
+
+        ignite.tables().createTable(metaTableDef.canonicalName(), (tableChange) -> {
+            SchemaConfigurationConverter.convert(metaTableDef, tableChange).changeReplicas(1).changePartitions(10);
+        });
+    }
+
     @Test
     public void testGetTables() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            DatabaseMetaData meta = conn.getMetaData();
+        DatabaseMetaData meta = conn.getMetaData();
 
-            ResultSet rs = meta.getTables("IGNITE", "PUBLIC", "%", new String[]{"TABLE"});
-            assertNotNull(rs);
-            assertTrue(rs.next());
-            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
-            assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
-            assertTrue(rs.next());
-            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
-            assertEquals("PERSON", rs.getString("TABLE_NAME"));
-
-            rs = meta.getTables("IGNITE", "PUBLIC", "%", null);
-            assertNotNull(rs);
-            assertTrue(rs.next());
-            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
-            assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
+        ResultSet rs = meta.getTables("IGNITE", "PUBLIC", "%", new String[]{"TABLE"});
+        assertNotNull(rs);
+        assertTrue(rs.next());
+        assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+        assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
+        assertTrue(rs.next());
+        assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+        assertEquals("PERSON", rs.getString("TABLE_NAME"));
+
+        rs = meta.getTables("IGNITE", "PUBLIC", "%", null);
+        assertNotNull(rs);
+        assertTrue(rs.next());
+        assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+        assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
 
-            rs = meta.getTables("IGNITE", "PUBLIC", "", new String[]{"WRONG"});
-            assertFalse(rs.next());
-        }
+        rs = meta.getTables("IGNITE", "PUBLIC", "", new String[]{"WRONG"});
+        assertFalse(rs.next());
     }
 
     @Test
     public void testGetColumns() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            DatabaseMetaData meta = conn.getMetaData();
+        DatabaseMetaData meta = conn.getMetaData();
 
-            ResultSet rs = meta.getColumns("IGNITE", "PUBLIC", "PERSON", "%");
+        ResultSet rs = meta.getColumns("IGNITE", "PUBLIC", "PERSON", "%");
 
-            assertNotNull(rs);
+        assertNotNull(rs);
 
-            Collection<String> names = new ArrayList<>(2);
+        Collection<String> names = new ArrayList<>(2);
 
-            names.add("NAME");
-            names.add("AGE");
-            names.add("ORGID");
+        names.add("NAME");
+        names.add("AGE");
+        names.add("ORGID");
 
-            int cnt = 0;
+        int cnt = 0;
 
-            while (rs.next()) {
-                String name = rs.getString("COLUMN_NAME");
+        while (rs.next()) {
+            String name = rs.getString("COLUMN_NAME");
 
-                assertTrue(names.remove(name));
+            assertTrue(names.remove(name));
 
-                if ("NAME".equals(name)) {
-                    assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
-                    assertEquals(1, rs.getInt("NULLABLE"));
-                } else if ("AGE".equals(name)) {
-                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
-                    assertEquals(1, rs.getInt("NULLABLE"));
-                } else if ("ORGID".equals(name)) {
-                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
-                    assertEquals(0, rs.getInt("NULLABLE"));
+            if ("NAME".equals(name)) {
+                assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
+                assertEquals(1, rs.getInt("NULLABLE"));
+            } else if ("AGE".equals(name)) {
+                assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                assertEquals(1, rs.getInt("NULLABLE"));
+            } else if ("ORGID".equals(name)) {
+                assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                assertEquals(0, rs.getInt("NULLABLE"));
 
-                }
-                cnt++;
             }
+            cnt++;
+        }
 
-            assertTrue(names.isEmpty());
-            assertEquals(3, cnt);
+        assertTrue(names.isEmpty());
+        assertEquals(3, cnt);
 
-            rs = meta.getColumns("IGNITE", "PUBLIC", "ORGANIZATION", "%");
+        rs = meta.getColumns("IGNITE", "PUBLIC", "ORGANIZATION", "%");
 
-            assertNotNull(rs);
+        assertNotNull(rs);
 
-            names.add("ID");
-            names.add("NAME");
-            names.add("BIGDATA");
-
-            cnt = 0;
-
-            while (rs.next()) {
-                String name = rs.getString("COLUMN_NAME");
-
-                assertTrue(names.remove(name));
-
-                if ("ID".equals(name)) {
-                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
-                    assertEquals(0, rs.getInt("NULLABLE"));
-                } else if ("NAME".equals(name)) {
-                    assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
-                    assertEquals(1, rs.getInt("NULLABLE"));
-                } else if ("BIGDATA".equals(name)) {
-                    assertEquals(DECIMAL, rs.getInt("DATA_TYPE"));
-                    assertEquals(rs.getString("TYPE_NAME"), "DECIMAL");
-                    assertEquals(1, rs.getInt("NULLABLE"));
-                    assertEquals(10, rs.getInt("DECIMAL_DIGITS"));
-                    assertEquals(20, rs.getInt("COLUMN_SIZE"));
-                }
-
-                cnt++;
+        names.add("ID");
+        names.add("NAME");
+        names.add("BIGDATA");
+
+        cnt = 0;
+
+        while (rs.next()) {
+            String name = rs.getString("COLUMN_NAME");
+
+            assertTrue(names.remove(name));
+
+            if ("ID".equals(name)) {
+                assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                assertEquals(0, rs.getInt("NULLABLE"));
+            } else if ("NAME".equals(name)) {
+                assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
+                assertEquals(1, rs.getInt("NULLABLE"));
+            } else if ("BIGDATA".equals(name)) {
+                assertEquals(DECIMAL, rs.getInt("DATA_TYPE"));
+                assertEquals(rs.getString("TYPE_NAME"), "DECIMAL");
+                assertEquals(1, rs.getInt("NULLABLE"));
+                assertEquals(10, rs.getInt("DECIMAL_DIGITS"));
+                assertEquals(20, rs.getInt("COLUMN_SIZE"));
             }
 
-            assertTrue(names.isEmpty());
-            assertEquals(3, cnt);
+            cnt++;
         }
+
+        assertTrue(names.isEmpty());
+        assertEquals(3, cnt);
     }
 
     /**
@@ -270,135 +282,119 @@ public class ItJdbcMetadataSelfTest extends AbstractJdbcSelfTest {
      */
     @Test
     public void testCheckSupports() throws SQLException {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            DatabaseMetaData meta = conn.getMetaData();
+        DatabaseMetaData meta = conn.getMetaData();
 
-            assertTrue(meta.supportsANSI92EntryLevelSQL());
-            assertTrue(meta.supportsAlterTableWithAddColumn());
-            assertTrue(meta.supportsAlterTableWithDropColumn());
-            assertTrue(meta.nullPlusNonNullIsNull());
-        }
+        assertTrue(meta.supportsANSI92EntryLevelSQL());
+        assertTrue(meta.supportsAlterTableWithAddColumn());
+        assertTrue(meta.supportsAlterTableWithDropColumn());
+        assertTrue(meta.nullPlusNonNullIsNull());
     }
 
     @Test
     public void testVersions() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            assertEquals(conn.getMetaData().getDatabaseProductVersion(), ProtocolVersion.LATEST_VER.toString(),
-                    "Unexpected ignite database product version.");
-            assertEquals(conn.getMetaData().getDriverVersion(), ProtocolVersion.LATEST_VER.toString(),
-                    "Unexpected ignite driver version.");
-        }
+        assertEquals(conn.getMetaData().getDatabaseProductVersion(), ProtocolVersion.LATEST_VER.toString(),
+                "Unexpected ignite database product version.");
+        assertEquals(conn.getMetaData().getDriverVersion(), ProtocolVersion.LATEST_VER.toString(),
+                "Unexpected ignite driver version.");
     }
 
     @Test
     public void testSchemasMetadata() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            ResultSet rs = conn.getMetaData().getSchemas();
-
-            Set<String> expectedSchemas = new HashSet<>(Arrays.asList("PUBLIC", "PUBLIC"));
+        ResultSet rs = conn.getMetaData().getSchemas();
 
-            Set<String> schemas = new HashSet<>();
+        Set<String> expectedSchemas = new HashSet<>(Arrays.asList("PUBLIC", "PUBLIC"));
 
-            while (rs.next()) {
-                schemas.add(rs.getString(1));
-            }
+        Set<String> schemas = new HashSet<>();
 
-            assertEquals(schemas, expectedSchemas);
+        while (rs.next()) {
+            schemas.add(rs.getString(1));
         }
+
+        assertEquals(schemas, expectedSchemas);
     }
 
     @Test
     public void testEmptySchemasMetadata() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            ResultSet rs = conn.getMetaData().getSchemas(null, "qqq");
+        ResultSet rs = conn.getMetaData().getSchemas(null, "qqq");
 
-            assertFalse(rs.next(), "Empty result set is expected");
-        }
+        assertFalse(rs.next(), "Empty result set is expected");
     }
 
     @Test
     public void testPrimaryKeyMetadata() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL);
-                ResultSet rs = conn.getMetaData().getPrimaryKeys(null, "PUBLIC", "PERSON")) {
+        ResultSet rs = conn.getMetaData().getPrimaryKeys(null, "PUBLIC", "PERSON");
 
-            int cnt = 0;
+        int cnt = 0;
 
-            while (rs.next()) {
-                assertEquals(rs.getString("COLUMN_NAME"), "ORGID");
-
-                cnt++;
-            }
+        while (rs.next()) {
+            assertEquals(rs.getString("COLUMN_NAME"), "ORGID");
 
-            assertEquals(1, cnt);
+            cnt++;
         }
+
+        assertEquals(1, cnt);
     }
 
     @Test
     public void testGetAllPrimaryKeys() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            ResultSet rs = conn.getMetaData().getPrimaryKeys(null, null, null);
-
-            Set<String> expectedPks = new HashSet<>(Arrays.asList(
-                    "PUBLIC.ORGANIZATION.PK_ORGANIZATION.ID",
-                    "PUBLIC.PERSON.PK_PERSON.ORGID"));
+        ResultSet rs = conn.getMetaData().getPrimaryKeys(null, null, null);
 
-            Set<String> actualPks = new HashSet<>(expectedPks.size());
+        Set<String> expectedPks = new HashSet<>(Arrays.asList(
+                "PUBLIC.ORGANIZATION.PK_ORGANIZATION.ID",
+                "PUBLIC.PERSON.PK_PERSON.ORGID"));
 
-            while (rs.next()) {
-                actualPks.add(rs.getString("TABLE_SCHEM")
-                        + '.' + rs.getString("TABLE_NAME")
-                        + '.' + rs.getString("PK_NAME")
-                        + '.' + rs.getString("COLUMN_NAME"));
-            }
+        Set<String> actualPks = new HashSet<>(expectedPks.size());
 
-            assertEquals(expectedPks, actualPks, "Metadata contains unexpected primary keys info.");
+        while (rs.next()) {
+            actualPks.add(rs.getString("TABLE_SCHEM")
+                    + '.' + rs.getString("TABLE_NAME")
+                    + '.' + rs.getString("PK_NAME")
+                    + '.' + rs.getString("COLUMN_NAME"));
         }
+
+        assertEquals(expectedPks, actualPks, "Metadata contains unexpected primary keys info.");
     }
 
     @Test
     public void testInvalidCatalog() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            DatabaseMetaData meta = conn.getMetaData();
+        DatabaseMetaData meta = conn.getMetaData();
 
-            ResultSet rs = meta.getSchemas("q", null);
+        ResultSet rs = meta.getSchemas("q", null);
 
-            assertFalse(rs.next(), "Results must be empty");
+        assertFalse(rs.next(), "Results must be empty");
 
-            rs = meta.getTables("q", null, null, null);
+        rs = meta.getTables("q", null, null, null);
 
-            assertFalse(rs.next(), "Results must be empty");
+        assertFalse(rs.next(), "Results must be empty");
 
-            rs = meta.getColumns("q", null, null, null);
+        rs = meta.getColumns("q", null, null, null);
 
-            assertFalse(rs.next(), "Results must be empty");
+        assertFalse(rs.next(), "Results must be empty");
 
-            rs = meta.getIndexInfo("q", null, null, false, false);
+        rs = meta.getIndexInfo("q", null, null, false, false);
 
-            assertFalse(rs.next(), "Results must be empty");
+        assertFalse(rs.next(), "Results must be empty");
 
-            rs = meta.getPrimaryKeys("q", null, null);
+        rs = meta.getPrimaryKeys("q", null, null);
 
-            assertFalse(rs.next(), "Results must be empty");
-        }
+        assertFalse(rs.next(), "Results must be empty");
     }
 
     @Test
     public void testGetTableTypes() throws Exception {
-        try (Connection conn = DriverManager.getConnection(URL)) {
-            DatabaseMetaData meta = conn.getMetaData();
+        DatabaseMetaData meta = conn.getMetaData();
 
-            ResultSet rs = meta.getTableTypes();
+        ResultSet rs = meta.getTableTypes();
 
-            assertTrue(rs.next());
+        assertTrue(rs.next());
 
-            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+        assertEquals("TABLE", rs.getString("TABLE_TYPE"));
 
-            assertFalse(rs.next());
-        }
+        assertFalse(rs.next());
     }
 
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16203")
     public void testParametersMetadata() throws Exception {
         // Perform checks few times due to query/plan caching.
         for (int i = 0; i < 3; i++) {
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMultiStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMultiStatementSelfTest.java
new file mode 100644
index 0000000..4ae7fba
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcMultiStatementSelfTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for ddl queries that contain multiply sql statements, separated by ";".
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-16204")
+public class ItJdbcMultiStatementSelfTest extends AbstractJdbcSelfTest {
+    /**
+     * Setup tables.
+     */
+    @BeforeEach
+    public void setupTables() throws Exception {
+        execute("DROP TABLE IF EXISTS TEST_TX; "
+                + "DROP TABLE IF EXISTS PUBLIC.TRANSACTIONS; "
+                + "DROP TABLE IF EXISTS ONE;"
+                + "DROP TABLE IF EXISTS TWO;");
+
+        execute("CREATE TABLE TEST_TX (ID INT PRIMARY KEY, AGE INT, NAME VARCHAR) ");
+
+        execute("INSERT INTO TEST_TX VALUES "
+                + "(1, 17, 'James'), "
+                + "(2, 43, 'Valery'), "
+                + "(3, 25, 'Michel'), "
+                + "(4, 19, 'Nick');");
+    }
+
+    /**
+     * Execute sql script using thin driver.
+     */
+    private void execute(String sql) throws Exception {
+        stmt.executeUpdate(sql);
+    }
+
+    /**
+     * Assert that script containing both h2 and non h2 (native) sql statements is handled correctly.
+     */
+    @Test
+    public void testMixedCommands() throws Exception {
+        execute("CREATE TABLE public.transactions (pk INT, id INT, k VARCHAR, v VARCHAR, PRIMARY KEY (pk, id)); "
+                + "CREATE INDEX transactions_id_k_v ON public.transactions (id, k, v) INLINE_SIZE 150; "
+                + "INSERT INTO public.transactions VALUES (1,2,'some', 'word') ; "
+                + "CREATE INDEX transactions_k_v_id ON public.transactions (k, v, id) INLINE_SIZE 150; "
+                + "CREATE INDEX transactions_pk_id ON public.transactions (pk, id) INLINE_SIZE 20;");
+    }
+
+    /**
+     * Sanity test for scripts, containing empty statements are handled correctly.
+     */
+    @Test
+    public void testEmptyStatements() throws Exception {
+        execute(";; ;;;;");
+        execute(" ;; ;;;; ");
+        execute("CREATE TABLE ONE (id INT PRIMARY KEY, VAL VARCHAR);;"
+                + "CREATE INDEX T_IDX ON ONE(val)"
+                + ";;UPDATE ONE SET VAL = 'SOME';;;  ");
+        execute("DROP INDEX T_IDX ;;  ;;"
+                + "UPDATE ONE SET VAL = 'SOME'");
+    }
+
+    /**
+     * Check multi-statement containing both h2 and native parser statements (having "?" args) works well.
+     */
+    @Test
+    public void testMultiStatementTxWithParams() throws Exception {
+        int leoAge = 28;
+
+        String nickolas = "Nickolas";
+
+        int gabAge = 84;
+        String gabName = "Gab";
+
+        int delYounger = 19;
+
+        String complexQuery =
+                "INSERT INTO TEST_TX VALUES (5, ?, 'Leo'); " // 1
+                    + ";;;;"
+                    + "BEGIN ; "
+                    + "UPDATE TEST_TX  SET name = ? WHERE name = 'Nick' ;" // 2
+                    + "INSERT INTO TEST_TX VALUES (6, ?, ?); "   // 3, 4
+                    + "DELETE FROM TEST_TX WHERE age < ?; "   // 5
+                    + "COMMIT;";
+
+        try (PreparedStatement p = conn.prepareStatement(complexQuery)) {
+            p.setInt(1, leoAge);
+            p.setString(2, nickolas);
+            p.setInt(3, gabAge);
+            p.setString(4, gabName);
+            p.setInt(5, delYounger);
+
+            assertFalse(p.execute(), "Expected, that first result is an update count.");
+
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the INSERT.");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of an empty statement.");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of an empty statement.");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of an empty statement.");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of an empty statement.");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the BEGIN");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the UPDATE");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the INSERT");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the DELETE");
+            assertTrue(p.getMoreResults(), "More results are expected.");
+            assertTrue(p.getUpdateCount() != -1, "Expected update count of the COMMIT");
+
+            assertFalse(p.getMoreResults(), "There should have been no results.");
+            assertFalse(p.getUpdateCount() != -1, "There should have been no update results.");
+        }
+
+        try (PreparedStatement sel = conn.prepareStatement("SELECT * FROM TEST_TX ORDER BY ID;")) {
+            try (ResultSet pers = sel.executeQuery()) {
+                assertTrue(pers.next());
+                assertEquals(43, age(pers));
+                assertEquals("Valery", name(pers));
+
+                assertTrue(pers.next());
+                assertEquals(25, age(pers));
+                assertEquals("Michel", name(pers));
+
+                assertTrue(pers.next());
+                assertEquals(19, age(pers));
+                assertEquals("Nickolas", name(pers));
+
+                assertTrue(pers.next());
+                assertEquals(28, age(pers));
+                assertEquals("Leo", name(pers));
+
+                assertTrue(pers.next());
+                assertEquals(84, age(pers));
+                assertEquals("Gab", name(pers));
+
+                assertFalse(pers.next());
+            }
+        }
+    }
+
+    /**
+     * Extract person's name from result set.
+     */
+    private static String name(ResultSet rs) throws SQLException {
+        return rs.getString("NAME");
+    }
+
+    /**
+     * Extract person's age from result set.
+     */
+    private static int age(ResultSet rs) throws SQLException {
+        return rs.getInt("AGE");
+    }
+
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcResultSetSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcResultSetSelfTest.java
index 14789c2..ad1447e 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcResultSetSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcResultSetSelfTest.java
@@ -30,69 +30,95 @@ import java.io.Serializable;
 import java.math.BigDecimal;
 import java.sql.Blob;
 import java.sql.Clob;
-import java.sql.Connection;
 import java.sql.Date;
-import java.sql.DriverManager;
 import java.sql.NClob;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.sql.Statement;
 import java.sql.Time;
 import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.ArrayList;
 import java.util.GregorianCalendar;
+import java.util.List;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.schema.configuration.SchemaConfigurationConverter;
 import org.apache.ignite.internal.tostring.S;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import org.apache.ignite.schema.SchemaBuilders;
+import org.apache.ignite.schema.definition.ColumnDefinition;
+import org.apache.ignite.schema.definition.ColumnType;
+import org.apache.ignite.schema.definition.TableDefinition;
+import org.apache.ignite.table.RecordView;
+import org.apache.ignite.table.Tuple;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 /**
  * Result set test.
  */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
 public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
-    /** SQL query. */
-    private static final String SQL =
+    /** SQL static query. */
+    private static final String STATIC_SQL =
             "SELECT 1::INTEGER as id, true as boolVal, 1::TINYINT as byteVal, 1::SMALLINT as shortVal, 1::INTEGER as intVal, 1::BIGINT "
                     + "as longVal, 1.0::FLOAT as floatVal, 1.0::DOUBLE as doubleVal, 1.0::DECIMAL as bigVal, "
                     + "'1' as strVal, '1', '1901-02-01'::DATE as dateVal, '01:01:01'::TIME as timeVal, 1::TIMESTAMP as tsVal;";
 
-    /** Statement. */
-    private Statement stmt;
-
-    /**
-     * Create the connection ant statement.
-     *
-     * @throws Exception if failed.
-     */
-    @BeforeEach
-    public void beforeTest() throws Exception {
-        Connection conn = DriverManager.getConnection(URL);
-
-        stmt = conn.createStatement();
-
-        assertNotNull(stmt);
-        assertFalse(stmt.isClosed());
-    }
-
-    /**
-     * Close the connection and statement.
-     *
-     * @throws Exception if failed.
-     */
-    @AfterEach
-    public void afterTest() throws Exception {
-        if (stmt != null) {
-            stmt.getConnection().close();
-
-            stmt.close();
-
-            assertTrue(stmt.isClosed());
-        }
+    /** SQL query. */
+    private static final String SQL_SINGLE_RES = "select id, boolVal, byteVal, shortVal, intVal, longVal, floatVal, "
+            + "doubleVal, bigVal, strVal from TEST WHERE id = 1";
+
+    @BeforeAll
+    public static void beforeClass() {
+        Ignite ignite = clusterNodes.get(0);
+
+        List<ColumnDefinition> columns = new ArrayList<>();
+
+        columns.add(SchemaBuilders.column("ID", ColumnType.INT32).build());
+        columns.add(SchemaBuilders.column("BOOLVAL", ColumnType.INT8).asNullable(true).build());
+        columns.add(SchemaBuilders.column("BYTEVAL", ColumnType.INT8).asNullable(true).build());
+        columns.add(SchemaBuilders.column("SHORTVAL", ColumnType.INT16).asNullable(true).build());
+        columns.add(SchemaBuilders.column("INTVAL", ColumnType.INT32).asNullable(true).build());
+        columns.add(SchemaBuilders.column("LONGVAL", ColumnType.INT64).asNullable(true).build());
+        columns.add(SchemaBuilders.column("FLOATVAL", ColumnType.FLOAT).asNullable(true).build());
+        columns.add(SchemaBuilders.column("DOUBLEVAL", ColumnType.DOUBLE).asNullable(true).build());
+        columns.add(SchemaBuilders.column("BIGVAL", ColumnType.decimalOf()).asNullable(true).build());
+        columns.add(SchemaBuilders.column("STRVAL", ColumnType.string()).asNullable(true).build());
+        columns.add(SchemaBuilders.column("ARRVAL", ColumnType.blobOf()).asNullable(true).build());
+        columns.add(SchemaBuilders.column("DATEVAL", ColumnType.DATE).asNullable(true).build());
+        columns.add(SchemaBuilders.column("TIMEVAL", ColumnType.TemporalColumnType.time()).asNullable(true).build());
+        columns.add(SchemaBuilders.column("TSVAL", ColumnType.TemporalColumnType.timestamp()).asNullable(true).build());
+        columns.add(SchemaBuilders.column("URLVAL", ColumnType.blobOf()).asNullable(true).build());
+
+        TableDefinition personTableDef = SchemaBuilders.tableBuilder("PUBLIC", "TEST")
+                .columns(columns)
+                .withPrimaryKey("ID")
+                .build();
+
+        ignite.tables().createTable(personTableDef.canonicalName(), (tableChange) ->
+                SchemaConfigurationConverter.convert(personTableDef, tableChange).changeReplicas(1).changePartitions(10)
+        );
+
+        RecordView<Tuple> tupleRecordView = ignite.tables().table("PUBLIC.TEST").recordView();
+
+        Tuple tuple = Tuple.create();
+
+        tuple.set("BOOLVAL", (byte) 1).set("BYTEVAL", (byte) 1).set("SHORTVAL", (short) 1)
+            .set("INTVAL", 1).set("LONGVAL", 1L).set("FLOATVAL", 1.0f).set("DOUBLEVAL", 1.0d)
+            .set("BIGVAL", new BigDecimal("1")).set("STRVAL", "1")
+            .set("DATEVAL", LocalDate.parse("1901-02-01"))
+            .set("TIMEVAL", LocalTime.parse("01:01:01"))
+            .set("TSVAL", Instant.ofEpochMilli(1));
+
+        tupleRecordView.insert(null, tuple.set("ID", 1));
+        tupleRecordView.insert(null, tuple.set("ID", 2));
     }
 
     @Test
     public void testBoolean() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -107,7 +133,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
                 assertEquals(1.0, rs.getDouble(2));
                 assertEquals(1.0f, rs.getFloat(2));
                 assertEquals(new BigDecimal(1), rs.getBigDecimal(2));
-                assertEquals(rs.getString(2), "true");
+                assertEquals(rs.getString(2), "1"); // Because we don't support bool values right now.
 
                 assertTrue(rs.getObject(2, Boolean.class));
                 assertEquals((byte) 1, rs.getObject(2, Byte.class));
@@ -117,7 +143,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
                 assertEquals(1.0f, rs.getObject(2, Float.class));
                 assertEquals(1.0, rs.getObject(2, Double.class));
                 assertEquals(new BigDecimal(1), rs.getObject(2, BigDecimal.class));
-                assertEquals("true", rs.getObject(2, String.class));
+                assertEquals("1", rs.getObject(2, String.class)); // Because we don't support bool values right now.
             }
 
             cnt++;
@@ -162,7 +188,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testByte() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -199,7 +225,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testShort() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -237,7 +263,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testInteger() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -275,7 +301,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testLong() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -313,7 +339,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testFloat() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -351,7 +377,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testDouble() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -389,7 +415,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testBigDecimal() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -404,8 +430,8 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
                 assertEquals(1, rs.getLong(9));
                 assertEquals(1.0, rs.getDouble(9));
                 assertEquals(1.0f, rs.getFloat(9));
-                assertEquals(new BigDecimal("1.0"), rs.getBigDecimal(9));
-                assertEquals(rs.getString(9), "1.0");
+                assertEquals(new BigDecimal("1.000"), rs.getBigDecimal(9));
+                assertEquals(rs.getString(9), "1.000");
 
                 assertTrue(rs.getObject(9, Boolean.class));
                 assertEquals((byte) 1, rs.getObject(9, Byte.class));
@@ -414,8 +440,8 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
                 assertEquals(1, rs.getObject(9, Long.class));
                 assertEquals(1.f, rs.getObject(9, Float.class));
                 assertEquals(1, rs.getObject(9, Double.class));
-                assertEquals(new BigDecimal("1.0"), rs.getObject(9, BigDecimal.class));
-                assertEquals(rs.getObject(9, String.class), "1.0");
+                assertEquals(new BigDecimal("1.000"), rs.getObject(9, BigDecimal.class));
+                assertEquals(rs.getObject(9, String.class), "1.000");
             }
 
             cnt++;
@@ -430,7 +456,6 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
      * @throws Exception If failed.
      */
     @Test
-    @Disabled
     public void testBigDecimalScale() throws Exception {
         assertEquals(convertStringToBigDecimalViaJdbc("0.1234", 2).toString(), "0.12");
         assertEquals(convertStringToBigDecimalViaJdbc("1.0005", 3).toString(), "1.001");
@@ -456,7 +481,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testString() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         int cnt = 0;
 
@@ -492,53 +517,25 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
         assertEquals(1, cnt);
     }
 
-    /**
-     * TODO: IGNITE-15163.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    @Disabled
-    public void testArray() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
-
-        int cnt = 0;
-
-        while (rs.next()) {
-            if (cnt == 0) {
-                assertArrayEquals(new byte[]{1}, rs.getBytes("arrVal"));
-                assertArrayEquals(new byte[]{1}, rs.getBytes(11));
-            }
-
-            cnt++;
-        }
-
-        assertEquals(1, cnt);
-    }
-
-    /**
-     * TODO: IGNITE-15163.
-     *
-     * @throws Exception If failed.
-     */
-    @SuppressWarnings("deprecation")
     @Test
     public void testDate() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(STATIC_SQL);
 
         int cnt = 0;
 
+        Date exp = Date.valueOf(LocalDate.parse("1901-02-01"));
+
         while (rs.next()) {
             if (cnt == 0) {
-                assert rs.getDate("dateVal").equals(new Date(1, 1, 1));
+                assertEquals(exp, rs.getDate("dateVal"));
 
-                assertEquals(new Date(1, 1, 1), rs.getDate(12));
-                assertEquals(new Time(new Date(1, 1, 1).getTime()), rs.getTime(12));
-                assertEquals(new Timestamp(new Date(1, 1, 1).getTime()), rs.getTimestamp(12));
+                assertEquals(exp, rs.getDate(12));
+                assertEquals(new Time(exp.getTime()), rs.getTime(12));
+                assertEquals(new Timestamp(exp.getTime()), rs.getTimestamp(12));
 
-                assertEquals(new Date(1, 1, 1), rs.getObject(12, Date.class));
-                assertEquals(new Time(new Date(1, 1, 1).getTime()), rs.getObject(12, Time.class));
-                assertEquals(new Timestamp(new Date(1, 1, 1).getTime()), rs.getObject(12, Timestamp.class));
+                assertEquals(exp, rs.getObject(12, Date.class));
+                assertEquals(new Time(exp.getTime()), rs.getObject(12, Time.class));
+                assertEquals(new Timestamp(exp.getTime()), rs.getObject(12, Timestamp.class));
             }
 
             cnt++;
@@ -548,14 +545,14 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
     }
 
     /**
-     * TODO: IGNITE-15163.
+     * Test date-time.
      *
      * @throws Exception If failed.
      */
     @SuppressWarnings("deprecation")
     @Test
     public void testTime() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(STATIC_SQL);
 
         int cnt = 0;
 
@@ -585,7 +582,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
      */
     @Test
     public void testTimestamp() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        ResultSet rs = stmt.executeQuery(STATIC_SQL);
 
         int cnt = 0;
 
@@ -607,46 +604,9 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
         assertEquals(1, cnt);
     }
 
-    /**
-     * TODO Enable when sql engine will be fully integrated.
-     *
-     * @throws Exception If failed.
-     */
-    @Test
-    @Disabled
-    public void testObject() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
-
-        int cnt = 0;
-
-        TestObjectField exp = new TestObjectField(100, "AAAA");
-
-        while (rs.next()) {
-            if (cnt == 0) {
-                assertEquals(exp, rs.getObject("objVal"));
-
-                assertEquals(exp, rs.getObject(15));
-
-                assertEquals(exp, rs.getObject(15, Object.class));
-
-                assertEquals(exp, rs.getObject(15, TestObjectField.class));
-            }
-
-            cnt++;
-        }
-
-        assertEquals(1, cnt);
-    }
-
-    /**
-     * TODO Enable when sql engine will be fully integrated.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
     public void testNavigation() throws Exception {
-        ResultSet rs = stmt.executeQuery("select id from TestObject where id > 0");
+        ResultSet rs = stmt.executeQuery("SELECT * FROM test where id > 0");
 
         assertTrue(rs.isBeforeFirst());
         assertFalse(rs.isAfterLast());
@@ -678,14 +638,14 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
         assertFalse(rs.isLast());
         assertEquals(0, rs.getRow());
 
-        rs = stmt.executeQuery("select id from TestObject where id < 0");
+        rs = stmt.executeQuery("select id from test where id < 0");
 
         assertFalse(rs.isBeforeFirst());
     }
 
     @Test
     public void testFindColumn() throws Exception {
-        final ResultSet rs = stmt.executeQuery(SQL);
+        final ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         assertNotNull(rs);
         assertTrue(rs.next());
@@ -697,7 +657,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testNotSupportedTypes() throws Exception {
-        final ResultSet rs = stmt.executeQuery(SQL);
+        final ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         assertTrue(rs.next());
 
@@ -748,7 +708,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testUpdateNotSupported() throws Exception {
-        final ResultSet rs = stmt.executeQuery(SQL);
+        final ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         assertTrue(rs.next());
 
@@ -921,7 +881,7 @@ public class ItJdbcResultSetSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testExceptionOnClosedResultSet() throws Exception {
-        final ResultSet rs = stmt.executeQuery(SQL);
+        final ResultSet rs = stmt.executeQuery(SQL_SINGLE_RES);
 
         rs.close();
 
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcSelectAfterAlterTable.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcSelectAfterAlterTable.java
new file mode 100644
index 0000000..135439d
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcSelectAfterAlterTable.java
@@ -0,0 +1,91 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Base class for complex SQL tests based on JDBC driver.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcSelectAfterAlterTable extends AbstractJdbcSelfTest {
+    /** {@inheritDoc} */
+    @BeforeEach
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        stmt.executeUpdate("CREATE TABLE PUBLIC.PERSON (ID BIGINT, NAME VARCHAR, CITY_ID BIGINT, PRIMARY KEY (ID, CITY_ID))");
+        stmt.executeUpdate("INSERT INTO PUBLIC.PERSON (ID, NAME, CITY_ID) values (1, 'name_1', 11)");
+    }
+
+    /** {@inheritDoc} */
+    @AfterEach
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+        clusterNodes.get(0).tables().dropTableAsync("PUBLIC.PERSON").get();
+    }
+
+    /**
+     * Alter table test.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSelectAfterAlterTableSingleNode() throws Exception {
+        stmt.executeUpdate("alter table PUBLIC.PERSON add AGE int");
+
+        checkNewColumn(stmt);
+    }
+
+    /**
+     * New column check.
+     *
+     * @param stmt Statement to check new column.
+     *
+     * @throws SQLException If failed.
+     */
+    public void checkNewColumn(Statement stmt) throws SQLException {
+        ResultSet rs = stmt.executeQuery("select * from PUBLIC.PERSON");
+
+        ResultSetMetaData meta = rs.getMetaData();
+
+        assertEquals(4, meta.getColumnCount());
+
+        boolean newColExists = false;
+
+        for (int i = 1; i <= meta.getColumnCount(); ++i) {
+            if ("AGE".equalsIgnoreCase(meta.getColumnName(i))) {
+                newColExists = true;
+
+                break;
+            }
+        }
+
+        assertTrue(newColExists);
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementCancelSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementCancelSelfTest.java
new file mode 100644
index 0000000..19b5cbe
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementCancelSelfTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Statement cancel test.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-16205")
+public class ItJdbcStatementCancelSelfTest extends ItJdbcAbstractStatementSelfTest {
+    /**
+     * Trying to cancel stament without query. In given case cancel is noop, so no exception expected.
+     */
+    @Test
+    public void testCancelingStmtWithoutQuery() {
+        try {
+            stmt.cancel();
+        } catch (Exception e) {
+            log.error("Unexpected exception.", e);
+
+            fail("Unexpected exception");
+        }
+    }
+
+    /**
+     * Trying to retrieve result set of a canceled query.
+     * SQLException with message "The query was cancelled while executing." expected.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testResultSetRetrievalInCanceledStatement() throws Exception {
+        stmt.execute("SELECT 1; SELECT 2; SELECT 3;");
+
+        assertNotNull(stmt.getResultSet());
+
+        stmt.cancel();
+
+        assertThrows(SQLException.class, () -> stmt.getResultSet(), "The query was cancelled while executing.");
+    }
+
+    /**
+     * Trying to cancel already cancelled query.
+     * No exceptions exceped.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCancelCanceledQuery() throws Exception {
+        stmt.execute("SELECT 1;");
+
+        assertNotNull(stmt.getResultSet());
+
+        stmt.cancel();
+
+        stmt.cancel();
+
+        assertThrows(SQLException.class, () -> stmt.getResultSet(), "The query was cancelled while executing.");
+    }
+
+    /**
+     * Trying to cancel closed query.
+     * SQLException with message "Statement is closed." expected.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCancelClosedStmt() throws Exception {
+        stmt.close();
+
+        assertThrows(SQLException.class, () -> stmt.cancel(), "Statement is closed.");
+    }
+
+    /**
+     * Trying to call <code>resultSet.next()</code> on a canceled query.
+     * SQLException with message "The query was cancelled while executing." expected.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testResultSetNextAfterCanceling() throws Exception {
+        stmt.setFetchSize(10);
+
+        ResultSet rs = stmt.executeQuery("select * from PUBLIC.PERSON");
+
+        assertTrue(rs.next());
+
+        stmt.cancel();
+
+        assertThrows(SQLException.class, rs::next, "The query was cancelled while executing.");
+    }
+
+    /**
+     * Ensure that it's possible to execute new query on cancelled statement.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCancelAnotherStmt() throws Exception {
+        stmt.setFetchSize(10);
+
+        ResultSet rs = stmt.executeQuery("select * from PUBLIC.PERSON");
+
+        assertTrue(rs.next());
+
+        stmt.cancel();
+
+        ResultSet rs2 = stmt.executeQuery("select * from PUBLIC.PERSON order by ID asc");
+
+        assertTrue(rs2.next(), "The other cursor mustn't be closed");
+    }
+
+    /**
+     * Ensure that stament cancel doesn't affect another statement workflow, created by the same connection.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testCancelAnotherStmtResultSet() throws Exception {
+        try (Statement anotherStmt = conn.createStatement()) {
+            ResultSet rs1 = stmt.executeQuery("select * from PUBLIC.PERSON WHERE ID % 2 = 0");
+
+            ResultSet rs2 = anotherStmt.executeQuery("select * from PUBLIC.PERSON  WHERE ID % 2 <> 0");
+
+            stmt.cancel();
+
+            assertThrows(SQLException.class, rs1::next, "The query was cancelled while executing.");
+
+            assertTrue(rs2.next(), "The other cursor mustn't be closed");
+        }
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementSelfTest.java
index 794c619..f08c654 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcStatementSelfTest.java
@@ -31,62 +31,33 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
 import java.sql.Statement;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 /**
  * Statement test.
  */
-@SuppressWarnings({"ThrowableNotThrown"})
-public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcStatementSelfTest extends ItJdbcAbstractStatementSelfTest {
     /** SQL query. */
-    private static final String SQL =
-            "select 1::INTEGER, true, 1::TINYINT, 1::SMALLINT, 1::INTEGER, 1::BIGINT, 1.0::FLOAT, 1.0::DOUBLE, 1.0::DOUBLE, '1';";
+    private static final String SQL = "select * from PERSON where age > 30";
 
-    /** Statement. */
-    private Statement stmt;
+    @BeforeAll
+    public static void beforeClass() throws Exception {
+        try (Statement statement = conn.createStatement()) {
+            statement.executeUpdate("create table TEST(ID int primary key, NAME varchar(20));");
 
-    /** Connection. */
-    private Connection conn;
+            int stmtCnt = 10;
 
-    /**
-     * Create the connection ant statement.
-     *
-     * @throws Exception if failed.
-     */
-    @BeforeEach
-    protected void beforeTest() throws Exception {
-        conn = DriverManager.getConnection(URL);
-
-        stmt = conn.createStatement();
-
-        assertNotNull(stmt);
-        assertFalse(stmt.isClosed());
-    }
-
-    /**
-     * Close the connection and statement.
-     *
-     * @throws Exception if failed.
-     */
-    @AfterEach
-    protected void afterTest() throws Exception {
-        if (stmt != null && !stmt.isClosed()) {
-            stmt.close();
-
-            assertTrue(stmt.isClosed());
+            for (int i = 0; i < stmtCnt; ++i) {
+                statement.executeUpdate("insert into TEST (ID, NAME) values (" + i + ", 'name_" + i + "'); ");
+            }
         }
-
-        conn.close();
-
-        assertTrue(stmt.isClosed());
-        assertTrue(conn.isClosed());
     }
 
     @Test
-    @Disabled("IGNITE-15108")
     public void testExecuteQuery0() throws Exception {
         ResultSet rs = stmt.executeQuery(SQL);
 
@@ -136,7 +107,6 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("IGNITE-15108")
     public void testExecute() throws Exception {
         assertTrue(stmt.execute(SQL));
 
@@ -172,7 +142,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("IGNITE-15108")
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16269")
     public void testMaxRows() throws Exception {
         stmt.setMaxRows(1);
 
@@ -262,12 +232,17 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testCloseResultSetByConnectionClose() throws Exception {
-        ResultSet rs = stmt.executeQuery(SQL);
+        try (
+                Connection conn = DriverManager.getConnection(URL);
+                Statement stmt = conn.createStatement()
+        ) {
+            ResultSet rs = stmt.executeQuery(SQL);
 
-        conn.close();
+            conn.close();
 
-        assertTrue(stmt.isClosed(), "Statement must be implicitly closed after close connection");
-        assertTrue(rs.isClosed(), "ResultSet must be implicitly closed after close connection");
+            assertTrue(stmt.isClosed(), "Statement must be implicitly closed after close connection");
+            assertTrue(rs.isClosed(), "ResultSet must be implicitly closed after close connection");
+        }
     }
 
     @Test
@@ -326,7 +301,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testExecuteQueryMultipleOnlyResultSets() throws Exception {
-        //        assertTrue(conn.getMetaData().supportsMultipleResultSets());
+        assertTrue(conn.getMetaData().supportsMultipleResultSets());
 
         int stmtCnt = 10;
 
@@ -358,10 +333,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("IGNITE-15108")
     public void testExecuteQueryMultipleOnlyDml() throws Exception {
-        conn.setSchema(null);
-
         Statement stmt0 = conn.createStatement();
 
         int stmtCnt = 10;
@@ -395,12 +367,8 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("IGNITE-15108")
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16276")
     public void testExecuteQueryMultipleMixed() throws Exception {
-        conn.setSchema(null);
-
-        Statement stmt0 = conn.createStatement();
-
         int stmtCnt = 10;
 
         StringBuilder sql = new StringBuilder("drop table if exists test; create table test(ID int primary key, NAME varchar(20)); ");
@@ -413,30 +381,28 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
             }
         }
 
-        assertFalse(stmt0.execute(sql.toString()));
+        assertFalse(stmt.execute(sql.toString()));
 
         // DROP TABLE statement
-        assertNull(stmt0.getResultSet());
-        assertEquals(0, stmt0.getUpdateCount());
+        assertNull(stmt.getResultSet());
+        assertEquals(0, stmt.getUpdateCount());
 
-        assertTrue(stmt0.getMoreResults(), "Result set doesn't have more results.");
+        assertTrue(stmt.getMoreResults(), "Result set doesn't have more results.");
 
         // CREATE TABLE statement
-        assertNull(stmt0.getResultSet());
-        assertEquals(0, stmt0.getUpdateCount());
-
-        boolean notEmptyResult = false;
+        assertNull(stmt.getResultSet());
+        assertEquals(0, stmt.getUpdateCount());
 
         for (int i = 0; i < stmtCnt; ++i) {
-            assertTrue(stmt0.getMoreResults());
+            assertTrue(stmt.getMoreResults());
 
             if (i % 2 == 0) {
-                assertNull(stmt0.getResultSet());
-                assertEquals(1, stmt0.getUpdateCount());
+                assertNull(stmt.getResultSet());
+                assertEquals(1, stmt.getUpdateCount());
             } else {
-                assertEquals(-1, stmt0.getUpdateCount());
+                assertEquals(-1, stmt.getUpdateCount());
 
-                ResultSet rs = stmt0.getResultSet();
+                ResultSet rs = stmt.getResultSet();
 
                 int rowsCnt = 0;
 
@@ -444,23 +410,16 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
                     rowsCnt++;
                 }
 
-                assertTrue(rowsCnt <= (i + 1) / 2);
-
-                if (rowsCnt == (i + 1) / 2) {
-                    notEmptyResult = true;
-                }
-
-                assertTrue(notEmptyResult);
-
-                assertFalse(stmt0.getMoreResults());
+                assertEquals((i + 1) / 2, rowsCnt);
             }
         }
+
+        assertFalse(stmt.getMoreResults());
     }
 
     @Test
-    @Disabled("IGNITE-15108")
     public void testExecuteUpdate() throws Exception {
-        final String sqlText = "update test set val=1 where _key=1";
+        final String sqlText = "update TEST set NAME='CHANGED_NAME_1' where ID=1;";
 
         assertEquals(1, stmt.executeUpdate(sqlText));
 
@@ -471,7 +430,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
 
     @Test
     public void testExecuteUpdateProducesResultSet() {
-        final String sqlText = "select * from test";
+        final String sqlText = "select * from TEST;";
 
         assertThrows(SQLException.class, () -> stmt.executeUpdate(sqlText),
                 "Given statement type does not match that declared by JDBC driver"
@@ -479,6 +438,23 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
+    public void testExecuteUpdateOnDdl() throws SQLException {
+        String tableName = "\"test_" + UUID.randomUUID().toString() + "\"";
+
+        stmt.executeUpdate("CREATE TABLE " + tableName + "(id INT PRIMARY KEY, val VARCHAR)");
+
+        ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + tableName);
+
+        assertNotNull(rs, "ResultSet expected");
+        assertTrue(rs.next(), "One row expected");
+        assertEquals(0L, rs.getLong(1));
+
+        stmt.executeUpdate("DROP TABLE " + tableName);
+
+        assertThrows(SQLException.class, () -> stmt.executeQuery("SELECT COUNT(*) FROM " + tableName));
+    }
+
+    @Test
     public void testClose() throws Exception {
         String sqlText = "select 1";
 
@@ -517,7 +493,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16269")
     public void testGetSetMaxRows() throws Exception {
         assertEquals(0, stmt.getMaxRows());
 
@@ -670,15 +646,7 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
         checkStatementClosed(() -> stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
     }
 
-    /**
-     * TODO Enable when batch query is supported
-     *
-     * <p>Verifies that empty batch can be performed.
-     *
-     * @throws Exception If failed.
-     */
     @Test
-    @Disabled
     public void testBatchEmpty() throws Exception {
         assertTrue(conn.getMetaData().supportsBatchUpdates());
 
@@ -752,26 +720,23 @@ public class ItJdbcStatementSelfTest extends AbstractJdbcSelfTest {
     }
 
     @Test
-    @Disabled("IGNITE-15108")
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16268")
     public void testStatementTypeMismatchUpdate() throws Exception {
         assertThrows(
                 SQLException.class,
-                () -> stmt.executeQuery("update test set val=28 where _key=1"),
+                () -> stmt.executeQuery("update TEST set NAME='28' where ID=1"),
                 "Given statement type does not match that declared by JDBC driver"
         );
 
-        ResultSet rs = stmt.executeQuery("select val from test where _key=1");
+        ResultSet rs = stmt.executeQuery("select NAME from TEST where ID=1");
 
         boolean next = rs.next();
 
         assertTrue(next);
 
-        assertEquals(
-                1,
-                rs.getInt(1),
+        assertEquals(1, rs.getInt(1),
                 "The data must not be updated. "
                         + "Because update statement is executed via 'executeQuery' method."
-                        + " Data [val=" + rs.getInt(1) + ']'
-        );
+                        + " Data [val=" + rs.getInt(1) + ']');
     }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcUpdateStatementSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcUpdateStatementSelfTest.java
new file mode 100644
index 0000000..baaa1a1
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/ItJdbcUpdateStatementSelfTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.runner.app.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.sql.SQLException;
+import org.apache.ignite.table.KeyValueView;
+import org.apache.ignite.table.Tuple;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Update statement self test.
+ */
+@Disabled("https://issues.apache.org/jira/browse/IGNITE-15655")
+public class ItJdbcUpdateStatementSelfTest extends ItJdbcAbstractStatementSelfTest {
+    /**
+     * Execute test.
+     *
+     * @throws SQLException If failed.
+     */
+    @Test
+    public void testExecute() throws SQLException {
+        stmt.execute("update PUBLIC.PERSON set firstName = 'Jack' where substring(SID, 2, 1)::int % 2 = 0");
+
+        KeyValueView<Tuple, Tuple> person = clusterNodes.get(0).tables()
+                .table("PUBLIC.PERSON").keyValueView();
+
+        assertEquals("John", person.get(null, Tuple.create().set("ID", 1)).stringValue("FIRSTNAME"));
+        assertEquals("Jack", person.get(null, Tuple.create().set("ID", 2)).stringValue("FIRSTNAME"));
+        assertEquals("Mike", person.get(null, Tuple.create().set("ID", 3)).stringValue("FIRSTNAME"));
+    }
+
+    /**
+     * Execute update test.
+     *
+     * @throws SQLException If failed.
+     */
+    @Test
+    public void testExecuteUpdate() throws SQLException {
+        int i = stmt.executeUpdate(
+                "update PUBLIC.PERSON set firstName = 'Jack' where substring(SID, 2, 1)::int % 2 = 0");
+
+        assertEquals(1, i);
+
+        KeyValueView<Tuple, Tuple> person = clusterNodes.get(0).tables()
+                .table("PUBLIC.PERSON").keyValueView();
+
+        assertEquals("John", person.get(null, Tuple.create().set("ID", 1)).stringValue("FIRSTNAME"));
+        assertEquals("Jack", person.get(null, Tuple.create().set("ID", 2)).stringValue("FIRSTNAME"));
+        assertEquals("Mike", person.get(null, Tuple.create().set("ID", 3)).stringValue("FIRSTNAME"));
+    }
+}