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

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


The following commit(s) were added to refs/heads/jdbc_over_thin_sql by this 
push:
     new 757b44c08ef IGNITE-26427  Jdbc. Statement for thin client backed 
connection (#6659)
757b44c08ef is described below

commit 757b44c08ef755066b63518cd6d8d9cd5458642c
Author: Max Zhuravkov <[email protected]>
AuthorDate: Mon Oct 6 14:50:23 2025 +0300

    IGNITE-26427  Jdbc. Statement for thin client backed connection (#6659)
---
 .../ignite/internal/jdbc2/JdbcConnection2.java     |  65 +-
 .../ignite/internal/jdbc2/JdbcResultSet.java       |  50 +-
 .../ignite/internal/jdbc2/JdbcStatement2.java      | 687 +++++++++++++++++++++
 .../internal/jdbc2/JdbcResultSet2SelfTest.java     |  77 ++-
 .../internal/jdbc2/JdbcStatement2SelfTest.java     | 457 ++++++++++++++
 5 files changed, 1313 insertions(+), 23 deletions(-)

diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
index 9fa8ff80b78..1033f4fb0a7 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcConnection2.java
@@ -39,10 +39,13 @@ import java.sql.Savepoint;
 import java.sql.ShardingKey;
 import java.sql.Statement;
 import java.sql.Struct;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
 import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantLock;
 import org.apache.ignite.internal.jdbc.ConnectionProperties;
 import org.apache.ignite.internal.jdbc.JdbcDatabaseMetadata;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryEventHandler;
@@ -58,7 +61,7 @@ public class JdbcConnection2 implements Connection {
 
     private static final String CONNECTION_IS_CLOSED = "Connection is closed.";
 
-    private static final String RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED 
=
+    static final String RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED =
             "Returning auto-generated keys is not supported.";
 
     private static final String CALLABLE_FUNCTIONS_ARE_NOT_SUPPORTED =
@@ -97,12 +100,20 @@ public class JdbcConnection2 implements Connection {
     private static final String TYPES_MAPPING_IS_NOT_SUPPORTED =
             "Types mapping is not supported.";
 
-    private final JdbcDatabaseMetadata metadata;
+    private final IgniteSql igniteSql;
 
-    private String schemaName;
+    private final ReentrantLock lock = new ReentrantLock();
+
+    private final List<Statement> statements = new ArrayList<>();
+
+    private final ConnectionProperties properties;
+
+    private final JdbcDatabaseMetadata metadata;
 
     private volatile boolean closed;
 
+    private String schemaName;
+
     private int txIsolation;
 
     private boolean autoCommit;
@@ -123,10 +134,12 @@ public class JdbcConnection2 implements Connection {
             JdbcQueryEventHandler eventHandler,
             ConnectionProperties props
     ) {
+        igniteSql = client;
         autoCommit = true;
         networkTimeoutMillis = props.getConnectionTimeout();
         txIsolation = TRANSACTION_SERIALIZABLE;
         schemaName = readSchemaName(props.getSchema());
+        properties = props;
 
         //noinspection ThisEscapedInObjectConstruction
         metadata = new JdbcDatabaseMetadata(this, eventHandler, 
props.getUrl(), props.getUsername());
@@ -152,7 +165,16 @@ public class JdbcConnection2 implements Connection {
 
         checkCursorOptions(resSetType, resSetConcurrency, resSetHoldability);
 
-        throw new UnsupportedOperationException();
+        JdbcStatement2 statement = new JdbcStatement2(this, igniteSql, 
schemaName, resSetHoldability);
+
+        lock.lock();
+        try {
+            statements.add(statement);
+        } finally {
+            lock.unlock();
+        }
+
+        return statement;
     }
 
     /** {@inheritDoc} */
@@ -284,6 +306,37 @@ public class JdbcConnection2 implements Connection {
         }
 
         closed = true;
+
+        List<Exception> suppressedExceptions = null;
+
+        lock.lock();
+        try {
+            if (closed) {
+                return;
+            }
+
+            for (Statement statement : statements) {
+                try {
+                    statement.close();
+                } catch (Exception e) {
+                    if (suppressedExceptions == null) {
+                        suppressedExceptions = new ArrayList<>();
+                    }
+                    suppressedExceptions.add(e);
+                }
+            }
+
+        } finally {
+            lock.unlock();
+        }
+
+        if (suppressedExceptions != null) {
+            SQLException err = new SQLException("Exception occurred while 
closing a connection.");
+            for (Exception suppressed : suppressedExceptions) {
+                err.addSuppressed(suppressed);
+            }
+            throw err;
+        }
     }
 
     /**
@@ -686,6 +739,10 @@ public class JdbcConnection2 implements Connection {
         return iface != null && iface.isAssignableFrom(JdbcConnection2.class);
     }
 
+    ConnectionProperties properties() {
+        return properties;
+    }
+
     private static void checkCursorOptions(
             int resSetType,
             int resSetConcurrency,
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
index 85517457fa5..2e82c529869 100644
--- 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcResultSet.java
@@ -91,6 +91,12 @@ public class JdbcResultSet implements ResultSet {
 
     private final Statement statement;
 
+    private final JdbcResultSetMetadata jdbcMeta;
+
+    private final int maxRows;
+
+    private boolean closeOnCompletion;
+
     private int fetchSize;
 
     private @Nullable SqlRow currentRow;
@@ -101,15 +107,15 @@ public class JdbcResultSet implements ResultSet {
 
     private boolean wasNull;
 
-    private JdbcResultSetMetadata jdbcMeta;
-
     /**
      * Constructor.
      */
     public JdbcResultSet(
             org.apache.ignite.sql.ResultSet<SqlRow> rs,
             Statement statement,
-            Supplier<ZoneId> zoneIdSupplier
+            Supplier<ZoneId> zoneIdSupplier,
+            boolean closeOnCompletion,
+            int maxRows
     ) {
         this.rs = rs;
 
@@ -122,6 +128,30 @@ public class JdbcResultSet implements ResultSet {
         this.closed = false;
         this.wasNull = false;
         this.jdbcMeta = new JdbcResultSetMetadata(rsMetadata);
+        this.closeOnCompletion = closeOnCompletion;
+        this.maxRows = maxRows;
+    }
+
+    int updateCount() {
+        assert !isQuery() : "Should not be called on a query";
+        if (rs.wasApplied() || rs.affectedRows() == -1) {
+            return 0;
+        } else {
+            //noinspection NumericCastThatLosesPrecision
+            return (int) rs.affectedRows();
+        }
+    }
+
+    boolean isQuery() {
+        return rs.hasRowSet();
+    }
+
+    void closeStatement(boolean close) {
+        closeOnCompletion = close;
+    }
+
+    private boolean hasNext() {
+        return rs.hasNext() && (maxRows == 0 || currentPosition < maxRows);
     }
 
     @Override
@@ -129,7 +159,7 @@ public class JdbcResultSet implements ResultSet {
         ensureNotClosed();
 
         try {
-            if (!rs.hasNext()) {
+            if (!hasNext()) {
                 currentRow = null;
                 return false;
             }
@@ -154,6 +184,11 @@ public class JdbcResultSet implements ResultSet {
         } catch (Exception e) {
             Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
             throw new SQLException(cause.getMessage(), cause);
+        } finally {
+            if (closeOnCompletion) {
+                JdbcStatement2 statement2 = 
statement.unwrap(JdbcStatement2.class);
+                statement2.closeIfAllResultsClosed();
+            }
         }
     }
 
@@ -913,7 +948,7 @@ public class JdbcResultSet implements ResultSet {
     public boolean isBeforeFirst() throws SQLException {
         ensureNotClosed();
 
-        return currentRow == null && rs.hasNext();
+        return currentRow == null && hasNext();
     }
 
     /** {@inheritDoc} */
@@ -921,7 +956,7 @@ public class JdbcResultSet implements ResultSet {
     public boolean isAfterLast() throws SQLException {
         ensureNotClosed();
 
-        boolean hasNext = rs.hasNext();
+        boolean hasNext = hasNext();
         // Result set is empty
         if (currentPosition == 0 && !hasNext) {
             return false;
@@ -943,7 +978,7 @@ public class JdbcResultSet implements ResultSet {
     public boolean isLast() throws SQLException {
         ensureNotClosed();
 
-        return currentRow != null && !rs.hasNext();
+        return currentRow != null && !hasNext();
     }
 
     /** {@inheritDoc} */
@@ -2277,6 +2312,7 @@ public class JdbcResultSet implements ResultSet {
 
             // Append nano seconds according to the specified precision.
             long nanos = value.getLong(ChronoField.NANO_OF_SECOND);
+            //noinspection NumericCastThatLosesPrecision
             long scaled = nanos / (long) Math.pow(10, 9 - precision);
 
             sb.append('.');
diff --git 
a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
new file mode 100644
index 00000000000..53fefc6bd4b
--- /dev/null
+++ 
b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc2/JdbcStatement2.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.jdbc2;
+
+import static java.sql.ResultSet.CONCUR_READ_ONLY;
+import static java.sql.ResultSet.FETCH_FORWARD;
+import static java.sql.ResultSet.TYPE_FORWARD_ONLY;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLWarning;
+import java.sql.Statement;
+import java.time.ZoneId;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.internal.client.sql.ClientSql;
+import org.apache.ignite.internal.client.sql.QueryModifier;
+import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
+import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
+import org.apache.ignite.internal.sql.SyncResultSetAdapter;
+import org.apache.ignite.internal.util.ArrayUtils;
+import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.sql.SqlRow;
+import org.apache.ignite.sql.Statement.StatementBuilder;
+import org.apache.ignite.sql.async.AsyncResultSet;
+import org.apache.ignite.table.mapper.Mapper;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * {@link Statement} implementation backed by the thin client.
+ */
+public class JdbcStatement2 implements Statement {
+
+    private static final EnumSet<QueryModifier> QUERY = 
EnumSet.of(QueryModifier.ALLOW_ROW_SET_RESULT);
+
+    private static final EnumSet<QueryModifier> DML_OR_DDL = EnumSet.of(
+            QueryModifier.ALLOW_AFFECTED_ROWS_RESULT, 
QueryModifier.ALLOW_APPLIED_RESULT);
+
+    private static final String RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED 
=
+            JdbcConnection2.RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED;
+
+    private static final String LARGE_UPDATE_NOT_SUPPORTED =
+            "executeLargeUpdate not implemented";
+
+    private static final String FIELD_SIZE_LIMIT_IS_NOT_SUPPORTED =
+            "Field size limit is not supported.";
+
+    private static final String CURSOR_NAME_IS_NOT_SUPPORTED =
+            "Setting cursor name is not supported.";
+
+    private static final String MULTIPLE_OPEN_RESULTS_ARE_NOT_SUPPORTED =
+            "Multiple open results are not supported.";
+
+    private static final String POOLING_IS_NOT_SUPPORTED =
+            "Pooling is not supported.";
+
+    private static final String STATEMENT_IS_CLOSED =
+            "Statement is closed.";
+    public static final String ONLY_FORWARD_DIRECTION_IS_SUPPORTED = "Only 
forward direction is supported.";
+
+    private final IgniteSql igniteSql;
+
+    private final Connection connection;
+
+    private final String schemaName;
+
+    private final int rsHoldability;
+
+    private volatile @Nullable JdbcResultSet resultSet;
+
+    private int queryTimeoutSeconds;
+
+    private int pageSize;
+
+    private int maxRows = 0;
+
+    private volatile boolean closed;
+
+    private boolean closeOnCompletion;
+
+    JdbcStatement2(
+            Connection connection,
+            IgniteSql igniteSql,
+            String schemaName,
+            int rsHoldability
+    ) {
+        this.connection = connection;
+        this.schemaName = schemaName;
+        this.igniteSql = igniteSql;
+        this.rsHoldability = rsHoldability;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ResultSet executeQuery(String sql) throws SQLException {
+        execute0(QUERY, Objects.requireNonNull(sql), 
ArrayUtils.OBJECT_EMPTY_ARRAY);
+
+        ResultSet rs = getResultSet();
+
+        if (rs == null) {
+            throw new SQLException("The query isn't SELECT query: " + sql, 
SqlStateCode.PARSING_EXCEPTION);
+        }
+
+        return rs;
+    }
+
+    JdbcResultSet createResultSet(org.apache.ignite.sql.ResultSet<SqlRow> 
resultSet) throws SQLException {
+        JdbcConnection2 connection2 = connection.unwrap(JdbcConnection2.class);
+        ZoneId zoneId = connection2.properties().getConnectionTimeZone();
+        return new JdbcResultSet(resultSet, this, () -> zoneId, 
closeOnCompletion, maxRows);
+    }
+
+    /**
+     * Execute the query with given parameters.
+     *
+     * @param sql Sql query.
+     * @param args Query parameters.
+     * @throws SQLException Onj error.
+     */
+    void execute0(Set<QueryModifier> queryModifiers, String sql, Object[] 
args) throws SQLException {
+        ensureNotClosed();
+
+        closeResults();
+
+        if (sql == null || sql.isEmpty()) {
+            throw new SQLException("SQL query is empty.");
+        }
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-26139 Implement 
autocommit = false
+        if (!connection.getAutoCommit()) {
+            throw new UnsupportedOperationException("Explicit transactions are 
not supported yet.");
+        }
+
+        if (sql.indexOf(';') == -1 || sql.indexOf(';') == sql.length() - 1) {
+            queryModifiers.remove(QueryModifier.ALLOW_MULTISTATEMENT);
+        }
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-26142 
multistatement.
+        if (queryModifiers.contains(QueryModifier.ALLOW_MULTISTATEMENT)) {
+            throw new UnsupportedOperationException("Multi-statements are not 
supported yet.");
+        }
+
+        StatementBuilder stmtBuilder = igniteSql.statementBuilder()
+                .query(sql)
+                .defaultSchema(schemaName);
+
+        if (queryTimeoutSeconds > 0) {
+            stmtBuilder.queryTimeout(queryTimeoutSeconds, TimeUnit.SECONDS);
+        }
+
+        if (getFetchSize() > 0) {
+            stmtBuilder.pageSize(getFetchSize());
+        }
+
+        JdbcConnection2 conn = connection.unwrap(JdbcConnection2.class);
+        ZoneId zoneId = conn.properties().getConnectionTimeZone();
+
+        org.apache.ignite.sql.Statement igniteStmt = stmtBuilder
+                .timeZoneId(zoneId)
+                .build();
+
+        ClientSql clientSql = (ClientSql) igniteSql;
+
+        AsyncResultSet<SqlRow> clientRs;
+        try {
+            clientRs = clientSql.executeAsyncInternal(null,
+                    (Mapper<SqlRow>) null,
+                    null,
+                    queryModifiers,
+                    igniteStmt,
+                    args
+            ).join();
+
+            SyncResultSetAdapter<SqlRow> syncRs = new 
SyncResultSetAdapter<>(clientRs);
+
+            resultSet = createResultSet(syncRs);
+        } catch (Exception e) {
+            Throwable cause = 
IgniteExceptionMapperUtil.mapToPublicException(e);
+            throw new SQLException(cause.getMessage(), cause);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int executeUpdate(String sql) throws SQLException {
+        Objects.requireNonNull(sql, "sql");
+
+        execute0(DML_OR_DDL, sql, ArrayUtils.OBJECT_EMPTY_ARRAY);
+
+        int rowCount = getUpdateCount();
+
+        if (isQuery()) {
+            closeResults();
+            throw new SQLException("The query is not DML statement: " + sql);
+        }
+
+        return Math.max(rowCount, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int executeUpdate(String sql, int autoGeneratedKeys) throws 
SQLException {
+        ensureNotClosed();
+
+        switch (autoGeneratedKeys) {
+            case RETURN_GENERATED_KEYS:
+                throw new 
SQLFeatureNotSupportedException((RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED));
+
+            case NO_GENERATED_KEYS:
+                return executeUpdate(sql);
+
+            default:
+                throw new SQLException("Invalid autoGeneratedKeys value");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int executeUpdate(String sql, int[] colIndexes) throws SQLException 
{
+        ensureNotClosed();
+
+        throw new 
SQLFeatureNotSupportedException(RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int executeUpdate(String sql, String[] colNames) throws 
SQLException {
+        ensureNotClosed();
+
+        throw new 
SQLFeatureNotSupportedException(RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void close() throws SQLException {
+        if (isClosed()) {
+            return;
+        }
+
+        closed = true;
+
+        closeResults();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getMaxFieldSize() throws SQLException {
+        ensureNotClosed();
+
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setMaxFieldSize(int max) throws SQLException {
+        ensureNotClosed();
+
+        if (max < 0) {
+            throw new SQLException("Invalid field limit.");
+        }
+
+        throw new 
SQLFeatureNotSupportedException(FIELD_SIZE_LIMIT_IS_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getMaxRows() throws SQLException {
+        ensureNotClosed();
+
+        return maxRows;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setMaxRows(int maxRows) throws SQLException {
+        ensureNotClosed();
+
+        if (maxRows < 0) {
+            throw new SQLException("Invalid max rows value.");
+        }
+
+        this.maxRows = maxRows;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setEscapeProcessing(boolean enable) throws SQLException {
+        ensureNotClosed();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getQueryTimeout() throws SQLException {
+        ensureNotClosed();
+
+        return queryTimeoutSeconds;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setQueryTimeout(int timeout) throws SQLException {
+        ensureNotClosed();
+
+        if (timeout < 0) {
+            throw new SQLException("Invalid timeout value.");
+        }
+
+        this.queryTimeoutSeconds = timeout;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void cancel() throws SQLException {
+        ensureNotClosed();
+
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public SQLWarning getWarnings() throws SQLException {
+        ensureNotClosed();
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearWarnings() throws SQLException {
+        ensureNotClosed();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setCursorName(String name) throws SQLException {
+        ensureNotClosed();
+
+        throw new 
SQLFeatureNotSupportedException(CURSOR_NAME_IS_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean execute(String sql) throws SQLException {
+        ensureNotClosed();
+
+        execute0(EnumSet.allOf(QueryModifier.class), 
Objects.requireNonNull(sql), ArrayUtils.OBJECT_EMPTY_ARRAY);
+
+        return isQuery();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean execute(String sql, int[] colIndexes) throws SQLException {
+        ensureNotClosed();
+
+        if (colIndexes != null && colIndexes.length > 0) {
+            throw new 
SQLFeatureNotSupportedException(RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED);
+        }
+
+        return execute(sql);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean execute(String sql, int autoGeneratedKeys) throws 
SQLException {
+        ensureNotClosed();
+
+        switch (autoGeneratedKeys) {
+            case RETURN_GENERATED_KEYS:
+                throw new 
SQLFeatureNotSupportedException(RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED);
+
+            case NO_GENERATED_KEYS:
+                return execute(sql);
+
+            default:
+                throw new SQLException("Invalid autoGeneratedKeys value.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean execute(String sql, String[] colNames) throws SQLException {
+        ensureNotClosed();
+
+        if (colNames != null && colNames.length > 0) {
+            throw new 
SQLFeatureNotSupportedException(RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED);
+        }
+
+        return execute(sql);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public @Nullable ResultSet getResultSet() throws SQLException {
+        ensureNotClosed();
+
+        return isQuery() ? resultSet : null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getUpdateCount() throws SQLException {
+        ensureNotClosed();
+
+        JdbcResultSet rs = resultSet;
+        if (rs == null || rs.isQuery()) {
+            return -1;
+        } else {
+            return rs.updateCount();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean getMoreResults() throws SQLException {
+        return getMoreResults(CLOSE_CURRENT_RESULT);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean getMoreResults(int current) throws SQLException {
+        ensureNotClosed();
+
+        switch (current) {
+            case CLOSE_CURRENT_RESULT:
+                return false;
+
+            case CLOSE_ALL_RESULTS:
+            case KEEP_CURRENT_RESULT:
+                throw new 
SQLFeatureNotSupportedException(MULTIPLE_OPEN_RESULTS_ARE_NOT_SUPPORTED);
+
+            default:
+                throw new SQLException("Invalid 'current' parameter.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long executeLargeUpdate(String sql) throws SQLException {
+        ensureNotClosed();
+
+        throw new SQLFeatureNotSupportedException(LARGE_UPDATE_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long executeLargeUpdate(String sql, int[] columnIndexes) throws 
SQLException {
+        ensureNotClosed();
+
+        throw new SQLFeatureNotSupportedException(LARGE_UPDATE_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long executeLargeUpdate(String sql, String[] columnNames) throws 
SQLException {
+        ensureNotClosed();
+
+        throw new SQLFeatureNotSupportedException(LARGE_UPDATE_NOT_SUPPORTED);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setFetchDirection(int direction) throws SQLException {
+        ensureNotClosed();
+
+        if (direction != FETCH_FORWARD) {
+            throw new 
SQLFeatureNotSupportedException(ONLY_FORWARD_DIRECTION_IS_SUPPORTED);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getFetchDirection() throws SQLException {
+        ensureNotClosed();
+
+        return FETCH_FORWARD;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setFetchSize(int fetchSize) throws SQLException {
+        ensureNotClosed();
+
+        if (fetchSize <= 0) {
+            throw new SQLException("Invalid fetch size.");
+        }
+
+        pageSize = fetchSize;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getFetchSize() throws SQLException {
+        ensureNotClosed();
+
+        return pageSize;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getResultSetConcurrency() throws SQLException {
+        ensureNotClosed();
+
+        return CONCUR_READ_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getResultSetType() throws SQLException {
+        ensureNotClosed();
+
+        return TYPE_FORWARD_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addBatch(String sql) throws SQLException {
+        ensureNotClosed();
+
+        Objects.requireNonNull(sql);
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-26143 batch 
operations
+        throw new UnsupportedOperationException("Batch operation");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearBatch() throws SQLException {
+        ensureNotClosed();
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-26143 batch 
operations
+        throw new UnsupportedOperationException("Batch operation");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int[] executeBatch() throws SQLException {
+        ensureNotClosed();
+
+        closeResults();
+
+        // TODO https://issues.apache.org/jira/browse/IGNITE-26143 batch 
operations
+        throw new UnsupportedOperationException("Batch operation");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Connection getConnection() throws SQLException {
+        ensureNotClosed();
+
+        return connection;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ResultSet getGeneratedKeys() throws SQLException {
+        ensureNotClosed();
+
+        throw new 
SQLFeatureNotSupportedException((RETURNING_AUTO_GENERATED_KEYS_IS_NOT_SUPPORTED));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getResultSetHoldability() throws SQLException {
+        ensureNotClosed();
+
+        return rsHoldability;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isClosed() throws SQLException {
+        return closed;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setPoolable(boolean poolable) throws SQLException {
+        ensureNotClosed();
+
+        if (poolable) {
+            throw new 
SQLFeatureNotSupportedException(POOLING_IS_NOT_SUPPORTED);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isPoolable() throws SQLException {
+        ensureNotClosed();
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void closeOnCompletion() throws SQLException {
+        ensureNotClosed();
+
+        closeOnCompletion = true;
+
+        JdbcResultSet rs = resultSet;
+        if (rs != null) {
+            rs.closeStatement(true);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCloseOnCompletion() throws SQLException {
+        ensureNotClosed();
+
+        return closeOnCompletion;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> T unwrap(Class<T> iface) throws SQLException {
+        if (!isWrapperFor(Objects.requireNonNull(iface))) {
+            throw new SQLException("Statement is not a wrapper for " + 
iface.getName());
+        }
+
+        return (T) this;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isWrapperFor(Class<?> iface) throws SQLException {
+        return iface != null && iface.isAssignableFrom(JdbcStatement2.class);
+    }
+
+    /**
+     * Gets the isQuery flag from the first result.
+     *
+     * @return isQuery flag.
+     */
+    protected boolean isQuery() {
+        if (resultSet == null) {
+            return false;
+        }
+        return resultSet.isQuery();
+    }
+
+    private void ensureNotClosed() throws SQLException {
+        if (isClosed()) {
+            throw new SQLException(STATEMENT_IS_CLOSED);
+        }
+    }
+
+    private void closeResults() throws SQLException {
+        JdbcResultSet rs = resultSet;
+        if (rs != null) {
+            rs.close();
+        }
+        resultSet = null;
+    }
+
+    /**
+     * Used by statement on closeOnCompletion mode.
+     */
+    void closeIfAllResultsClosed() throws SQLException {
+        if (!closeOnCompletion) {
+            return;
+        }
+
+        close();
+    }
+}
diff --git 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
index 1e9035f60e4..01bfc0cda3e 100644
--- 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
+++ 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcResultSet2SelfTest.java
@@ -66,6 +66,7 @@ import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 import org.mockito.internal.stubbing.answers.ThrowsException;
 
@@ -144,7 +145,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
             RuntimeException cause = new RuntimeException("Some error");
             when(igniteRs.hasNext()).thenThrow(cause);
 
-            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault);
+            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
 
             SQLException err = assertThrows(SQLException.class, rs::next);
             assertEquals("Some error", err.getMessage());
@@ -162,7 +163,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
             when(igniteRs.hasNext()).thenReturn(true);
             when(igniteRs.next()).thenThrow(cause);
 
-            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault);
+            ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
 
             SQLException err = assertThrows(SQLException.class, rs::next);
             assertEquals("Some error", err.getMessage());
@@ -179,27 +180,33 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
         when(igniteRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of()));
 
-        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault);
+        JdbcStatement2 statement2 = Mockito.mock(JdbcStatement2.class);
+        when(statement.unwrap(JdbcStatement2.class)).thenReturn(statement2);
+
+        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, true, 0);
 
         rs.close();
         rs.close();
 
         verify(igniteRs, times(1)).close();
         verify(igniteRs, times(1)).metadata();
-        verifyNoMoreInteractions(igniteRs);
+        verify(statement2, times(1)).closeIfAllResultsClosed();
+        verifyNoMoreInteractions(igniteRs, statement2);
     }
 
-    @Test
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
     @SuppressWarnings("unchecked")
-    public void closeExceptionIsWrapped() {
-        Statement statement = Mockito.mock(Statement.class);
+    public void closeExceptionIsWrapped(boolean closeOnCompletion) throws 
SQLException {
+        JdbcStatement2 statement = Mockito.mock(JdbcStatement2.class);
+        when(statement.unwrap(JdbcStatement2.class)).thenReturn(statement);
 
         org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
 
         RuntimeException cause = new RuntimeException("Some error");
         doAnswer(new ThrowsException(cause)).when(igniteRs).close();
 
-        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault);
+        ResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, closeOnCompletion, 0);
 
         SQLException err = assertThrows(SQLException.class, rs::close);
         assertEquals("Some error", err.getMessage());
@@ -223,7 +230,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         RuntimeException cause = new RuntimeException("Corrupted value");
         when(row.value(0)).thenThrow(cause);
 
-        JdbcResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault);
+        JdbcResultSet rs = new JdbcResultSet(igniteRs, statement, 
ZoneId::systemDefault, false, 0);
         assertTrue(rs.next());
 
         SQLException err = assertThrows(SQLException.class, () -> 
rs.getValue(1));
@@ -232,6 +239,41 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         assertSame(cause, err.getCause().getCause());
     }
 
+    @Test
+    public void maxRows() throws SQLException {
+        ColumnMetadataImpl column = new ColumnMetadataImpl("C1", 
ColumnType.INT32, 0, 0, false, null);
+        ResultSetMetadata apiMeta = new ResultSetMetadataImpl(List.of(column));
+        Statement statement = Mockito.mock(Statement.class);
+        Supplier<ZoneId> zoneIdSupplier = ZoneId::systemDefault;
+
+        List<List<Object>> rows = List.of(
+                List.of(1),
+                List.of(2),
+                List.of(3),
+                List.of(4)
+        );
+
+        try (ResultSet rs = new JdbcResultSet(new ResultSetStub(apiMeta, 
rows), statement, zoneIdSupplier, false, 3)) {
+            assertTrue(rs.next());
+            assertTrue(rs.next());
+            assertTrue(rs.next());
+            // MaxRows exceeded
+            assertFalse(rs.next());
+            assertFalse(rs.next());
+        }
+
+        // no limit
+
+        try (ResultSet rs = new JdbcResultSet(new ResultSetStub(apiMeta, 
rows), statement, zoneIdSupplier, false, 0)) {
+            assertTrue(rs.next());
+            assertTrue(rs.next());
+            assertTrue(rs.next());
+            assertTrue(rs.next());
+            // No more rows
+            assertFalse(rs.next());
+        }
+    }
+
     @Override
     protected ResultSet createResultSet(@Nullable ZoneId zoneId, 
List<ColumnDefinition> cols, List<List<Object>> rows) {
         Statement statement = Mockito.mock(Statement.class);
@@ -239,7 +281,6 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
         return createResultSet(statement, zoneId, cols, rows);
     }
 
-    @SuppressWarnings("unchecked")
     private static ResultSet createResultSet(
             Statement statement,
             @SuppressWarnings("unused")
@@ -247,6 +288,18 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
             List<ColumnDefinition> cols,
             List<List<Object>> rows
     ) {
+        return createResultSet(statement, zoneId, cols, rows, 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static ResultSet createResultSet(
+            Statement statement,
+            @SuppressWarnings("unused")
+            @Nullable ZoneId zoneId,
+            List<ColumnDefinition> cols,
+            List<List<Object>> rows,
+            int maxRows
+    ) {
 
         Supplier<ZoneId> zoneIdSupplier = () -> {
             if (zoneId != null) {
@@ -261,7 +314,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
             org.apache.ignite.sql.ResultSet<SqlRow> rs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
             when(rs.metadata()).thenReturn(null);
 
-            return new JdbcResultSet(rs, statement, zoneIdSupplier);
+            return new JdbcResultSet(rs, statement, zoneIdSupplier, false, 0);
         }
 
         List<ColumnMetadata> apiCols = new ArrayList<>();
@@ -276,7 +329,7 @@ public class JdbcResultSet2SelfTest extends 
JdbcResultSetBaseSelfTest {
 
         ResultSetMetadata apiMeta = new ResultSetMetadataImpl(apiCols);
 
-        return new JdbcResultSet(new ResultSetStub(apiMeta, rows), statement, 
zoneIdSupplier);
+        return new JdbcResultSet(new ResultSetStub(apiMeta, rows), statement, 
zoneIdSupplier, false, maxRows);
     }
 
     private static class ResultSetStub implements 
org.apache.ignite.sql.ResultSet<SqlRow> {
diff --git 
a/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
new file mode 100644
index 00000000000..086dd9843ac
--- /dev/null
+++ 
b/modules/jdbc/src/test/java/org/apache/ignite/internal/jdbc2/JdbcStatement2SelfTest.java
@@ -0,0 +1,457 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.jdbc2;
+
+import static 
org.apache.ignite.jdbc.util.JdbcTestUtils.assertThrowsSqlException;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Statement;
+import java.util.List;
+import org.apache.ignite.internal.jdbc.ConnectionProperties;
+import org.apache.ignite.internal.jdbc.ConnectionPropertiesImpl;
+import org.apache.ignite.internal.sql.ResultSetMetadataImpl;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.sql.SqlRow;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link JdbcStatement2}.
+ */
+public class JdbcStatement2SelfTest extends BaseIgniteAbstractTest {
+
+    @Test
+    public void close() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertFalse(stmt.isClosed());
+
+            stmt.close();
+            assertTrue(stmt.isClosed());
+
+            expectClosed(() -> stmt.executeQuery("SELECT 1"));
+            expectClosed(() -> stmt.executeUpdate("UPDATE t SET val=1"));
+
+            expectClosed(() -> stmt.setMaxFieldSize(100_000));
+            expectClosed(stmt::getMaxFieldSize);
+
+            expectClosed(() -> stmt.setEscapeProcessing(true));
+            expectClosed(() -> stmt.setEscapeProcessing(false));
+
+            expectClosed(() -> stmt.setQueryTimeout(-1));
+            expectClosed(() -> stmt.setQueryTimeout(1));
+            expectClosed(stmt::getQueryTimeout);
+
+            expectClosed(stmt::cancel);
+
+            expectClosed(stmt::getWarnings);
+            expectClosed(stmt::clearWarnings);
+
+            expectClosed(() -> stmt.setCursorName("C"));
+            expectClosed(() -> stmt.setCursorName(null));
+
+            expectClosed(() -> stmt.execute("SELECT 1"));
+
+            expectClosed(stmt::getResultSet);
+
+            expectClosed(stmt::getUpdateCount);
+
+            expectClosed(stmt::getMoreResults);
+
+            expectClosed(() -> 
stmt.setFetchDirection(ResultSet.FETCH_FORWARD));
+            expectClosed(() -> 
stmt.setFetchDirection(ResultSet.FETCH_REVERSE));
+            expectClosed(() -> 
stmt.setFetchDirection(ResultSet.FETCH_UNKNOWN));
+            expectClosed(stmt::getFetchDirection);
+
+            expectClosed(() -> stmt.setFetchSize(-1));
+            expectClosed(() -> stmt.setFetchSize(0));
+            expectClosed(() -> stmt.setFetchSize(1));
+            expectClosed(stmt::getFetchSize);
+
+            expectClosed(stmt::getResultSetConcurrency);
+            expectClosed(stmt::getResultSetType);
+
+            expectClosed(() -> stmt.addBatch("UPDATE t SET val=2"));
+
+            expectClosed(stmt::clearBatch);
+
+            expectClosed(stmt::executeBatch);
+
+            expectClosed(stmt::getConnection);
+
+            expectClosed(stmt::getMoreResults);
+
+            expectClosed(() -> 
stmt.getMoreResults(Statement.CLOSE_CURRENT_RESULT));
+            expectClosed(() -> 
stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
+            expectClosed(() -> 
stmt.getMoreResults(Statement.CLOSE_ALL_RESULTS));
+
+            expectClosed(stmt::getGeneratedKeys);
+
+            expectClosed(() -> stmt.executeUpdate("UPDATE t SET val=2", 
Statement.RETURN_GENERATED_KEYS));
+            expectClosed(() -> stmt.executeUpdate("UPDATE t SET val=2", 
Statement.NO_GENERATED_KEYS));
+
+            expectClosed(() -> stmt.executeUpdate("UPDATE t SET val=2", new 
int[]{0}));
+            expectClosed(() -> stmt.executeUpdate("UPDATE t SET val=2", new 
String[]{"C1"}));
+
+            expectClosed(stmt::getResultSetHoldability);
+
+            expectClosed(() -> stmt.setPoolable(true));
+            expectClosed(() -> stmt.setPoolable(false));
+            expectClosed(stmt::isPoolable);
+
+            expectClosed(stmt::closeOnCompletion);
+            expectClosed(stmt::isCloseOnCompletion);
+        }
+    }
+
+    @Test
+    public void setMaxFieldSize() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    "Field size limit is not supported.",
+                    () -> stmt.setMaxFieldSize(1)
+            );
+
+            assertEquals(0, stmt.getMaxFieldSize());
+
+            assertThrowsSqlException(
+                    SQLException.class,
+                    "Invalid field limit.",
+                    () -> stmt.setMaxFieldSize(-1)
+            );
+        }
+    }
+
+    @Test
+    public void queryTimeout() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            stmt.setQueryTimeout(2);
+            assertEquals(2, stmt.getQueryTimeout());
+
+            stmt.setQueryTimeout(0);
+            assertEquals(0, stmt.getQueryTimeout());
+
+            stmt.setQueryTimeout(4);
+            assertEquals(4, stmt.getQueryTimeout());
+
+            assertThrowsSqlException(
+                    SQLException.class,
+                    "Invalid timeout value.",
+                    () -> stmt.setQueryTimeout(-1)
+            );
+
+            // No changes
+            assertEquals(4, stmt.getQueryTimeout());
+        }
+    }
+
+    @Test
+    public void setMaxRows() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Unlimited
+            assertEquals(0, stmt.getMaxRows());
+
+            stmt.setMaxRows(1000);
+            assertEquals(1000, stmt.getMaxRows());
+
+            // Change to unlimited
+            stmt.setMaxRows(0);
+            assertEquals(0, stmt.getMaxRows());
+
+            assertThrowsSqlException(SQLException.class,
+                    "Invalid max rows value.",
+                    () -> stmt.setMaxRows(-1)
+            );
+        }
+    }
+
+    @Test
+    public void setEscapeProcessing() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Does nothing
+            stmt.setEscapeProcessing(true);
+            stmt.setEscapeProcessing(false);
+        }
+    }
+
+    @Test
+    public void warnings() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Does nothing
+            assertNull(stmt.getWarnings());
+            stmt.clearWarnings();
+        }
+    }
+
+    @Test
+    public void setCursorName() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            String error = "Setting cursor name is not supported.";
+
+            assertThrowsSqlException(SQLException.class,
+                    error,
+                    () -> stmt.setCursorName("C")
+            );
+
+            assertThrowsSqlException(SQLException.class,
+                    error,
+                    () -> stmt.setCursorName(null)
+            );
+        }
+    }
+
+    @Test
+    public void setFetchDirection() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Initial state
+            int value = ResultSet.FETCH_FORWARD;
+            assertEquals(value, stmt.getFetchDirection());
+
+            stmt.setFetchDirection(value);
+            assertEquals(value, stmt.getFetchDirection());
+
+            // Does not change anything
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    "Only forward direction is supported.",
+                    () -> stmt.setFetchDirection(ResultSet.FETCH_REVERSE)
+            );
+            assertEquals(value, stmt.getFetchDirection());
+
+            // Does not change anything
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    "Only forward direction is supported.",
+                    () -> stmt.setFetchDirection(ResultSet.FETCH_UNKNOWN)
+            );
+            assertEquals(value, stmt.getFetchDirection());
+        }
+    }
+
+    @Test
+    public void setFetchSize() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Fetch size is a hint
+            assertEquals(0, stmt.getFetchSize());
+
+            stmt.setFetchSize(1000);
+            assertEquals(1000, stmt.getFetchSize());
+
+            assertThrowsSqlException(SQLException.class,
+                    "Invalid fetch size.",
+                    () -> stmt.setFetchSize(-1)
+            );
+        }
+    }
+
+    @Test
+    public void getResultSetConcurrency() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertEquals(ResultSet.CONCUR_READ_ONLY, 
stmt.getResultSetConcurrency());
+        }
+    }
+
+    @Test
+    public void getResultSetType() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType());
+        }
+    }
+
+    @Test
+    public void getConnection() throws SQLException {
+        Connection connection = Mockito.mock(Connection.class);
+
+        try (Statement stmt = createStatement(connection)) {
+            assertSame(connection, stmt.getConnection());
+        }
+    }
+
+    @Test
+    public void getMoreResults() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // Nothing
+            assertFalse(stmt.getMoreResults(Statement.CLOSE_CURRENT_RESULT));
+
+            String error = "Multiple open results are not supported.";
+
+            assertThrowsSqlException(SQLException.class,
+                    error,
+                    () -> stmt.getMoreResults(Statement.CLOSE_ALL_RESULTS));
+
+            assertThrowsSqlException(SQLException.class,
+                    error,
+                    () -> stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT));
+        }
+    }
+
+    @Test
+    public void getGeneratedKeys() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    "Returning auto-generated keys is not supported.",
+                    stmt::getGeneratedKeys
+            );
+        }
+    }
+
+    @Test
+    public void updateWithColumns() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            String sql = "UPDATE t SET c = 1";
+            String error = "Returning auto-generated keys is not supported.";
+
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeUpdate(sql, 
Statement.RETURN_GENERATED_KEYS)
+            );
+
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeUpdate(sql, new int[]{1})
+            );
+
+            assertThrowsSqlException(
+                    SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeUpdate(sql, new String[]{"id"})
+            );
+        }
+    }
+
+    @Test
+    public void getResultSetHoldability() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            // any value
+            assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, 
stmt.getResultSetHoldability());
+        }
+    }
+
+    @Test
+    public void setPoolable() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertFalse(stmt.isPoolable());
+
+            String error = "Pooling is not supported.";
+
+            assertThrowsSqlException(SQLException.class, error, () -> {
+                stmt.setPoolable(true);
+            });
+
+            // Nothing happens
+            stmt.setPoolable(false);
+            assertFalse(stmt.isPoolable());
+        }
+    }
+
+    @Test
+    public void closeOnCompletion() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            JdbcStatement2 jdbc = stmt.unwrap(JdbcStatement2.class);
+
+            @SuppressWarnings("unchecked")
+            org.apache.ignite.sql.ResultSet<SqlRow> igniteRs = 
Mockito.mock(org.apache.ignite.sql.ResultSet.class);
+            when(igniteRs.metadata()).thenReturn(new 
ResultSetMetadataImpl(List.of()));
+
+            {
+                ResultSet rs = jdbc.createResultSet(igniteRs);
+                rs.close();
+                assertFalse(stmt.isClosed());
+            }
+
+            stmt.closeOnCompletion();
+
+            {
+                ResultSet rs = jdbc.createResultSet(igniteRs);
+                rs.close();
+                assertTrue(stmt.isClosed());
+            }
+        }
+    }
+
+    @Test
+    public void largeMaxRows() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            assertThrows(UnsupportedOperationException.class, () -> 
stmt.setLargeMaxRows(1));
+
+            assertEquals(0, stmt.getLargeMaxRows());
+        }
+    }
+
+    @Test
+    public void largeUpdateMethods() throws SQLException {
+        try (Statement stmt = createStatement()) {
+            String error = "executeLargeUpdate not implemented";
+
+            assertThrowsSqlException(SQLFeatureNotSupportedException.class,
+                    () -> stmt.executeLargeUpdate("UPDATE t SET val=2"));
+
+            assertThrowsSqlException(SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeLargeUpdate("UPDATE t SET val=2", 
Statement.RETURN_GENERATED_KEYS));
+
+            assertThrowsSqlException(SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeLargeUpdate("UPDATE t SET val=2", 
Statement.NO_GENERATED_KEYS));
+
+            assertThrowsSqlException(SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeLargeUpdate("UPDATE t SET val=2", new 
int[]{0}));
+
+            assertThrowsSqlException(SQLFeatureNotSupportedException.class,
+                    error,
+                    () -> stmt.executeLargeUpdate("UPDATE t SET val=2", new 
String[]{"C1"}));
+        }
+    }
+
+    private static Statement createStatement() throws SQLException {
+        Connection connection = Mockito.mock(Connection.class);
+        JdbcConnection2 connection2 = Mockito.mock(JdbcConnection2.class);
+
+        ConnectionProperties properties = new ConnectionPropertiesImpl();
+
+        when(connection.unwrap(JdbcConnection2.class)).thenReturn(connection2);
+        when(connection2.properties()).thenReturn(properties);
+
+        return createStatement(connection);
+    }
+
+    private static Statement createStatement(Connection connection) {
+        IgniteSql igniteSql = Mockito.mock(IgniteSql.class);
+        return new JdbcStatement2(connection, igniteSql, "PUBLIC", 
ResultSet.HOLD_CURSORS_OVER_COMMIT);
+    }
+
+    private static void expectClosed(Executable method) {
+        assertThrowsSqlException(SQLException.class, "Statement is closed.", 
method);
+    }
+}

Reply via email to