This is an automated email from the ASF dual-hosted git repository. korlov pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 1cbcdf7e960 IGNITE-25824 Need STORAGE_PROFILE Validation in CREATE ZONE (#6200) 1cbcdf7e960 is described below commit 1cbcdf7e96032f5e2fd397113e015a5aea22c091 Author: Mikhail Efremov <jakuten...@gmail.com> AuthorDate: Mon Jul 14 15:30:15 2025 +0600 IGNITE-25824 Need STORAGE_PROFILE Validation in CREATE ZONE (#6200) --- modules/sql-engine/build.gradle | 2 +- .../internal/sql/api/ItSqlCreateZoneTest.java | 87 ++++++++++ .../internal/sql/engine/ItCreateTableDdlTest.java | 29 +++- .../internal/sql/engine/SqlQueryProcessor.java | 9 +- .../sql/engine/prepare/PrepareServiceImpl.java | 6 +- .../ddl/ClusterWideStorageProfileValidator.java | 88 ++++++++++ .../prepare/ddl/DdlSqlToCommandConverter.java | 18 +- .../prepare/ddl/StorageProfileValidator.java | 34 ++++ .../sql/engine/exec/ExecutionServiceImplTest.java | 2 +- .../sql/engine/framework/TestBuilders.java | 2 +- .../ddl/AbstractDdlSqlToCommandConverterTest.java | 4 +- .../prepare/ddl/DdlSqlToCommandConverterTest.java | 6 + .../DistributionZoneSqlToCommandConverterTest.java | 184 +++++++++++++++++---- 13 files changed, 430 insertions(+), 41 deletions(-) diff --git a/modules/sql-engine/build.gradle b/modules/sql-engine/build.gradle index 31d060e12f4..e7ea8ed53ee 100644 --- a/modules/sql-engine/build.gradle +++ b/modules/sql-engine/build.gradle @@ -123,7 +123,7 @@ dependencies { integrationTestImplementation libs.awaitility integrationTestImplementation libs.netty.handler integrationTestImplementation project(':ignite-api') - integrationTestImplementation project(':ignite-schema') + integrationTestImplementation project(':ignite-metastorage') integrationTestImplementation project(':ignite-catalog') integrationTestImplementation project(':ignite-transactions') integrationTestImplementation project(':ignite-storage-api') diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlCreateZoneTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlCreateZoneTest.java new file mode 100644 index 00000000000..b3cf44ad20e --- /dev/null +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlCreateZoneTest.java @@ -0,0 +1,87 @@ +/* + * 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.sql.api; + +import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_AIMEM_PROFILE_NAME; +import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_AIPERSIST_PROFILE_NAME; +import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_ROCKSDB_PROFILE_NAME; +import static org.apache.ignite.internal.TestDefaultProfilesNames.DEFAULT_TEST_PROFILE_NAME; +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCode; +import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import org.apache.ignite.internal.ClusterPerTestIntegrationTest; +import org.apache.ignite.sql.SqlException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@Timeout(60) +class ItSqlCreateZoneTest extends ClusterPerTestIntegrationTest { + private static final String ZONE_MANE = "test_zone"; + private static final String NOT_EXISTED_PROFILE_NAME = "not-existed-profile"; + private static final String EXTRA_PROFILE_NAME = "extra-profile"; + /** Nodes bootstrap configuration pattern. */ + private static final String NODE_BOOTSTRAP_CFG_TEMPLATE_WITH_EXTRA_PROFILE = "ignite {\n" + + " network: {\n" + + " port: {},\n" + + " nodeFinder.netClusterNodes: [ {} ]\n" + + " },\n" + + " storage.profiles: {" + + " " + DEFAULT_TEST_PROFILE_NAME + ".engine: test, " + + " " + DEFAULT_AIPERSIST_PROFILE_NAME + ".engine: aipersist, " + + " " + DEFAULT_AIMEM_PROFILE_NAME + ".engine: aimem, " + + " " + EXTRA_PROFILE_NAME + ".engine: aipersist, " + + " " + DEFAULT_ROCKSDB_PROFILE_NAME + ".engine: rocksdb" + + " },\n" + + " clientConnector.port: {},\n" + + " rest.port: {},\n" + + " failureHandler.dumpThreadsOnFailure: false\n" + + "}"; + + @Override + protected int initialNodes() { + return 1; + } + + @Test + void testCreateZoneSucceedWithCorrectStorageProfileOnSameNode() { + assertDoesNotThrow(() -> createZoneQuery(0, "default")); + } + + @Test + void testCreateZoneSucceedWithCorrectStorageProfileOnDifferentNode() { + cluster.startNode(1, NODE_BOOTSTRAP_CFG_TEMPLATE_WITH_EXTRA_PROFILE); + assertDoesNotThrow(() -> createZoneQuery(0, EXTRA_PROFILE_NAME)); + } + + @Test + void testCreateZoneFailedWithoutCorrectStorageProfileInCluster() { + assertThrowsWithCode( + SqlException.class, + STMT_VALIDATION_ERR, + () -> createZoneQuery(0, NOT_EXISTED_PROFILE_NAME), + "Some storage profiles don't exist [missedProfileNames=[" + NOT_EXISTED_PROFILE_NAME + "]]." + ); + } + + private List<List<Object>> createZoneQuery(int nodeIdx, String storageProfile) { + return executeSql(nodeIdx, format("CREATE ZONE IF NOT EXISTS {} STORAGE PROFILES ['{}']", ZONE_MANE, storageProfile)); + } +} diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java index 90a721aa13e..79b6f0b2613 100644 --- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java +++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItCreateTableDdlTest.java @@ -31,7 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -763,12 +763,33 @@ public class ItCreateTableDdlTest extends BaseSqlIntegrationTest { @Test public void creatingTableOnZoneReferencingNonExistingProfile() { + String zoneName = "test_zone"; String tableName = "test_table"; + String nonExistingProfileName = "no-such-profile"; - sql("CREATE ZONE test_zone STORAGE PROFILES ['no-such-profile']"); - sql("CREATE TABLE " + tableName + " (id INT PRIMARY KEY, val INT) ZONE test_zone"); + // Try to create zone with not existed storage profile. + assertThrowsSqlException( + STMT_VALIDATION_ERR, + "Some storage profiles don't exist [missedProfileNames=[" + nonExistingProfileName + "]].", + () -> sql("CREATE ZONE \"" + zoneName + "\" STORAGE PROFILES ['" + nonExistingProfileName + "']") + ); + + // Check that the zone wasn't created and table creation fails with zone not found reason. + assertThrowsSqlException( + STMT_VALIDATION_ERR, + "Distribution zone with name '" + zoneName + "' not found.", + () -> sql("CREATE TABLE " + tableName + " (id INT PRIMARY KEY, val INT) ZONE \"" + zoneName + "\"") + ); + + // Try to create table with default zone and wrong storage profile. + assertThrowsSqlException( + STMT_VALIDATION_ERR, + "Zone with name 'Default' does not contain table's storage profile [storageProfile='" + nonExistingProfileName + "'].", + () -> sql("CREATE TABLE " + tableName + " (id INT PRIMARY KEY, val INT) STORAGE PROFILE '" + nonExistingProfileName + "'") + ); + // Verify that there still no the desired table. Table table = CLUSTER.aliveNode().tables().table(tableName); - assertThat(table, is(notNullValue())); + assertThat(table, is(nullValue())); } } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java index 7fc5a8ba4c5..f2e11594072 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java @@ -85,6 +85,8 @@ import org.apache.ignite.internal.sql.engine.prepare.PrepareService; import org.apache.ignite.internal.sql.engine.prepare.PrepareServiceImpl; import org.apache.ignite.internal.sql.engine.prepare.QueryMetadata; import org.apache.ignite.internal.sql.engine.prepare.QueryPlan; +import org.apache.ignite.internal.sql.engine.prepare.ddl.ClusterWideStorageProfileValidator; +import org.apache.ignite.internal.sql.engine.prepare.ddl.DdlSqlToCommandConverter; import org.apache.ignite.internal.sql.engine.prepare.pruning.PartitionPrunerImpl; import org.apache.ignite.internal.sql.engine.schema.SqlSchemaManager; import org.apache.ignite.internal.sql.engine.schema.SqlSchemaManagerImpl; @@ -281,6 +283,10 @@ public class SqlQueryProcessor implements QueryProcessor, SystemViewProvider { metricManager.registerSource(sqlQueryMetricSource); metricManager.enable(sqlQueryMetricSource); + var storageProfileValidator = new ClusterWideStorageProfileValidator(logicalTopologyService); + + var ddlSqlToCommandConverter = new DdlSqlToCommandConverter(storageProfileValidator); + var prepareSvc = registerService(PrepareServiceImpl.create( nodeName, CACHE_FACTORY, @@ -288,7 +294,8 @@ public class SqlQueryProcessor implements QueryProcessor, SystemViewProvider { metricManager, clusterCfg, nodeCfg, - sqlSchemaManager + sqlSchemaManager, + ddlSqlToCommandConverter )); var msgSrvc = registerService(new MessageServiceImpl( diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java index 7db2b2273be..21ac08029f3 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java @@ -144,6 +144,7 @@ public class PrepareServiceImpl implements PrepareService { * @param clusterCfg Cluster SQL configuration. * @param nodeCfg Node SQL configuration. * @param schemaManager Schema manager to use on validation phase to bind identifiers in AST with particular schema objects. + * @param ddlSqlToCommandConverter Converter from SQL DDL operators to catalog commands. */ public static PrepareServiceImpl create( String nodeName, @@ -152,13 +153,14 @@ public class PrepareServiceImpl implements PrepareService { MetricManager metricManager, SqlDistributedConfiguration clusterCfg, SqlLocalConfiguration nodeCfg, - SqlSchemaManager schemaManager + SqlSchemaManager schemaManager, + DdlSqlToCommandConverter ddlSqlToCommandConverter ) { return new PrepareServiceImpl( nodeName, clusterCfg.planner().estimatedNumberOfQueries().value(), cacheFactory, - new DdlSqlToCommandConverter(), + ddlSqlToCommandConverter, clusterCfg.planner().maxPlanningTimeMillis().value(), nodeCfg.planner().threadCount().value(), metricManager, diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ClusterWideStorageProfileValidator.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ClusterWideStorageProfileValidator.java new file mode 100644 index 00000000000..0f522cac493 --- /dev/null +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ClusterWideStorageProfileValidator.java @@ -0,0 +1,88 @@ +/* + * 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.sql.engine.prepare.ddl; + +import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; +import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalNode; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyService; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot; +import org.apache.ignite.sql.SqlException; + +/** + * Storage profile names validator that checks presence of given to validate profile names across cluster. + */ +public class ClusterWideStorageProfileValidator implements StorageProfileValidator { + private final LogicalTopologyService logicalTopologyService; + + public ClusterWideStorageProfileValidator(LogicalTopologyService logicalTopologyService) { + this.logicalTopologyService = logicalTopologyService; + } + + @Override + public void validate(Collection<String> storageProfiles) { + LogicalTopologySnapshot localLogicalTopologySnapshot = logicalTopologyService.localLogicalTopology(); + + Set<String> missedStorageProfileNames = findStorageProfileNotPresentedInLogicalTopologySnapshot( + storageProfiles, + localLogicalTopologySnapshot + ); + + if (!missedStorageProfileNames.isEmpty()) { + throw new SqlException(STMT_VALIDATION_ERR, format( + "Some storage profiles don't exist [missedProfileNames={}].", + missedStorageProfileNames + )); + } + } + + + private static Set<String> findStorageProfileNotPresentedInLogicalTopologySnapshot( + Collection<String> storageProfiles, + LogicalTopologySnapshot snapshot + ) { + Set<String> topologyWideProfiles = extractStorageProfileNamesFromLogicalTopologySnapshot(snapshot); + + Set<String> missedProfiles = new HashSet<>(); + + for (String profileName : storageProfiles) { + if (!topologyWideProfiles.contains(profileName)) { + missedProfiles.add(profileName); + } + } + + return missedProfiles; + } + + private static Set<String> extractStorageProfileNamesFromLogicalTopologySnapshot(LogicalTopologySnapshot snapshot) { + Set<LogicalNode> logicalNodes = snapshot.nodes(); + + // Assume default persistent + rocks + aimem profiles on each node in average. + Set<String> topologyWideProfiles = new HashSet<>(logicalNodes.size() * 3); + + for (LogicalNode logicalNode : logicalNodes) { + topologyWideProfiles.addAll(logicalNode.storageProfiles()); + } + + return topologyWideProfiles; + } +} diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java index 735e0521ccf..548be018b93 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java @@ -162,10 +162,15 @@ public class DdlSqlToCommandConverter { /** Zone options set. */ private final Set<String> knownZoneOptionNames; + /** Storage profiles validator. */ + private final StorageProfileValidator storageProfileValidator; + /** * Constructor. + * + * @param storageProfileValidator Storage profile names validator. */ - public DdlSqlToCommandConverter() { + public DdlSqlToCommandConverter(StorageProfileValidator storageProfileValidator) { knownZoneOptionNames = EnumSet.allOf(ZoneOptionEnum.class) .stream() .map(Enum::name) @@ -206,6 +211,8 @@ public class DdlSqlToCommandConverter { )); alterReplicasOptionInfo = new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, AlterZoneCommandBuilder::replicas); + + this.storageProfileValidator = storageProfileValidator; } /** @@ -723,11 +730,20 @@ public class DdlSqlToCommandConverter { List<StorageProfileParams> profiles = extractProfiles(createZoneNode.storageProfiles()); + Set<String> storageProfileNames = new HashSet<>(profiles.size()); + + for (StorageProfileParams profile : profiles) { + storageProfileNames.add(profile.storageProfile()); + } + + storageProfileValidator.validate(storageProfileNames); + builder.storageProfilesParams(profiles); return builder.build(); } + /** * Converts the given '{@code ALTER ZONE}' AST to the {@link AlterZoneCommand} catalog command. */ diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/StorageProfileValidator.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/StorageProfileValidator.java new file mode 100644 index 00000000000..360f274b61a --- /dev/null +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/StorageProfileValidator.java @@ -0,0 +1,34 @@ +/* + * 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.sql.engine.prepare.ddl; + +import java.util.Collection; + +/** + * Common validator for storage profile names. + */ +@FunctionalInterface +public interface StorageProfileValidator { + + /** + * Checks that provided storage profile names are existing and valid. + * + * @param storageProfiles Storage profile names to check. + */ + void validate(Collection<String> storageProfiles); +} diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java index 9d2d4925724..6d01feb0c41 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java @@ -241,7 +241,7 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { } private void setupCluster(CacheFactory mappingCacheFactory, Function<String, QueryTaskExecutor> executorsFactory) { - DdlSqlToCommandConverter converter = new DdlSqlToCommandConverter(); + DdlSqlToCommandConverter converter = new DdlSqlToCommandConverter(storageProfiles -> {}); testCluster = new TestCluster(); executionServices = nodeNames.stream() diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java index 47620d6058a..a07353a4df0 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java @@ -744,7 +744,7 @@ public class TestBuilders { ConcurrentMap<String, Long> tablesSize = new ConcurrentHashMap<>(); var schemaManager = createSqlSchemaManager(catalogManager, tablesSize); var prepareService = new PrepareServiceImpl(clusterName, 0, CaffeineCacheFactory.INSTANCE, - new DdlSqlToCommandConverter(), planningTimeout, PLANNING_THREAD_COUNT, + new DdlSqlToCommandConverter(storageProfiles -> {}), planningTimeout, PLANNING_THREAD_COUNT, new NoOpMetricManager(), schemaManager); Map<String, List<String>> systemViewsByNode = new HashMap<>(); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/AbstractDdlSqlToCommandConverterTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/AbstractDdlSqlToCommandConverterTest.java index bf6d605b918..bebcc90f235 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/AbstractDdlSqlToCommandConverterTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/AbstractDdlSqlToCommandConverterTest.java @@ -43,9 +43,9 @@ import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest; /** * Common methods for {@link DdlSqlToCommandConverter} testing. */ -class AbstractDdlSqlToCommandConverterTest extends BaseIgniteAbstractTest { +abstract class AbstractDdlSqlToCommandConverterTest extends BaseIgniteAbstractTest { /** DDL SQL to command converter. */ - final DdlSqlToCommandConverter converter = new DdlSqlToCommandConverter(); + DdlSqlToCommandConverter converter; final Catalog catalog = mock(Catalog.class); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverterTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverterTest.java index fb020940dd5..b60ea68418d 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverterTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverterTest.java @@ -91,6 +91,7 @@ import org.hamcrest.CustomMatcher; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; @@ -107,6 +108,11 @@ import org.mockito.Mockito; public class DdlSqlToCommandConverterTest extends AbstractDdlSqlToCommandConverterTest { private static final Integer TEST_ZONE_ID = 100; + @BeforeEach + void setUp() { + converter = new DdlSqlToCommandConverter(storageProfiles -> {}); + } + @Test void testCheckDuplicates() { assertThrows( diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java index 5fb83f9a700..9a8ee625315 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java @@ -21,14 +21,20 @@ import static org.apache.ignite.internal.catalog.CatalogService.DEFAULT_STORAGE_ import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows; import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCode; +import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.calcite.sql.SqlDdl; import org.apache.calcite.sql.SqlNode; @@ -46,16 +52,19 @@ import org.apache.ignite.internal.catalog.storage.AlterZoneEntry; import org.apache.ignite.internal.catalog.storage.DropZoneEntry; import org.apache.ignite.internal.catalog.storage.NewZoneEntry; import org.apache.ignite.internal.catalog.storage.SetDefaultZoneEntry; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalNode; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyService; +import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot; +import org.apache.ignite.internal.network.ClusterNodeImpl; import org.apache.ignite.internal.partitiondistribution.DistributionAlgorithm; -import org.apache.ignite.lang.ErrorGroups.Sql; +import org.apache.ignite.network.NetworkAddress; import org.apache.ignite.sql.SqlException; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; 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; -import org.mockito.Mockito; /** * Tests the conversion of a sql zone definition to a command. @@ -78,8 +87,34 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC ZoneOptionEnum.CONSISTENCY_MODE ); - @BeforeAll - public static void setUp() { + private static final String AIPERSIST_STORAGE_PROFILE = "segmented_aipersist"; + + private static final String ROCKSDB_STORAGE_PROFILE = "lru_rocks"; + + private static final List<String> NODE_DEFAULT_STORAGE_PROFILES = List.of( + DEFAULT_STORAGE_PROFILE, + AIPERSIST_STORAGE_PROFILE, + ROCKSDB_STORAGE_PROFILE + ); + + private LogicalTopologyService logicalTopologyService; + + @BeforeEach + public void setUp() { + // Default mock + logicalTopologyService = mock(LogicalTopologyService.class); + + LogicalTopologySnapshot defaultLogicalTopologySnapshot = new LogicalTopologySnapshot( + 0, + IntStream.range(0, 2) + .mapToObj(nodeIdx -> createLocalNode(nodeIdx, NODE_DEFAULT_STORAGE_PROFILES)) + .collect(Collectors.toList()) + ); + + when(logicalTopologyService.localLogicalTopology()).thenReturn(defaultLogicalTopologySnapshot); + + converter = new DdlSqlToCommandConverter(new ClusterWideStorageProfileValidator(logicalTopologyService)); + assertThat(ZoneOptionEnum.values().length, is(NUMERIC_OPTIONS.size() + STRING_OPTIONS.size())); } @@ -169,7 +204,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC + "distribution_algorithm='rendezvous', " + "data_nodes_filter='$[?(@.region == \"US\")]', " + "data_nodes_auto_adjust=300, " - + "storage_profiles='lru_rocks , segmented_aipersist ' " + + "storage_profiles='" + ROCKSDB_STORAGE_PROFILE + " , " + AIPERSIST_STORAGE_PROFILE + " ' " : "CREATE ZONE test " + "(partitions 2, " + "replicas 5, " @@ -177,7 +212,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC + "distribution algorithm 'rendezvous', " + "nodes filter '$[?(@.region == \"US\")]', " + "auto adjust 300) " - + "storage profiles ['lru_rocks', 'segmented_aipersist '] "; + + "storage profiles ['" + ROCKSDB_STORAGE_PROFILE + "' , '" + AIPERSIST_STORAGE_PROFILE + " '] "; CatalogCommand cmd = convert(sql); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor(); @@ -192,8 +227,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC List<CatalogStorageProfileDescriptor> storageProfiles = desc.storageProfiles().profiles(); assertThat(storageProfiles, hasSize(2)); - assertThat(storageProfiles.get(0).storageProfile(), equalTo("lru_rocks")); - assertThat(storageProfiles.get(1).storageProfile(), equalTo("segmented_aipersist")); + assertThat(storageProfiles.get(0).storageProfile(), equalTo(ROCKSDB_STORAGE_PROFILE)); + assertThat(storageProfiles.get(1).storageProfile(), equalTo(AIPERSIST_STORAGE_PROFILE)); } // Check remaining options. @@ -202,11 +237,11 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC ? "CREATE ZONE test with " + "data_nodes_auto_adjust_scale_up=100, " + "data_nodes_auto_adjust_scale_down=200, " - + "storage_profiles='lru_rocks'" + + "storage_profiles='" + ROCKSDB_STORAGE_PROFILE + "'" : "CREATE ZONE test " + "(auto scale up 100, " + "auto scale down 200) " - + "storage profiles ['lru_rocks']"; + + "storage profiles ['" + ROCKSDB_STORAGE_PROFILE + "']"; CatalogCommand cmd = convert(sql); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor(); @@ -251,6 +286,86 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(desc.replicas(), equalTo(DistributionAlgorithm.ALL_REPLICAS)); } + @ParameterizedTest(name = "with syntax = {0}") + @ValueSource(booleans = {true, false}) + public void testSingleNonExistedStorageProfile(boolean withPresent) { + String nonExistedStorageProfileName = "not_existed_profile"; + + String sql = withPresent + ? "CREATE ZONE test WITH STORAGE_PROFILES='" + nonExistedStorageProfileName + "'" + : "CREATE ZONE test STORAGE PROFILES ['" + nonExistedStorageProfileName + "']"; + + expectStatementValidationError( + sql, + "Some storage profiles don't exist [missedProfileNames=[" + nonExistedStorageProfileName + "]]." + ); + } + + @ParameterizedTest(name = "with syntax = {0}") + @ValueSource(booleans = {true, false}) + public void testSeveralNonExistedStorageProfiles(boolean withPresent) { + String nonExistedStorageProfileName1 = "not_existed_profile_1"; + String nonExistedStorageProfileName2 = "not_existed_profile_2"; + + String sql = withPresent + ? "CREATE ZONE test WITH STORAGE_PROFILES='" + nonExistedStorageProfileName1 + ", " + nonExistedStorageProfileName2 + "'" + : "CREATE ZONE test STORAGE PROFILES ['" + nonExistedStorageProfileName1 + "', '" + nonExistedStorageProfileName2 + "']"; + + expectStatementValidationError( + sql, + "Some storage profiles don't exist [missedProfileNames=[" + + nonExistedStorageProfileName1 + ", " + + nonExistedStorageProfileName2 + "]]." + ); + } + + @ParameterizedTest(name = "with syntax = {0}") + @ValueSource(booleans = {true, false}) + public void testNonExistedStorageProfilesAmongExistedOnes(boolean withPresent) { + String nonExistedStorageProfileName = "not_existed_profile"; + + String sql = withPresent + ? "CREATE ZONE test WITH STORAGE_PROFILES='" + + AIPERSIST_STORAGE_PROFILE + ", " + + nonExistedStorageProfileName + ", " + + ROCKSDB_STORAGE_PROFILE + "'" + : "CREATE ZONE test STORAGE PROFILES ['" + + AIPERSIST_STORAGE_PROFILE + "', '" + + nonExistedStorageProfileName + "', '" + + ROCKSDB_STORAGE_PROFILE + "']"; + + expectStatementValidationError( + sql, + "Some storage profiles don't exist [missedProfileNames=[" + nonExistedStorageProfileName + "]]." + ); + } + + @ParameterizedTest(name = "with syntax = {0}") + @ValueSource(booleans = {true, false}) + public void testExistedStorageProfileOnDisjointProfileSetsInLogicalTopologySnapshot(boolean withPresent) throws SqlParseException { + when(logicalTopologyService.localLogicalTopology()).thenReturn(new LogicalTopologySnapshot( + 0, + List.of( + createLocalNode(0, List.of(AIPERSIST_STORAGE_PROFILE)), + createLocalNode(1, List.of(ROCKSDB_STORAGE_PROFILE)), + createLocalNode(2, List.of(DEFAULT_STORAGE_PROFILE)) + ) + )); + + String sql = withPresent + ? "CREATE ZONE test WITH STORAGE_PROFILES='" + DEFAULT_STORAGE_PROFILE + "'" + : "CREATE ZONE test STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "']"; + + CatalogCommand cmd = convert(sql); + + List<CatalogStorageProfileDescriptor> storageProfiles = invokeAndGetFirstEntry(cmd, NewZoneEntry.class) + .descriptor() + .storageProfiles() + .profiles(); + assertThat(storageProfiles, hasSize(1)); + assertThat(storageProfiles.get(0).storageProfile(), equalTo(DEFAULT_STORAGE_PROFILE)); + } + private static List<Arguments> defaultQuorum() { return List.of( Arguments.of(1, 1), @@ -345,7 +460,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(RenameZoneCommand.class)); - Mockito.when(catalog.zone("TEST")).thenReturn(mock(CatalogZoneDescriptor.class)); + when(catalog.zone("TEST")).thenReturn(mock(CatalogZoneDescriptor.class)); AlterZoneEntry entry = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class); @@ -361,7 +476,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC RenameZoneCommand zoneCmd = (RenameZoneCommand) cmd; - Mockito.when(catalog.zone("TEST")).thenReturn(mock(CatalogZoneDescriptor.class)); + when(catalog.zone("TEST")).thenReturn(mock(CatalogZoneDescriptor.class)); AlterZoneEntry entry = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class); @@ -381,10 +496,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(zoneMock.name()).thenReturn("TEST"); - Mockito.when(zoneMock.filter()).thenReturn(""); + when(zoneMock.name()).thenReturn("TEST"); + when(zoneMock.filter()).thenReturn(""); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class).descriptor(); @@ -425,10 +540,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(AlterZoneCommand.class)); CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(zoneMock.name()).thenReturn("TEST"); - Mockito.when(zoneMock.filter()).thenReturn(""); + when(zoneMock.name()).thenReturn("TEST"); + when(zoneMock.filter()).thenReturn(""); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class).descriptor(); @@ -455,10 +570,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(AlterZoneCommand.class)); CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(zoneMock.name()).thenReturn("TEST"); - Mockito.when(zoneMock.filter()).thenReturn(""); + when(zoneMock.name()).thenReturn("TEST"); + when(zoneMock.filter()).thenReturn(""); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class).descriptor(); @@ -477,10 +592,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(AlterZoneCommand.class)); CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(zoneMock.name()).thenReturn("TEST"); - Mockito.when(zoneMock.filter()).thenReturn(""); + when(zoneMock.name()).thenReturn("TEST"); + when(zoneMock.filter()).thenReturn(""); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class).descriptor(); @@ -494,7 +609,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(AlterZoneSetDefaultCommand.class)); CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); SetDefaultZoneEntry entry = invokeAndGetFirstEntry(cmd, SetDefaultZoneEntry.class); @@ -546,7 +661,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC assertThat(cmd, instanceOf(DropZoneCommand.class)); CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class); - Mockito.when(catalog.zone("TEST")).thenReturn(zoneMock); + when(catalog.zone("TEST")).thenReturn(zoneMock); DropZoneEntry entry = invokeAndGetFirstEntry(cmd, DropZoneEntry.class); @@ -676,9 +791,22 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC private void expectStatementValidationError(String sql, String errorMessageFragment) { assertThrowsWithCode( SqlException.class, - Sql.STMT_VALIDATION_ERR, + STMT_VALIDATION_ERR, () -> convert(sql), errorMessageFragment ); } + + private static LogicalNode createLocalNode(int nodeIdx, List<String> storageProfiles) { + return new LogicalNode( + new ClusterNodeImpl( + UUID.randomUUID(), + "node" + nodeIdx, + new NetworkAddress("127.0.0.1", 3344 + nodeIdx) + ), + Map.of(), + Map.of(), + storageProfiles + ); + } }