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

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


The following commit(s) were added to refs/heads/jdbc_over_thin_sql by this 
push:
     new 0ec5fb41ec6 IGNITE-26086 Ability to restrict query execution by type 
in thin client SQL API (#6383)
0ec5fb41ec6 is described below

commit 0ec5fb41ec65ae3db08297a292b24d2062ed6434
Author: Pavel Pereslegin <[email protected]>
AuthorDate: Thu Aug 14 10:24:28 2025 +0300

    IGNITE-26086 Ability to restrict query execution by type in thin client SQL 
API (#6383)
---
 .../client/proto/ProtocolBitmaskFeature.java       |  7 +-
 .../ignite/internal/client/sql/QueryModifier.java  | 90 +++++++++++++++++++++
 .../client/proto/sql/QueryModifierTest.java        | 64 +++++++++++++++
 .../ignite/client/handler/ItClientHandlerTest.java |  1 +
 .../ignite/client/handler/ClientHandlerModule.java |  3 +-
 .../handler/ClientInboundMessageHandler.java       |  4 +-
 .../handler/requests/sql/ClientSqlCommon.java      | 34 ++++++++
 .../requests/sql/ClientSqlExecuteBatchRequest.java |  2 +-
 .../requests/sql/ClientSqlExecuteRequest.java      | 11 +--
 .../sql/ClientSqlExecuteScriptRequest.java         |  2 +-
 .../handler/requests/sql/ClientSqlProperties.java  | 14 +++-
 .../handler/requests/sql/ClientSqlCommonTest.java  | 92 ++++++++++++++++++++++
 .../ignite/internal/client/TcpClientChannel.java   |  3 +-
 .../ignite/internal/client/sql/ClientSql.java      | 41 +++++++++-
 .../org/apache/ignite/client/ClientSqlTest.java    | 55 +++++++++++++
 .../org/apache/ignite/client/fakes/FakeCursor.java |  7 ++
 .../runner/app/client/ItThinClientSqlTest.java     | 81 +++++++++++++++++++
 .../ignite/internal/sql/engine/SqlQueryType.java   | 15 ++++
 18 files changed, 510 insertions(+), 16 deletions(-)

diff --git 
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
index d019b30ff7b..b123fd0fcb4 100644
--- 
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
+++ 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ProtocolBitmaskFeature.java
@@ -77,7 +77,12 @@ public enum ProtocolBitmaskFeature {
     /**
      * Direct mapping for SQL queries within explicit transactions.
      */
-    SQL_DIRECT_TX_MAPPING(10);
+    SQL_DIRECT_TX_MAPPING(10),
+
+    /**
+     * Thin SQL client supports iteration over the results of script execution.
+     */
+    SQL_MULTISTATEMENT_SUPPORT(11);
 
     private static final EnumSet<ProtocolBitmaskFeature> 
ALL_FEATURES_AS_ENUM_SET =
             EnumSet.allOf(ProtocolBitmaskFeature.class);
diff --git 
a/modules/client-common/src/main/java/org/apache/ignite/internal/client/sql/QueryModifier.java
 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/sql/QueryModifier.java
new file mode 100644
index 00000000000..60bfcc28e74
--- /dev/null
+++ 
b/modules/client-common/src/main/java/org/apache/ignite/internal/client/sql/QueryModifier.java
@@ -0,0 +1,90 @@
+/*
+ * 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.client.sql;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * Classifier of SQL queries depending on the type of result returned.
+ */
+public enum QueryModifier {
+    /** SELECT-like queries. */
+    ALLOW_ROW_SET_RESULT(0),
+
+    /** DML-like queries. */
+    ALLOW_AFFECTED_ROWS_RESULT(1),
+
+    /** DDL-like queries. */
+    ALLOW_APPLIED_RESULT(2),
+
+    /** Queries with transaction control statements. */
+    ALLOW_TX_CONTROL(3);
+
+    /** A set containing all modifiers. **/
+    public static final Set<QueryModifier> ALL = 
EnumSet.allOf(QueryModifier.class);
+
+    /** A set of modifiers that can apply to single statements. **/
+    public static final Set<QueryModifier> SINGLE_STMT_MODIFIERS = 
EnumSet.complementOf(EnumSet.of(ALLOW_TX_CONTROL));
+
+    private static final QueryModifier[] VALS = new 
QueryModifier[values().length];
+
+    static {
+        for (QueryModifier type : values()) {
+            assert VALS[type.id] == null : "Found duplicate id " + type.id;
+
+            VALS[type.id] = type;
+        }
+    }
+
+    private final int id;
+
+    QueryModifier(int id) {
+        this.id = id;
+    }
+
+    /** Packs a set of modifiers. */
+    public static byte pack(Set<QueryModifier> modifiers) {
+        assert VALS.length < 8 : "Packing more than 7 values is not supported";
+
+        int result = 0;
+
+        for (QueryModifier modifier : modifiers) {
+            result = result | 1 << modifier.id;
+        }
+
+        return (byte) result;
+    }
+
+    /** Unpacks a set of modifiers. */
+    public static Set<QueryModifier> unpack(byte data) {
+        assert VALS.length < 8 : "Unpacking more than 7 values is not 
supported";
+
+        Set<QueryModifier> modifiers = EnumSet.noneOf(QueryModifier.class);
+
+        for (QueryModifier modifier : VALS) {
+            int target = 1 << modifier.id;
+
+            if ((target & data) == target) {
+                modifiers.add(modifier);
+            }
+        }
+
+        return modifiers;
+    }
+}
diff --git 
a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/sql/QueryModifierTest.java
 
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/sql/QueryModifierTest.java
new file mode 100644
index 00000000000..443f604e5d3
--- /dev/null
+++ 
b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/sql/QueryModifierTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.client.proto.sql;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.client.sql.QueryModifier;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link QueryModifier}.
+ */
+public class QueryModifierTest {
+    @ParameterizedTest
+    @EnumSource(QueryModifier.class)
+    public void testPackingUnpackingAllModifiers(QueryModifier modifier) {
+        Set<QueryModifier> result = 
QueryModifier.unpack(QueryModifier.pack(EnumSet.of(modifier)));
+
+        assertThat(result, hasSize(1));
+        assertThat(result.iterator().next(), is(modifier));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testPackingUnpackingArgs")
+    public void testPackingUnpacking(Set<QueryModifier> modifiers) {
+        byte resultByte = QueryModifier.pack(modifiers);
+        Set<QueryModifier> result = QueryModifier.unpack(resultByte);
+
+        assertEquals(modifiers, result);
+    }
+
+    private static Stream<Arguments> testPackingUnpackingArgs() {
+        return Stream.of(Arguments.of(EnumSet.noneOf(QueryModifier.class)),
+                Arguments.of(Set.of(QueryModifier.ALLOW_ROW_SET_RESULT, 
QueryModifier.ALLOW_AFFECTED_ROWS_RESULT)),
+                Arguments.of(Set.of(QueryModifier.ALLOW_AFFECTED_ROWS_RESULT, 
QueryModifier.ALLOW_APPLIED_RESULT)),
+                Arguments.of(Set.of(QueryModifier.ALLOW_APPLIED_RESULT, 
QueryModifier.ALLOW_TX_CONTROL)),
+                Arguments.of(Set.of(QueryModifier.ALLOW_TX_CONTROL, 
QueryModifier.ALLOW_ROW_SET_RESULT)),
+                Arguments.of(QueryModifier.ALL));
+    }
+}
diff --git 
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
 
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
index 5f8d7662f00..883e0687ea8 100644
--- 
a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
+++ 
b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/ItClientHandlerTest.java
@@ -545,6 +545,7 @@ public class ItClientHandlerTest extends 
BaseIgniteAbstractTest {
             expected.set(8);
             expected.set(9);
             expected.set(10);
+            expected.set(11);
             assertEquals(expected, supportedFeatures);
 
             var extensionsLen = unpacker.unpackInt();
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
index 9af9a4f672d..87dc5ff650b 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientHandlerModule.java
@@ -95,7 +95,8 @@ public class ClientHandlerModule implements IgniteComponent, 
PlatformComputeTran
             ProtocolBitmaskFeature.TX_PIGGYBACK,
             ProtocolBitmaskFeature.TX_ALLOW_NOOP_ENLIST,
             ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS,
-            ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING
+            ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING,
+            ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT
     ));
 
     /** Connection id generator.
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
index 0851d81dbd1..6548b15d787 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.client.handler;
 
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING;
+import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.STREAMER_RECEIVER_EXECUTION_OPTIONS;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_ALLOW_NOOP_ENLIST;
@@ -943,7 +944,8 @@ public class ClientInboundMessageHandler
                 return ClientSqlExecuteRequest.process(
                         partitionOperationsExecutor, in, requestId, 
cancelHandles, queryProcessor, resources, metrics, tsTracker,
                         clientContext.hasFeature(SQL_PARTITION_AWARENESS), 
clientContext.hasFeature(SQL_DIRECT_TX_MAPPING), txManager,
-                        clockService, notificationSender(requestId), 
resolveCurrentUsername()
+                        clockService, notificationSender(requestId), 
resolveCurrentUsername(),
+                        clientContext.hasFeature(SQL_MULTISTATEMENT_SUPPORT)
                 );
 
             case ClientOp.OPERATION_CANCEL:
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java
index 7a00cfe7ebc..dc251f2bf58 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommon.java
@@ -17,11 +17,16 @@
 
 package org.apache.ignite.client.handler.requests.sql;
 
+import java.util.Collection;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.sql.QueryModifier;
+import org.apache.ignite.internal.sql.engine.SqlQueryType;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnMetadata.ColumnOrigin;
 import org.apache.ignite.sql.ResultSetMetadata;
@@ -196,4 +201,33 @@ class ClientSqlCommon {
             }
         }
     }
+
+    static Set<SqlQueryType> 
convertQueryModifierToQueryType(Collection<QueryModifier> queryModifiers) {
+        EnumSet<SqlQueryType> queryTypes = EnumSet.noneOf(SqlQueryType.class);
+
+        for (QueryModifier queryModifier : queryModifiers) {
+            switch (queryModifier) {
+                case ALLOW_ROW_SET_RESULT:
+                    queryTypes.addAll(SqlQueryType.HAS_ROW_SET_TYPES);
+                    break;
+
+                case ALLOW_AFFECTED_ROWS_RESULT:
+                    
queryTypes.addAll(SqlQueryType.RETURNS_AFFECTED_ROWS_TYPES);
+                    break;
+
+                case ALLOW_APPLIED_RESULT:
+                    queryTypes.addAll(SqlQueryType.SUPPORT_WAS_APPLIED_TYPES);
+                    break;
+
+                case ALLOW_TX_CONTROL:
+                    queryTypes.add(SqlQueryType.TX_CONTROL);
+                    break;
+
+                default:
+                    throw new IllegalArgumentException("Unexpected modifier " 
+ queryModifier);
+            }
+        }
+
+        return queryTypes;
+    }
 }
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteBatchRequest.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteBatchRequest.java
index 5d00b67ac9f..fd7429b568b 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteBatchRequest.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteBatchRequest.java
@@ -66,7 +66,7 @@ public class ClientSqlExecuteBatchRequest {
         cancelHandleMap.put(requestId, cancelHandle);
 
         InternalTransaction tx = readTx(in, tsTracker, resources, null, null, 
null);
-        ClientSqlProperties props = new ClientSqlProperties(in);
+        ClientSqlProperties props = new ClientSqlProperties(in, false);
         String statement = in.unpackString();
         BatchedArguments arguments = readArgs(in);
 
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java
index 6fe275e5e8c..89bfbd7bc07 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteRequest.java
@@ -42,7 +42,6 @@ import 
org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.internal.sql.api.AsyncResultSetImpl;
 import org.apache.ignite.internal.sql.engine.QueryProcessor;
 import org.apache.ignite.internal.sql.engine.SqlProperties;
-import org.apache.ignite.internal.sql.engine.SqlQueryType;
 import 
org.apache.ignite.internal.sql.engine.prepare.partitionawareness.PartitionAwarenessMetadata;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.tx.TxManager;
@@ -97,7 +96,8 @@ public class ClientSqlExecuteRequest {
             TxManager txManager,
             ClockService clockService,
             NotificationSender notificationSender,
-            @Nullable String username
+            @Nullable String username,
+            boolean sqlMultistatementsSupported
     ) {
         CancelHandle cancelHandle = CancelHandle.create();
         cancelHandles.put(requestId, cancelHandle);
@@ -108,7 +108,7 @@ public class ClientSqlExecuteRequest {
 
         long[] resIdHolder = {0};
         InternalTransaction tx = readTx(in, timestampTracker, resources, 
txManager, notificationSender, resIdHolder);
-        ClientSqlProperties props = new ClientSqlProperties(in);
+        ClientSqlProperties props = new ClientSqlProperties(in, 
sqlMultistatementsSupported);
         String statement = in.unpackString();
         Object[] arguments = readArgsNotNull(in);
 
@@ -247,11 +247,8 @@ public class ClientSqlExecuteRequest {
             @Nullable Object... arguments
     ) {
         try {
-            SqlProperties properties = new SqlProperties(props)
-                    .allowedQueryTypes(SqlQueryType.SINGLE_STMT_TYPES);
-
             CompletableFuture<AsyncResultSetImpl<SqlRow>> fut = 
qryProc.queryAsync(
-                        properties,
+                        props,
                         timestampTracker,
                         (InternalTransaction) transaction,
                         token,
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteScriptRequest.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteScriptRequest.java
index 23bfc2fdb1e..34160621ca0 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteScriptRequest.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlExecuteScriptRequest.java
@@ -59,7 +59,7 @@ public class ClientSqlExecuteScriptRequest {
         CancelHandle cancelHandle = CancelHandle.create();
         cancelHandleMap.put(requestId, cancelHandle);
 
-        ClientSqlProperties props = new ClientSqlProperties(in);
+        ClientSqlProperties props = new ClientSqlProperties(in, false);
         String script = in.unpackString();
         Object[] arguments = ClientSqlExecuteRequest.readArgsNotNull(in);
 
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java
index c6e879226c4..ad0f36e1ea4 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlProperties.java
@@ -18,7 +18,9 @@
 package org.apache.ignite.client.handler.requests.sql;
 
 import java.time.ZoneId;
+import java.util.Set;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.client.sql.QueryModifier;
 import org.apache.ignite.internal.sql.SqlCommon;
 import org.apache.ignite.internal.sql.engine.SqlProperties;
 import org.apache.ignite.lang.util.IgniteNameUtils;
@@ -35,7 +37,9 @@ class ClientSqlProperties {
 
     private final @Nullable String timeZoneId;
 
-    ClientSqlProperties(ClientMessageUnpacker in) {
+    private final Set<QueryModifier> queryModifiers;
+
+    ClientSqlProperties(ClientMessageUnpacker in, boolean 
unpackQueryModifiers) {
         schema = in.tryUnpackNil() ? null : 
IgniteNameUtils.parseIdentifier(in.unpackString());
         pageSize = in.tryUnpackNil() ? SqlCommon.DEFAULT_PAGE_SIZE : 
in.unpackInt();
         queryTimeout = in.tryUnpackNil() ? 0 : in.unpackLong();
@@ -45,6 +49,10 @@ class ClientSqlProperties {
         // Skip properties - not used by SQL engine.
         in.unpackInt(); // Number of properties.
         in.readBinaryUnsafe(); // Binary tuple with properties
+
+        queryModifiers = unpackQueryModifiers
+                ? QueryModifier.unpack(in.unpackByte())
+                : QueryModifier.SINGLE_STMT_MODIFIERS;
     }
 
     public @Nullable String schema() {
@@ -64,7 +72,9 @@ class ClientSqlProperties {
     }
 
     SqlProperties toSqlProps() {
-        SqlProperties sqlProperties = new 
SqlProperties().queryTimeout(queryTimeout);
+        SqlProperties sqlProperties = new SqlProperties()
+                .queryTimeout(queryTimeout)
+                
.allowedQueryTypes(ClientSqlCommon.convertQueryModifierToQueryType(queryModifiers));
 
         if (schema != null) {
             sqlProperties.defaultSchema(schema);
diff --git 
a/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommonTest.java
 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommonTest.java
new file mode 100644
index 00000000000..da7041d4baa
--- /dev/null
+++ 
b/modules/client-handler/src/test/java/org/apache/ignite/client/handler/requests/sql/ClientSqlCommonTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.client.sql.QueryModifier;
+import org.apache.ignite.internal.sql.engine.SqlQueryType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+/**
+ * Tests for {@link ClientSqlCommonTest}.
+ */
+public class ClientSqlCommonTest {
+    @ParameterizedTest
+    @EnumSource(QueryModifier.class)
+    void testConvertQueryModifierToQueryType(QueryModifier type) {
+        Set<SqlQueryType> sqlQueryTypes = 
ClientSqlCommon.convertQueryModifierToQueryType(Set.of(type));
+
+        assertFalse(sqlQueryTypes.isEmpty());
+
+        sqlQueryTypes.forEach(sqlQueryType -> {
+            switch (type) {
+                case ALLOW_ROW_SET_RESULT:
+                    assertTrue(sqlQueryType.hasRowSet());
+                    break;
+
+                case ALLOW_AFFECTED_ROWS_RESULT:
+                    assertTrue(sqlQueryType.returnsAffectedRows());
+                    break;
+
+                case ALLOW_APPLIED_RESULT:
+                    assertTrue(sqlQueryType.supportsWasApplied());
+                    break;
+
+                case ALLOW_TX_CONTROL:
+                    assertFalse(sqlQueryType.supportsIndependentExecution());
+                    break;
+
+                default:
+                    fail("Unsupported type " + type);
+            }
+        });
+    }
+
+    @Test
+    void testAllQueryTypesCoveredByQueryModifiers() {
+        Set<SqlQueryType> sqlQueryTypesFromModifiers = 
EnumSet.noneOf(SqlQueryType.class);
+
+        for (QueryModifier modifier : QueryModifier.values()) {
+            Set<SqlQueryType> queryTypes = 
ClientSqlCommon.convertQueryModifierToQueryType(Set.of(modifier));
+
+            for (SqlQueryType queryType : queryTypes) {
+                boolean added = sqlQueryTypesFromModifiers.add(queryType);
+
+                assertTrue(added, "Duplicate type: " + queryType);
+            }
+        }
+
+        Set<SqlQueryType> allQueryTypes = 
Arrays.stream(SqlQueryType.values()).collect(Collectors.toSet());
+
+        allQueryTypes.removeAll(sqlQueryTypesFromModifiers);
+
+        assertThat(allQueryTypes, is(empty()));
+    }
+}
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index bca4fb06bc3..9a5fa6705c0 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -92,7 +92,8 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             ProtocolBitmaskFeature.TX_PIGGYBACK,
             ProtocolBitmaskFeature.TX_ALLOW_NOOP_ENLIST,
             ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS,
-            ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING
+            ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING,
+            ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT
     ));
 
     /** Minimum supported heartbeat interval. */
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
index b41861518c1..9d9e01f875b 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.client.sql;
 
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_DIRECT_TX_MAPPING;
+import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_MULTISTATEMENT_SUPPORT;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.SQL_PARTITION_AWARENESS;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_DELAYED_ACKS;
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_DIRECT_MAPPING;
@@ -31,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.TimeUnit;
@@ -287,6 +289,38 @@ public class ClientSql implements IgniteSql {
             @Nullable CancellationToken cancellationToken,
             Statement statement,
             @Nullable Object... arguments) {
+        return executeAsyncInternal(
+                transaction,
+                mapper,
+                cancellationToken,
+                QueryModifier.SINGLE_STMT_MODIFIERS,
+                statement,
+                arguments
+        );
+    }
+
+    /**
+     * Executes SQL statement in an asynchronous way.
+     *
+     * <p>Note: This method isn't part of the public API, it is used to 
execute only specific types of queries.
+     *
+     * @param transaction Transaction to execute the statement within or 
{@code null}.
+     * @param cancellationToken Cancellation token or {@code null}.
+     * @param mapper Mapper that defines the row type and the way to map 
columns to the type members. See {@link Mapper#of}.
+     * @param statement SQL statement to execute.
+     * @param queryModifiers Query modifiers.
+     * @param arguments Arguments for the statement.
+     * @param <T> A type of object contained in result set.
+     * @return Operation future.
+     */
+    public <T> CompletableFuture<AsyncResultSet<T>> executeAsyncInternal(
+            @Nullable Transaction transaction,
+            @Nullable Mapper<T> mapper,
+            @Nullable CancellationToken cancellationToken,
+            Set<QueryModifier> queryModifiers,
+            Statement statement,
+            @Nullable Object... arguments
+    ) {
         Objects.requireNonNull(statement);
 
         PartitionMappingProvider mappingProvider = 
mappingProviderCache.getIfPresent(new PaCacheKey(statement));
@@ -320,7 +354,7 @@ public class ClientSql implements IgniteSql {
 
         return txStartFut.thenCompose(tx -> ch.serviceAsync(
                 ClientOp.SQL_EXEC,
-                payloadWriter(ctx, transaction, cancellationToken, statement, 
arguments, shouldTrackOperation),
+                payloadWriter(ctx, transaction, cancellationToken, 
queryModifiers, statement, arguments, shouldTrackOperation),
                 payloadReader(ctx, mapper, tx, statement),
                 () -> DirectTxUtils.resolveChannel(ctx, ch, 
shouldTrackOperation, tx, mapping),
                 null,
@@ -381,6 +415,7 @@ public class ClientSql implements IgniteSql {
             WriteContext ctx,
             @Nullable Transaction transaction,
             @Nullable CancellationToken cancellationToken,
+            Set<QueryModifier> queryModifiers,
             Statement statement,
             @Nullable Object[] arguments,
             boolean requestAck
@@ -401,6 +436,10 @@ public class ClientSql implements IgniteSql {
 
             packProperties(w, null);
 
+            if 
(w.clientChannel().protocolContext().isFeatureSupported(SQL_MULTISTATEMENT_SUPPORT))
 {
+                w.out().packByte(QueryModifier.pack(queryModifiers));
+            }
+
             w.out().packString(statement.query());
 
             w.out().packObjectArrayAsBinaryTuple(arguments);
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientSqlTest.java 
b/modules/client/src/test/java/org/apache/ignite/client/ClientSqlTest.java
index 353efe23c67..682cab4bfdc 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientSqlTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientSqlTest.java
@@ -17,8 +17,10 @@
 
 package org.apache.ignite.client;
 
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -34,8 +36,10 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.Period;
 import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -44,6 +48,7 @@ import org.apache.ignite.client.fakes.FakeIgniteTables;
 import org.apache.ignite.internal.client.sql.ClientDirectTxMode;
 import org.apache.ignite.internal.client.sql.ClientSql;
 import org.apache.ignite.internal.client.sql.PartitionMappingProvider;
+import org.apache.ignite.internal.client.sql.QueryModifier;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.sql.IgniteSql;
@@ -55,6 +60,8 @@ import org.apache.ignite.sql.async.AsyncResultSet;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 /**
@@ -262,6 +269,54 @@ public class ClientSqlTest extends AbstractClientTableTest 
{
         }
     }
 
+    @ParameterizedTest(name = "{0} => {1}")
+    @MethodSource("testQueryModifiersArgs")
+    void testQueryModifiers(QueryModifier modifier, String expectedQueryTypes) 
{
+        IgniteSql sql = client.sql();
+
+        AsyncResultSet<SqlRow> results = await(((ClientSql) 
sql).executeAsyncInternal(
+                null, null, null, Set.of(modifier), 
sql.createStatement("SELECT ALLOWED QUERY TYPES")));
+
+        assertTrue(results.hasRowSet());
+
+        SqlRow row = results.currentPage().iterator().next();
+
+        assertThat(row.stringValue(0), equalTo(expectedQueryTypes));
+    }
+
+    private static List<Arguments> testQueryModifiersArgs() {
+        List<Arguments> res = new ArrayList<>();
+
+        for (QueryModifier modifier : QueryModifier.values()) {
+            String expected;
+
+            switch (modifier) {
+                case ALLOW_ROW_SET_RESULT:
+                    expected = "EXPLAIN, QUERY";
+                    break;
+
+                case ALLOW_AFFECTED_ROWS_RESULT:
+                    expected = "DML";
+                    break;
+
+                case ALLOW_APPLIED_RESULT:
+                    expected = "DDL, KILL";
+                    break;
+
+                case ALLOW_TX_CONTROL:
+                    expected = "TX_CONTROL";
+                    break;
+
+                default:
+                    throw new IllegalArgumentException("Unexpected type: " + 
modifier);
+            }
+
+            res.add(Arguments.of(modifier, expected));
+        }
+
+        return res;
+    }
+
     private static IgniteClient createClientWithPaCacheOfSize(int cacheSize) {
         var builder = IgniteClient.builder()
                 .addresses(new String[]{"127.0.0.1:" + serverPort})
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeCursor.java 
b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeCursor.java
index f8b7c54d892..51759bb99ce 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeCursor.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeCursor.java
@@ -31,6 +31,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
 import org.apache.ignite.internal.sql.engine.InternalSqlRow;
 import org.apache.ignite.internal.sql.engine.SqlProperties;
@@ -118,6 +119,12 @@ public class FakeCursor implements 
AsyncSqlCursor<InternalSqlRow> {
             paMeta = new PartitionAwarenessMetadata(100500, new int[] {0}, new 
int[0], DirectTxMode.SUPPORTED);
             rows.add(getRow(1));
             columns.add(new FakeColumnMetadata("col1", ColumnType.INT32));
+        } else if ("SELECT ALLOWED QUERY TYPES".equals(qry)) {
+            paMeta = null;
+            String row = 
properties.allowedQueryTypes().stream().map(SqlQueryType::name).sorted()
+                    .collect(Collectors.joining(", "));
+            rows.add(getRow(row));
+            columns.add(new FakeColumnMetadata("col1", ColumnType.STRING));
         } else {
             paMeta = null;
             rows.add(getRow(1));
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientSqlTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientSqlTest.java
index 51967dccbf8..466e3df4c93 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientSqlTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientSqlTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.runner.app.client;
 
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -31,13 +32,19 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletionException;
+import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.internal.catalog.commands.CatalogUtils;
+import org.apache.ignite.internal.client.sql.ClientSql;
+import org.apache.ignite.internal.client.sql.QueryModifier;
 import org.apache.ignite.internal.security.authentication.UserDetails;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
@@ -682,6 +689,80 @@ public class ItThinClientSqlTest extends 
ItAbstractThinClientTest {
         }
     }
 
+    @Test
+    @SuppressWarnings("ThrowableNotThrown")
+    public void testSqlQueryModifiers() {
+        ClientSql sql = (ClientSql) client().sql();
+
+        Set<QueryModifier> selectType = 
EnumSet.of(QueryModifier.ALLOW_ROW_SET_RESULT);
+        Set<QueryModifier> dmlType = 
EnumSet.of(QueryModifier.ALLOW_AFFECTED_ROWS_RESULT);
+        Set<QueryModifier> ddlType = 
EnumSet.of(QueryModifier.ALLOW_APPLIED_RESULT);
+
+        Statement ddlStatement = client().sql().createStatement("CREATE TABLE 
x(id INT PRIMARY KEY)");
+        Statement dmlStatement = client().sql().createStatement("INSERT INTO x 
VALUES (1), (2), (3)");
+        Statement selectStatement = client().sql().createStatement("SELECT * 
FROM x");
+
+        BiConsumer<Statement, Set<QueryModifier>> check = (stmt, types) -> {
+            await(sql.executeAsyncInternal(
+                    null,
+                    () -> SqlRow.class,
+                    null,
+                    types,
+                    stmt
+            ));
+        };
+
+        // Incorrect modifier for DDL.
+        {
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(ddlStatement, selectType),
+                    "Invalid SQL statement type"
+            );
+
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(ddlStatement, dmlType),
+                    "Invalid SQL statement type"
+            );
+        }
+
+        // Incorrect modifier for DML.
+        {
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(dmlStatement, selectType),
+                    "Invalid SQL statement type"
+            );
+
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(dmlStatement, ddlType),
+                    "Invalid SQL statement type"
+            );
+        }
+
+        // Incorrect modifier for SELECT.
+        {
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(selectStatement, dmlType),
+                    "Invalid SQL statement type"
+            );
+
+            IgniteTestUtils.assertThrows(
+                    SqlException.class,
+                    () -> check.accept(selectStatement, ddlType),
+                    "Invalid SQL statement type"
+            );
+        }
+
+        // No exception expected with correct modifiers.
+        check.accept(ddlStatement, QueryModifier.ALL);
+        check.accept(dmlStatement, QueryModifier.ALL);
+        check.accept(selectStatement, QueryModifier.ALL);
+    }
+
     private static class Pojo {
         public int num;
 
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
index 52e4ec6a696..11366d2e028 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
@@ -54,6 +54,21 @@ public enum SqlQueryType {
     /** A set of all query types. **/
     public static final Set<SqlQueryType> ALL = 
EnumSet.allOf(SqlQueryType.class);
 
+    /** A set of types that {@link #hasRowSet() has row set}. Usually 
represented by SELECT statement. */
+    public static final Set<SqlQueryType> HAS_ROW_SET_TYPES = 
Arrays.stream(values())
+            
.filter(SqlQueryType::hasRowSet).collect(Collectors.toCollection(() -> 
EnumSet.noneOf(SqlQueryType.class)));
+
+    /** A set of types that {@link #returnsAffectedRows() returns number of 
affected rows}. Represented by various DML statements. */
+    public static final Set<SqlQueryType> RETURNS_AFFECTED_ROWS_TYPES = 
Arrays.stream(values())
+            
.filter(SqlQueryType::returnsAffectedRows).collect(Collectors.toCollection(() 
-> EnumSet.noneOf(SqlQueryType.class)));
+
+    /**
+     * A set of types that {@link #supportsWasApplied() returns boolean 
indicating whether command was applied or not}. Represented by
+     * various DDL statements and operational commands.
+     */
+    public static final Set<SqlQueryType> SUPPORT_WAS_APPLIED_TYPES = 
Arrays.stream(values())
+            
.filter(SqlQueryType::supportsWasApplied).collect(Collectors.toCollection(() -> 
EnumSet.noneOf(SqlQueryType.class)));
+
     /**
      * Returns {@code true} if a parse tree of a statement of this type should 
be cached.
      * Otherwise returns {@code false}.


Reply via email to