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}.