diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java index ce1bc69c9b77d..b3cd00428d8d1 100644 --- a/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java +++ b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfiguration.java @@ -358,6 +358,9 @@ public class CacheConfiguration extends MutableConfiguration impleme /** Query entities. */ private Collection qryEntities; + /** API used to configure query entities in this configuration instance. */ + private transient QueryEntityConfigurationSource qryEntityCfgSrc; + /** Partition loss policy. */ private PartitionLossPolicy partLossPlc = DFLT_PARTITION_LOSS_POLICY; @@ -469,6 +472,7 @@ public CacheConfiguration(CompleteConfiguration cfg) { pluginCfgs = cc.getPluginConfigurations(); qryDetailMetricsSz = cc.getQueryDetailMetricsSize(); qryEntities = cc.getQueryEntities() == Collections.emptyList() ? null : cc.getQueryEntities(); + qryEntityCfgSrc = cc.qryEntityCfgSrc; qryParallelism = cc.getQueryParallelism(); readFromBackup = cc.isReadFromBackup(); rebalanceBatchSize = cc.getRebalanceBatchSize(); @@ -1933,9 +1937,13 @@ public Class[] getIndexedTypes() { *

* To expose fields of these types onto SQL level and to index them you have to use annotations * from package {@link org.apache.ignite.cache.query.annotations}. + *

+ * This method must not be used together with {@link #setQueryEntities(Collection)}. + * Repeated calls replace the indexed types, query entities and key configuration configured by the previous call. * * @param indexedTypes Key and value type pairs. * @return {@code this} for chaining. + * @throws CacheException If query entities have already been configured through {@link #setQueryEntities(Collection)}. */ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { if (F.isEmpty(indexedTypes)) @@ -1949,8 +1957,7 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { A.ensure((len & 1) == 0, "Number of indexed types is expected to be even. Refer to method javadoc for details."); - if (this.indexedTypes != null) - throw new CacheException("Indexed types can be set only once."); + checkQueryEntityConfigurationSource(QueryEntityConfigurationSource.INDEXED_TYPES); Class[] newIndexedTypes = new Class[len]; @@ -1961,8 +1968,8 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { newIndexedTypes[i] = U.box(indexedTypes[i]); } - if (qryEntities == null) - qryEntities = new ArrayList<>(); + Collection newQryEntities = new ArrayList<>(); + Collection newKeyCfgs = new ArrayList<>(); for (int i = 0; i < len; i += 2) { Class keyCls = newIndexedTypes[i]; @@ -1972,7 +1979,7 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { boolean dup = false; - for (QueryEntity entity : qryEntities) { + for (QueryEntity entity : newQryEntities) { if (Objects.equals(entity.findValueType(), newEntity.findValueType())) { dup = true; @@ -1981,7 +1988,7 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { } if (!dup) - qryEntities.add(newEntity); + newQryEntities.add(newEntity); // Set key configuration if needed. String affFieldName = BinaryUtils.affinityFieldName(keyCls); @@ -1989,31 +1996,28 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { if (affFieldName != null) { CacheKeyConfiguration newKeyCfg = new CacheKeyConfiguration(newEntity.getKeyType(), affFieldName); - if (F.isEmpty(keyCfg)) - keyCfg = new CacheKeyConfiguration[] { newKeyCfg }; - else { - boolean keyCfgDup = false; + boolean keyCfgDup = false; - for (CacheKeyConfiguration oldKeyCfg : keyCfg) { - if (Objects.equals(oldKeyCfg.getTypeName(), newKeyCfg.getTypeName())) { - keyCfgDup = true; + for (CacheKeyConfiguration oldKeyCfg : newKeyCfgs) { + if (Objects.equals(oldKeyCfg.getTypeName(), newKeyCfg.getTypeName())) { + keyCfgDup = true; - break; - } + break; } + } - if (!keyCfgDup) { - CacheKeyConfiguration[] keyCfg0 = new CacheKeyConfiguration[keyCfg.length + 1]; + if (!keyCfgDup) + newKeyCfgs.add(newKeyCfg); + } + } - System.arraycopy(keyCfg, 0, keyCfg0, 0, keyCfg.length); + this.indexedTypes = newIndexedTypes; - keyCfg0[keyCfg0.length - 1] = newKeyCfg; + qryEntities = newQryEntities; - keyCfg = keyCfg0; - } - } - } - } + keyCfg = newKeyCfgs.isEmpty() ? null : newKeyCfgs.toArray(new CacheKeyConfiguration[0]); + + qryEntityCfgSrc = QueryEntityConfigurationSource.INDEXED_TYPES; return this; } @@ -2075,42 +2079,34 @@ public CacheConfiguration setPartitionLossPolicy(PartitionLossPolicy partL /** * Sets query entities configuration. + *

+ * This method must not be used together with {@link #setIndexedTypes(Class[])}. + * Repeated calls replace the query entities configured by the previous call. * * @param qryEntities Query entities. * @return {@code this} for chaining. + * @throws CacheException If query entities have already been configured through {@link #setIndexedTypes(Class[])}. */ public CacheConfiguration setQueryEntities(Collection qryEntities) { - if (this.qryEntities == null) { - this.qryEntities = new ArrayList<>(qryEntities); - - return this; - } + checkQueryEntityConfigurationSource(QueryEntityConfigurationSource.QUERY_ENTITIES); - for (QueryEntity entity : qryEntities) { - boolean found = false; + this.qryEntities = new ArrayList<>(qryEntities); - for (QueryEntity existing : this.qryEntities) { - if (Objects.equals(entity.findValueType(), existing.findValueType())) { - found = true; - - break; - } - } - - if (!found) - this.qryEntities.add(entity); - } + qryEntityCfgSrc = QueryEntityConfigurationSource.QUERY_ENTITIES; return this; } /** - * Clear query entities. + * Clears query entities. + *

+ * Calling this method does not reset the API used to configure query entities and does not allow switching between + * {@link #setIndexedTypes(Class[])} and {@link #setQueryEntities(Collection)}. * * @return {@code this} for chaining. */ public CacheConfiguration clearQueryEntities() { - this.qryEntities = null; + qryEntities = null; return this; } @@ -2484,6 +2480,28 @@ public CacheConfiguration setIndexPath(String idxPath) { return S.toString(CacheConfiguration.class, this); } + /** */ + void replaceQueryEntities(Collection qryEntities) { + this.qryEntities = new ArrayList<>(qryEntities); + } + + /** */ + private void checkQueryEntityConfigurationSource(QueryEntityConfigurationSource src) { + if (qryEntityCfgSrc != null && qryEntityCfgSrc != src) { + throw new CacheException("Query entities can be configured either with setIndexedTypes or setQueryEntities, " + + "but not both [cacheName=" + name + ']'); + } + } + + /** API used to configure query entities. */ + private enum QueryEntityConfigurationSource { + /** */ + INDEXED_TYPES, + + /** */ + QUERY_ENTITIES + } + /** * Filter that accepts all nodes. */ diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfigurationInternalAccessor.java b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfigurationInternalAccessor.java new file mode 100644 index 0000000000000..48f34ad9b527e --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/configuration/CacheConfigurationInternalAccessor.java @@ -0,0 +1,39 @@ +/* + * 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.configuration; + +import java.util.Collection; +import org.apache.ignite.cache.QueryEntity; + +/** Provides internal access to {@link CacheConfiguration} implementation details. */ +public final class CacheConfigurationInternalAccessor { + /** */ + private CacheConfigurationInternalAccessor() { + // No-op. + } + + /** + * Replaces query entities without changing the query-entity configuration source. + * + * @param cfg Cache configuration. + * @param qryEntities Query entities. + */ + public static void replaceQueryEntities(CacheConfiguration cfg, Collection qryEntities) { + cfg.replaceQueryEntities(qryEntities); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java index 3cbaf1cf77056..022a852ddb701 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/ClusterCachesInfo.java @@ -46,6 +46,7 @@ import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.CacheConfigurationInternalAccessor; import org.apache.ignite.configuration.DataRegionConfiguration; import org.apache.ignite.configuration.DataStorageConfiguration; import org.apache.ignite.configuration.NearCacheConfiguration; @@ -1893,7 +1894,8 @@ private void initStartCachesForLocalJoin(boolean firstNode, boolean reconnect) { */ private CacheConfiguration mergeConfigurations(CacheConfiguration loc, CacheConfiguration received) { // Schema is supposed to get merged earlier. - loc.setQueryEntities(received.getQueryEntities()); + CacheConfigurationInternalAccessor.replaceQueryEntities(loc, received.getQueryEntities()); + loc.setSqlSchema(received.getSqlSchema()); loc.setSqlFunctionClasses(received.getSqlFunctionClasses()); loc.setSqlEscapeAll(received.isSqlEscapeAll()); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java index 7f0b4d36bd11a..cdeb137d35cf3 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheUtils.java @@ -57,6 +57,7 @@ import org.apache.ignite.cache.store.CacheStoreSessionListener; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.CacheConfigurationInternalAccessor; import org.apache.ignite.configuration.DataRegionConfiguration; import org.apache.ignite.configuration.DataStorageConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; @@ -1740,8 +1741,10 @@ public static void initializeConfigDefaults(IgniteLogger log, CacheConfiguration Collection entities = cfg.getQueryEntities(); if (!F.isEmpty(entities)) { - cfg.clearQueryEntities().setQueryEntities( - QueryUtils.normalizeQueryEntities(recoveryMode, entities, cfg)); + CacheConfigurationInternalAccessor.replaceQueryEntities( + cfg, + QueryUtils.normalizeQueryEntities(recoveryMode, entities, cfg) + ); } } @@ -2158,8 +2161,11 @@ public static CacheConfiguration patchCacheConfiguration( boolean isSqlEscape, int qryParallelism ) { - return new CacheConfiguration<>(oldCfg) - .setQueryEntities(entities) + CacheConfiguration newCfg = new CacheConfiguration<>(oldCfg); + + CacheConfigurationInternalAccessor.replaceQueryEntities(newCfg, entities); + + return newCfg .setSqlSchema(sqlSchema) .setSqlEscapeAll(isSqlEscape) .setQueryParallelism(qryParallelism); diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityTest.java new file mode 100644 index 0000000000000..1611fb7b7cc68 --- /dev/null +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityTest.java @@ -0,0 +1,536 @@ +/* + * 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.processors.cache; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import javax.cache.CacheException; +import org.apache.ignite.cache.CacheKeyConfiguration; +import org.apache.ignite.cache.QueryEntity; +import org.apache.ignite.cache.QueryIndex; +import org.apache.ignite.cache.affinity.AffinityKeyMapped; +import org.apache.ignite.cache.query.SqlFieldsQuery; +import org.apache.ignite.cache.query.annotations.QuerySqlField; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.processors.query.QueryEntityEx; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.cluster.ClusterState.ACTIVE; +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; +import static org.junit.Assert.assertArrayEquals; + +/** Tests query entity configuration in {@link CacheConfiguration}. */ +public class CacheConfigurationQueryEntityTest extends GridCommonAbstractTest { + /** */ + private static final String MIXED_QUERY_ENTITIES_API_ERROR = + "Query entities can be configured either with setIndexedTypes or setQueryEntities, but not both [cacheName=%s]"; + + /** */ + private static final String CACHE_NAME = "query-entity-merge-cache"; + + /** */ + private static final String NAME_FIELD = "name"; + + /** */ + private static final String AGE_FIELD = "age"; + + /** */ + private boolean staticCfg; + + /** */ + private boolean indexedTypesCfg; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + if (staticCfg) { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + if (indexedTypesCfg) + ccfg.setIndexedTypes(Integer.class, Person.class); + + cfg.setCacheConfiguration(ccfg); + } + + return cfg; + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + super.afterTest(); + } + + /** + * Verifies that a repeated {@link CacheConfiguration#setQueryEntities} call replaces query entities + * configured by the previous call. + */ + @Test + public void testRepeatedSetQueryEntitiesReplacesPreviousEntities() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(Person.class.getName()) + .setFields(fields(NAME_FIELD, String.class)); + + QueryEntity second = new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(AnnotatedPerson.class.getName()) + .setFields(fields(NAME_FIELD, String.class)); + + ccfg.setQueryEntities(Collections.singleton(first)); + ccfg.setQueryEntities(Collections.singleton(second)); + + assertEquals(1, ccfg.getQueryEntities().size()); + assertSame(second, ccfg.getQueryEntities().iterator().next()); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + + assertEquals(AnnotatedPerson.class.getName(), entity.getValueType()); + assertEquals(Integer.class.getName(), entity.getKeyType()); + assertEquals(second.getFields(), entity.getFields()); + } + + /** + * Verifies that a repeated {@link CacheConfiguration#setQueryEntities} call replaces the previously + * configured entity even when both entities have the same value type. + */ + @Test + public void testRepeatedSetQueryEntitiesReplacesEntityWithSameValueType() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(Person.class.getName()) + .setFields(fields(NAME_FIELD, String.class)); + + QueryEntity second = new QueryEntity() + .setKeyType(String.class.getName()) + .setValueType(Person.class.getName()) + .setFields(fields(NAME_FIELD, String.class)); + + ccfg.setQueryEntities(Collections.singleton(first)); + ccfg.setQueryEntities(Collections.singleton(second)); + + assertEquals(1, ccfg.getQueryEntities().size()); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + + assertEquals(Person.class.getName(), entity.getValueType()); + assertEquals(String.class.getName(), entity.getKeyType()); + assertEquals(second.getFields(), entity.getFields()); + } + + /** + * Verifies that a repeated {@link CacheConfiguration#setIndexedTypes} call replaces indexed types and + * the metadata generated by the previous call. + */ + @Test + public void testRepeatedSetIndexedTypesReplacesPreviousConfiguration() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(FirstKey.class, Person.class); + ccfg.setIndexedTypes(SecondKey.class, AnnotatedPerson.class); + + assertArrayEquals( + new Class[] {SecondKey.class, AnnotatedPerson.class}, + ccfg.getIndexedTypes() + ); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + + assertEquals(SecondKey.class.getName(), entity.getKeyType()); + assertEquals(AnnotatedPerson.class.getName(), entity.getValueType()); + + CacheKeyConfiguration[] keyCfg = ccfg.getKeyConfiguration(); + + assertNotNull(keyCfg); + assertEquals(1, keyCfg.length); + assertEquals(SecondKey.class.getName(), keyCfg[0].getTypeName()); + assertEquals("secondAffinityKey", keyCfg[0].getAffinityKeyFieldName()); + } + + /** + * Verifies that {@link CacheConfiguration#setIndexedTypes} replaces an explicitly configured + * key configuration. + */ + @Test + public void testSetIndexedTypesReplacesExplicitKeyConfiguration() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setKeyConfiguration( + new CacheKeyConfiguration("ExplicitKey", "explicitAffinityKey") + ); + + ccfg.setIndexedTypes(SecondKey.class, Person.class); + + CacheKeyConfiguration[] keyCfg = ccfg.getKeyConfiguration(); + + assertNotNull(keyCfg); + assertEquals(1, keyCfg.length); + assertEquals(SecondKey.class.getName(), keyCfg[0].getTypeName()); + assertEquals("secondAffinityKey", keyCfg[0].getAffinityKeyFieldName()); + } + + /** + * Verifies that query entities cannot be configured through {@link CacheConfiguration#setQueryEntities} + * after {@link CacheConfiguration#setIndexedTypes} was used. + */ + @Test + public void testSetQueryEntitiesAfterSetIndexedTypesFails() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setIndexedTypes(Integer.class, AnnotatedPerson.class); + + String msg = String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singleton(configuredEntity(AnnotatedPerson.class))), + CacheException.class, + msg + ); + } + + /** + * Verifies that query entities cannot be configured through {@link CacheConfiguration#setIndexedTypes} + * after {@link CacheConfiguration#setQueryEntities} was used. + */ + @Test + public void testSetIndexedTypesAfterSetQueryEntitiesFails() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setQueryEntities(Collections.singleton(configuredEntity(AnnotatedPerson.class))); + + String msg = String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME); + + assertThrows( + log, + () -> ccfg.setIndexedTypes(Integer.class, AnnotatedPerson.class), + CacheException.class, + msg + ); + } + + /** + * Verifies that clearing query entities does not allow switching from {@link CacheConfiguration#setIndexedTypes} + * to {@link CacheConfiguration#setQueryEntities}. + */ + @Test + public void testClearQueryEntitiesDoesNotAllowSwitchFromIndexedTypes() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setIndexedTypes(Integer.class, Person.class); + + ccfg.clearQueryEntities(); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singleton(configuredEntity(Person.class))), + CacheException.class, + String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME) + ); + } + + /** + * Verifies that clearing query entities does not allow switching from {@link CacheConfiguration#setQueryEntities} + * to {@link CacheConfiguration#setIndexedTypes}. + */ + @Test + public void testClearQueryEntitiesDoesNotAllowSwitchFromQueryEntities() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setQueryEntities(Collections.singleton(configuredEntity(Person.class))); + + ccfg.clearQueryEntities(); + + assertThrows( + log, + () -> ccfg.setIndexedTypes(Integer.class, Person.class), + CacheException.class, + String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME) + ); + } + + /** Verifies that clearing query entities does not prevent configuring them again through the same API. */ + @Test + public void testSetQueryEntitiesCanBeUsedAfterClearQueryEntities() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = configuredEntity(Person.class); + QueryEntity second = configuredEntity(AnnotatedPerson.class); + + ccfg.setQueryEntities(Collections.singleton(first)); + + ccfg.clearQueryEntities(); + + ccfg.setQueryEntities(Collections.singleton(second)); + + Collection entities = ccfg.getQueryEntities(); + + assertEquals(1, entities.size()); + assertSame(second, entities.iterator().next()); + } + + /** Verifies that clearing query entities does not prevent configuring indexed types again through the same API. */ + @Test + public void testSetIndexedTypesCanBeUsedAfterClearQueryEntities() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(FirstKey.class, Person.class); + + ccfg.clearQueryEntities(); + + ccfg.setIndexedTypes(SecondKey.class, AnnotatedPerson.class); + + Collection entities = ccfg.getQueryEntities(); + + assertEquals(1, entities.size()); + + QueryEntity entity = entities.iterator().next(); + + assertEquals(SecondKey.class.getName(), entity.getKeyType()); + assertEquals(AnnotatedPerson.class.getName(), entity.getValueType()); + } + + /** Verifies that the indexed-types configuration source is preserved when {@link CacheConfiguration} is copied. */ + @Test + public void testIndexedTypesConfigurationSourceIsPreservedOnCopy() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setIndexedTypes(Integer.class, Person.class); + + CacheConfiguration copy = new CacheConfiguration<>(ccfg); + + assertThrows( + log, + () -> copy.setQueryEntities(Collections.singleton(configuredEntity(Person.class))), + CacheException.class, + String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME) + ); + } + + /** Verifies that the query-entities configuration source is preserved when {@link CacheConfiguration} is copied. */ + @Test + public void testQueryEntitiesConfigurationSourceIsPreservedOnCopy() { + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setQueryEntities(Collections.singleton(configuredEntity(Person.class))); + + CacheConfiguration copy = new CacheConfiguration<>(ccfg); + + assertThrows( + log, + () -> copy.setIndexedTypes(Integer.class, Person.class), + CacheException.class, + String.format(MIXED_QUERY_ENTITIES_API_ERROR, CACHE_NAME) + ); + } + + /** + * Verifies that a statically configured cache using {@link CacheConfiguration#setIndexedTypes} starts successfully + * and preserves its query-entity configuration source during cache initialization. + */ + @Test + public void testCacheConfiguredWithIndexedTypesStartsSuccessfully() throws Exception { + staticCfg = true; + + indexedTypesCfg = true; + + IgniteEx node = startGrid(0); + + assertNotNull(node.cache(CACHE_NAME)); + + QueryEntity entity = singleQueryEntity(node); + + assertEquals(Person.class.getName(), entity.getValueType()); + } + + /** + * Verifies that a node with a statically configured cache using {@link CacheConfiguration#setIndexedTypes} can + * join an active cluster. + */ + @Test + public void testNodeWithIndexedTypesConfigurationJoinsCluster() throws Exception { + staticCfg = true; + indexedTypesCfg = true; + + IgniteEx node0 = startGrid(0); + + node0.cluster().state(ACTIVE); + + IgniteEx node1 = startGrid(1); + + assertNotNull(node1.cache(CACHE_NAME)); + + QueryEntity entity = singleQueryEntity(node1); + + assertEquals(Person.class.getName(), entity.getValueType()); + } + + /** + * Verifies that adding SQL metadata to a statically configured cache preserves {@link QueryEntityEx} and its + * extended metadata. + * + *

A schema-add operation patches both the runtime cache configuration and the cache descriptor configuration. + * Internal query-entity replacement must keep these configurations independent and must not modify the descriptor + * configuration through a shared mutable collection.

+ */ + @Test + public void testSchemaAddPreservesQueryEntityExMetadata() throws Exception { + staticCfg = true; + + IgniteEx node = startGrid(0); + + node.cluster().state(ACTIVE); + + DynamicCacheDescriptor desc = node.context().cache().cacheDescriptor(CACHE_NAME); + + assertTrue(desc.cacheConfiguration().getQueryEntities().isEmpty()); + + node.cache(CACHE_NAME).query(new SqlFieldsQuery( + "CREATE TABLE TEST_TBL (" + + "ID1 INT, " + + "ID2 INT, " + + "VAL VARCHAR NOT NULL, " + + "PRIMARY KEY (ID1, ID2)" + + ") WITH \"CACHE_NAME=" + CACHE_NAME + "\"" + )).getAll(); + + Collection entities = node.context().cache() + .cacheDescriptor(CACHE_NAME) + .cacheConfiguration() + .getQueryEntities(); + + assertEquals(1, entities.size()); + + QueryEntity entity = entities.iterator().next(); + + assertTrue(entity instanceof QueryEntityEx); + + QueryEntityEx entityEx = (QueryEntityEx)entity; + + assertTrue(entityEx.sql()); + assertTrue(entityEx.isPreserveKeysOrder()); + assertTrue(entityEx.fillAbsentPKsWithDefaults()); + } + + /** */ + private static QueryEntity configuredEntity(Class valCls, QueryIndex... indexes) { + LinkedHashMap fields = new LinkedHashMap<>(); + + fields.put(NAME_FIELD, String.class.getName()); + fields.put(AGE_FIELD, Integer.class.getName()); + + QueryEntity res = new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(valCls.getName()) + .setTableName(valCls.getSimpleName()) + .setFields(fields); + + if (indexes.length != 0) + res.setIndexes(Arrays.asList(indexes)); + + return res; + } + + /** */ + private static LinkedHashMap fields(Object... vals) { + assertTrue(vals.length % 2 == 0); + + LinkedHashMap fields = new LinkedHashMap<>(); + + for (int i = 0; i < vals.length; i += 2) + fields.put((String)vals[i], ((Class)vals[i + 1]).getName()); + + return fields; + } + + /** */ + private static QueryEntity singleQueryEntity(IgniteEx node) { + Collection entities = (Collection)node.context().cache() + .cacheConfiguration(CACHE_NAME) + .getQueryEntities(); + + assertEquals(1, entities.size()); + + return entities.iterator().next(); + } + + /** */ + private static class Person { + /** */ + private final String name; + + /** */ + private final int age; + + /** */ + private Person(String name, int age) { + this.name = name; + this.age = age; + } + } + + /** */ + private static class AnnotatedPerson { + /** */ + @QuerySqlField(index = true, notNull = true) + private String name; + + /** */ + @QuerySqlField + private int age; + + /** */ + @QuerySqlField + private float weight; + + /** */ + @QuerySqlField(scale = 2) + private float height; + } + + /** */ + private static class FirstKey { + /** */ + @AffinityKeyMapped + private int firstAffinityKey; + } + + /** */ + private static class SecondKey { + /** */ + @AffinityKeyMapped + private int secondAffinityKey; + } +} diff --git a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingTestSuite.java b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingTestSuite.java index 2e78be26fda11..eac46d49207b8 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingTestSuite.java +++ b/modules/indexing/src/test/java/org/apache/ignite/testsuites/IgniteCacheWithIndexingTestSuite.java @@ -22,6 +22,7 @@ import org.apache.ignite.internal.processors.cache.BinaryTypeRegistrationTest; import org.apache.ignite.internal.processors.cache.CacheBinaryKeyConcurrentQueryTest; import org.apache.ignite.internal.processors.cache.CacheConfigurationP2PTest; +import org.apache.ignite.internal.processors.cache.CacheConfigurationQueryEntityTest; import org.apache.ignite.internal.processors.cache.CacheGroupMetricsWithIndexBuildFailTest; import org.apache.ignite.internal.processors.cache.CacheGroupMetricsWithIndexTest; import org.apache.ignite.internal.processors.cache.CacheIndexStreamerTest; @@ -126,7 +127,9 @@ EnumClassImplementingIndexedInterfaceTest.class, IndexCorruptionRebuildTest.class, - SQLCacheConfigStoragePathTest.class + SQLCacheConfigStoragePathTest.class, + + CacheConfigurationQueryEntityTest.class, }) public class IgniteCacheWithIndexingTestSuite { }