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
commit 88aa4d316dcf9415fabd43f6f17da0de52659f37 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 | 12 +-- .../sql/ClientSqlExecuteScriptRequest.java | 2 +- .../handler/requests/sql/ClientSqlProperties.java | 15 +++- .../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, 511 insertions(+), 17 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 99ebec8a4c1..461f0e247c5 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 277655aa259..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,12 +247,8 @@ public class ClientSqlExecuteRequest { @Nullable Object... arguments ) { try { - SqlProperties properties = new SqlProperties(props) - .allowedQueryTypes(SqlQueryType.SINGLE_STMT_TYPES) - .allowMultiStatement(false); - 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..2931dd315eb 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,10 @@ class ClientSqlProperties { } SqlProperties toSqlProps() { - SqlProperties sqlProperties = new SqlProperties().queryTimeout(queryTimeout); + SqlProperties sqlProperties = new SqlProperties() + .queryTimeout(queryTimeout) + .allowedQueryTypes(ClientSqlCommon.convertQueryModifierToQueryType(queryModifiers)) + .allowMultiStatement(false); 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 ca14d6b4a75..7f63ace6266 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; @@ -32,13 +33,19 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; 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; @@ -741,6 +748,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}.
