From 632986878e91f39acf7bb185104ef511088ec9d5 Mon Sep 17 00:00:00 2001 From: "Valuyskiy.O.Y" Date: Mon, 20 Jul 2026 11:59:50 +1000 Subject: [PATCH 1/5] IGNITE-28907 Add reproducer --- .../query/MixedIndexConfigurationTest.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java new file mode 100644 index 0000000000000..ac005e59c89e5 --- /dev/null +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java @@ -0,0 +1,155 @@ +package org.apache.ignite.internal.processors.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.QueryEntity; +import org.apache.ignite.cache.QueryIndex; +import org.apache.ignite.cache.QueryIndexType; +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.spi.systemview.view.SystemView; +import org.apache.ignite.spi.systemview.view.sql.SqlIndexView; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +/** */ +public class MixedIndexConfigurationTest extends GridCommonAbstractTest { + /** */ + private static final String CACHE_NAME = "mixed-index-cache"; + + /** */ + private static final String COMPOSITE_IDX_NAME = "PERSON_NAME_AGE_IDX"; + + /** */ + private static final String INDEXES_VIEW = "indexes"; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setSqlSchema("PUBLIC") + .setIndexedTypes(Integer.class, Person.class) + .setQueryEntities(Collections.singletonList(configuredPersonEntity())); + + cfg.setCacheConfiguration(ccfg); + + return cfg; + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + super.afterTest(); + } + + /** + * Verifies that the node starts and the cache works, but the composite index configured through QueryEntity is + * silently omitted. + */ + @Test + public void testConfiguredCompositeIndexIsIgnored() throws Exception { + IgniteEx node = startGrid(0); + + awaitPartitionMapExchange(); + + // Node successfully joined the cluster + assertEquals(1, node.cluster().forServers().nodes().size()); + + IgniteCache cache = node.cache(CACHE_NAME); + + assertNotNull(cache); + + String name = "Alice"; + int age = 22; + + // Cache is operational despite partially ignored SQL configuration + cache.put(1, new Person(name, age)); + + Person person = cache.get(1); + + assertNotNull(person); + assertEquals(name, person.name); + assertEquals(age, person.age); + + List indexes = cacheIndexes(node); + + String annotationIdxName = QueryUtils.normalizeObjectName(annotationIndexName(), false); + + assertTrue("Annotation-based index was not created", + indexes.stream().anyMatch(idx -> annotationIdxName.equals(idx.indexName()))); + + assertFalse("Configured composite index unexpectedly exists", + indexes.stream().anyMatch(idx -> COMPOSITE_IDX_NAME.equals(idx.indexName()))); + } + + /** */ + private static QueryEntity configuredPersonEntity() { + LinkedHashMap fields = new LinkedHashMap<>(); + + fields.put("name", String.class.getName()); + fields.put("age", Integer.class.getName()); + + QueryIndex compositeIdx = new QueryIndex( + Arrays.asList("name", "age"), + QueryIndexType.SORTED + ).setName(COMPOSITE_IDX_NAME); + + return new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(Person.class.getName()) + .setTableName(Person.class.getSimpleName()) + .setFields(fields) + .setIndexes(Collections.singletonList(compositeIdx)); + } + + /** */ + private static List cacheIndexes(IgniteEx node) { + SystemView indexes = node.context().systemView().view(INDEXES_VIEW); + + assertNotNull(indexes); + + List res = new ArrayList<>(); + + for (SqlIndexView idx : indexes) { + if (CACHE_NAME.equals(idx.cacheName())) + res.add(idx); + } + + return res; + } + + /** */ + private static String annotationIndexName() { + QueryEntity entity = new QueryEntity(Integer.class, Person.class); + + QueryIndex idx = entity.getIndexes().iterator().next(); + + return QueryUtils.indexName(entity, idx); + } + + /** */ + public static class Person { + /** */ + @QuerySqlField(index = true) + private final String name; + + /** */ + @QuerySqlField + private final int age; + + /** */ + private Person(String name, int age) { + this.name = name; + this.age = age; + } + } +} From 2527db4c20eb9c86cf26c4178131371167da9818 Mon Sep 17 00:00:00 2001 From: "Valuyskiy.O.Y" Date: Mon, 14 Sep 2026 09:55:01 +1000 Subject: [PATCH 2/5] IGNITE-28907 Support merging QueryEntity metadata in CacheConfiguration --- .../configuration/CacheConfiguration.java | 67 +- .../processors/query/QueryEntityMerger.java | 261 ++++ ...acheConfigurationQueryEntityMergeTest.java | 1202 +++++++++++++++++ .../query/MixedIndexConfigurationTest.java | 155 --- .../IgniteCacheWithIndexingTestSuite.java | 5 +- 5 files changed, 1509 insertions(+), 181 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityMerger.java create mode 100644 modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityMergeTest.java delete mode 100644 modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java 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..7f03c8654dbce 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 @@ -53,6 +53,7 @@ import org.apache.ignite.cache.store.CacheStoreSessionListener; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.internal.binary.BinaryUtils; +import org.apache.ignite.internal.processors.query.QueryEntityMerger; import org.apache.ignite.internal.processors.query.QueryUtils; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.A; @@ -1968,26 +1969,23 @@ public CacheConfiguration setIndexedTypes(Class... indexedTypes) { Class keyCls = newIndexedTypes[i]; Class valCls = newIndexedTypes[i + 1]; - QueryEntity newEntity = new QueryEntity(keyCls, valCls); + QueryEntity incomingEntity = new QueryEntity(keyCls, valCls); - boolean dup = false; + QueryEntity existingEntity = findQueryEntity(incomingEntity.findValueType()); - for (QueryEntity entity : qryEntities) { - if (Objects.equals(entity.findValueType(), newEntity.findValueType())) { - dup = true; + if (existingEntity == null) + qryEntities.add(incomingEntity); + else { + QueryEntity mergedEntity = QueryEntityMerger.merge(getName(), existingEntity, incomingEntity); - break; - } + replaceQueryEntity(existingEntity, mergedEntity); } - if (!dup) - qryEntities.add(newEntity); - // Set key configuration if needed. String affFieldName = BinaryUtils.affinityFieldName(keyCls); if (affFieldName != null) { - CacheKeyConfiguration newKeyCfg = new CacheKeyConfiguration(newEntity.getKeyType(), affFieldName); + CacheKeyConfiguration newKeyCfg = new CacheKeyConfiguration(incomingEntity.getKeyType(), affFieldName); if (F.isEmpty(keyCfg)) keyCfg = new CacheKeyConfiguration[] { newKeyCfg }; @@ -2080,25 +2078,21 @@ public CacheConfiguration setPartitionLossPolicy(PartitionLossPolicy partL * @return {@code this} for chaining. */ public CacheConfiguration setQueryEntities(Collection qryEntities) { - if (this.qryEntities == null) { - this.qryEntities = new ArrayList<>(qryEntities); + if (this.qryEntities == null) + this.qryEntities = new ArrayList<>(); - return this; - } + for (QueryEntity incomingEntity : qryEntities) { + String valType = incomingEntity.findValueType(); - for (QueryEntity entity : qryEntities) { - boolean found = false; + QueryEntity existingEntity = findQueryEntity(valType); - for (QueryEntity existing : this.qryEntities) { - if (Objects.equals(entity.findValueType(), existing.findValueType())) { - found = true; + if (existingEntity == null) + this.qryEntities.add(incomingEntity); + else { + QueryEntity mergedEntity = QueryEntityMerger.merge(getName(), existingEntity, incomingEntity); - break; - } + replaceQueryEntity(existingEntity, mergedEntity); } - - if (!found) - this.qryEntities.add(entity); } return this; @@ -2484,6 +2478,29 @@ public CacheConfiguration setIndexPath(String idxPath) { return S.toString(CacheConfiguration.class, this); } + /** */ + private QueryEntity findQueryEntity(String valType) { + if (qryEntities == null) + return null; + + for (QueryEntity entity : qryEntities) { + if (Objects.equals(entity.findValueType(), valType)) + return entity; + } + + return null; + } + + /** */ + private void replaceQueryEntity(QueryEntity oldEntity, QueryEntity newEntity) { + Collection updated = new ArrayList<>(qryEntities.size()); + + for (QueryEntity entity : qryEntities) + updated.add(entity == oldEntity ? newEntity : entity); + + qryEntities = updated; + } + /** * Filter that accepts all nodes. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityMerger.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityMerger.java new file mode 100644 index 0000000000000..16b0cc6d67dab --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityMerger.java @@ -0,0 +1,261 @@ +/* + * 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.query; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import javax.cache.CacheException; +import org.apache.ignite.cache.QueryEntity; +import org.apache.ignite.cache.QueryIndex; +import org.apache.ignite.internal.util.typedef.F; + +/** Utility for merging compatible {@link QueryEntity} metadata. */ +public final class QueryEntityMerger { + /** */ + private final String cacheName; + + /** */ + private QueryEntityMerger(String cacheName) { + this.cacheName = cacheName; + } + + /** + * Merges incoming query entity metadata into existing entity. + * + * @param cacheName Cache name. + * @param existing Existing query entity. + * @param incoming Incoming query entity. + * @return Merged query entity. + * @throws CacheException If entities contain conflicting metadata. + */ + public static QueryEntity merge(String cacheName, QueryEntity existing, QueryEntity incoming) { + return new QueryEntityMerger(cacheName).merge0(existing, incoming); + } + + /** */ + private QueryEntity merge0(QueryEntity ex, QueryEntity in) { + if (!Objects.equals(ex.findValueType(), in.findValueType())) { + throw new CacheException( + "Failed to merge query entities because value types differ " + + "[cacheName=" + cacheName + + ", existingValueType=" + ex.findValueType() + + ", incomingValueType=" + in.findValueType() + ']' + ); + } + + QueryEntity res = new QueryEntity(ex); + + res.setKeyType(mergeKeyType(ex, in)); + + res.setValueType(mergeProperty("valueType", ex.getValueType(), in.getValueType())); + res.setTableName(mergeProperty("tableName", ex.getTableName(), in.getTableName())); + res.setKeyFieldName(mergeProperty("keyFieldName", ex.getKeyFieldName(), in.getKeyFieldName())); + res.setValueFieldName(mergeProperty("valueFieldName", ex.getValueFieldName(), in.getValueFieldName())); + + res.setFields(mergeFields(ex.getFields(), in.getFields())); + + res.setKeyFields(mergeSet(ex.getKeyFields(), in.getKeyFields())); + res.setNotNullFields(mergeSet(ex.getNotNullFields(), in.getNotNullFields())); + + res.setAliases(mergeMap("aliases", ex.getAliases(), in.getAliases())); + res.setDefaultFieldValues(mergeMap("defaultFieldValues", ex.getDefaultFieldValues(), in.getDefaultFieldValues())); + res.setFieldsPrecision(mergeMap("fieldsPrecision", ex.getFieldsPrecision(), in.getFieldsPrecision())); + res.setFieldsScale(mergeMap("fieldsScale", ex.getFieldsScale(), in.getFieldsScale())); + + res.setIndexes(mergeIndexes(res, ex.getIndexes(), in.getIndexes())); + + return res; + } + + /** */ + private String mergeKeyType(QueryEntity ex, QueryEntity in) { + String exKeyType = ex.findKeyType(); + String inKeyType = in.findKeyType(); + + if (exKeyType != null && inKeyType != null && !Objects.equals(exKeyType, inKeyType)) { + throw mergeConflict( + "keyType", + exKeyType, + inKeyType + ); + } + + return ex.getKeyType() != null ? ex.getKeyType() : in.getKeyType(); + } + + /** */ + private T mergeProperty(String propName, T existingVal, T incomingVal) { + if (existingVal == null) + return incomingVal; + + if (incomingVal == null) + return existingVal; + + if (Objects.equals(existingVal, incomingVal)) + return existingVal; + + throw mergeConflict(propName, existingVal, incomingVal); + } + + /** */ + private LinkedHashMap mergeFields( + Map existingFields, + Map incomingFields + ) { + if (existingFields == null && incomingFields == null) + return null; + + LinkedHashMap res = new LinkedHashMap<>(); + + if (existingFields != null) + res.putAll(existingFields); + + if (incomingFields == null) + return res; + + for (Map.Entry entry : incomingFields.entrySet()) { + String field = entry.getKey(); + String incomingType = entry.getValue(); + + if (!res.containsKey(field)) { + res.put(field, incomingType); + + continue; + } + + String existingType = res.get(field); + + if (!Objects.equals(existingType, incomingType)) + throw mergeConflict("fieldType[" + field + ']', existingType, incomingType); + } + + return res; + } + + /** */ + private Map mergeMap(String propName, Map existingVals, Map incomingVals) { + if (existingVals == null && incomingVals == null) + return null; + + Map res = new HashMap<>(); + + if (existingVals != null) + res.putAll(existingVals); + + if (incomingVals == null) + return res; + + for (Map.Entry entry : incomingVals.entrySet()) { + String field = entry.getKey(); + T incomingVal = entry.getValue(); + + if (!res.containsKey(field)) { + res.put(field, incomingVal); + + continue; + } + + T existingVal = res.get(field); + + if (!Objects.equals(existingVal, incomingVal)) + throw mergeConflict(propName + '[' + field + ']', existingVal, incomingVal); + } + + return res; + } + + /** */ + private Set mergeSet(Set existing, Set incoming) { + if (F.isEmpty(existing) && F.isEmpty(incoming)) + return null; + + Set res = new LinkedHashSet<>(); + + if (existing != null) + res.addAll(existing); + + if (incoming != null) + res.addAll(incoming); + + return res; + } + + /** */ + private Collection mergeIndexes( + QueryEntity entity, + Collection existingIndexes, + Collection incomingIndexes + ) { + if (F.isEmpty(existingIndexes) && F.isEmpty(incomingIndexes)) + return null; + + List res = new ArrayList<>(); + + Map indexesByName = new HashMap<>(); + + if (existingIndexes != null) { + for (QueryIndex idx : existingIndexes) { + String idxName = QueryUtils.indexName(entity, idx); + + res.add(idx); + + indexesByName.put(idxName, idx); + } + } + + if (incomingIndexes == null) + return res; + + for (QueryIndex incomingIdx : incomingIndexes) { + String idxName = QueryUtils.indexName(entity, incomingIdx); + + QueryIndex existingIdx = indexesByName.get(idxName); + + if (existingIdx == null) { + res.add(incomingIdx); + + indexesByName.put(idxName, incomingIdx); + + continue; + } + + if (!existingIdx.equals(incomingIdx)) + throw mergeConflict("index[" + idxName + ']', existingIdx, incomingIdx); + } + + return res; + } + + /** */ + private CacheException mergeConflict(String propName, Object existingVal, Object incomingVal) { + return new CacheException( + "Failed to merge query entities due to conflicting metadata " + + "[cacheName=" + cacheName + + ", property=" + propName + + ", existingValue=" + existingVal + + ", incomingValue=" + incomingVal + ']' + ); + } +} diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityMergeTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityMergeTest.java new file mode 100644 index 0000000000000..36ccc5e10918e --- /dev/null +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/CacheConfigurationQueryEntityMergeTest.java @@ -0,0 +1,1202 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import javax.cache.CacheException; +import org.apache.ignite.cache.QueryEntity; +import org.apache.ignite.cache.QueryIndex; +import org.apache.ignite.cache.QueryIndexType; +import org.apache.ignite.cache.query.annotations.QuerySqlField; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.processors.query.QueryUtils; +import org.apache.ignite.spi.systemview.view.SystemView; +import org.apache.ignite.spi.systemview.view.sql.SqlIndexView; +import org.apache.ignite.spi.systemview.view.sql.SqlTableColumnView; +import org.apache.ignite.spi.systemview.view.sql.SqlTableView; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.internal.processors.query.schema.management.SchemaManager.SQL_TBLS_VIEW; +import static org.apache.ignite.internal.processors.query.schema.management.SchemaManager.SQL_TBL_COLS_VIEW; +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; + +/** Tests for merging QueryEntity metadata in CacheConfiguration. */ +public class CacheConfigurationQueryEntityMergeTest extends GridCommonAbstractTest { + /** */ + private static final String CACHE_NAME = "query-entity-merge-cache"; + + /** */ + private static final String COMPOSITE_IDX = "PERSON_NAME_AGE_IDX"; + + /** */ + private static final String NAME_IDX = "EXPLICIT_NAME_IDX"; + + /** */ + private static final String AGE_IDX = "EXPLICIT_AGE_IDX"; + + /** */ + private static final String NAME_FIELD = "name"; + + /** */ + private static final String AGE_FIELD = "age"; + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + super.afterTest(); + } + + /** Query entities with different value types must not be merged. */ + @Test + public void testDifferentValueTypesAreNotMerged() 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)); + + node.createCache(ccfg); + + Collection entities = entities(node); + + assertEquals(2, entities.size()); + + for (Class cls : List.of(Person.class, AnnotatedPerson.class)) + assertTrue(entities.stream().anyMatch(e -> cls.getName().equals(e.getValueType()))); + } + + /** Query entities with the same value type but different key type are a conflict. */ + @Test + public void testConflictingKeyTypesFail() { + 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)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=keyType, existingValue=%s, incomingValue=%s]", + CACHE_NAME, Integer.class.getName(), String.class.getName()); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singleton(second)), + CacheException.class, + msg + ); + } + + /** + * Checks that query entities with conflicting effective key types cannot be merged. + *

+ * The first entity does not define {@code keyType} explicitly. Instead, its effective key type is derived from + * {@code keyFieldName} and the corresponding field type. The second entity defines a different key type explicitly. + *

+ * Although {@link QueryEntity#getKeyType()} returns {@code null} for the first entity, + * {@link QueryEntity#findKeyType()} resolves its key type from the field metadata. Therefore, the entities must be + * treated as having conflicting key types. + */ + @Test + public void testConflictingImplicitAndExplicitKeyTypes() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = new QueryEntity() + .setValueType(Person.class.getName()) + .setFields(fields("id", Integer.class)) + .setKeyFieldName("id"); // keyType is null + + QueryEntity second = new QueryEntity() + .setValueType(Person.class.getName()) + .setKeyType(String.class.getName()); + + ccfg.setQueryEntities(Collections.singleton(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=keyType, existingValue=%s, incomingValue=%s]", + CACHE_NAME, Integer.class.getName(), String.class.getName()); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singleton(second)), + CacheException.class, + msg + ); + } + + /** + * Checks that query entities with matching effective value types can be merged when one value type is defined + * implicitly and the other one explicitly. + *

+ * The first entity does not define {@code valueType} explicitly. Its effective value type is derived from + * {@code valueFieldName} and the corresponding field type, so {@link QueryEntity#getValueType()} returns + * {@code null}, while {@link QueryEntity#findValueType()} resolves it to the person type. + *

+ * The second entity defines the same value type explicitly. Since both entities have the same effective value + * type, they must be merged without a conflict. + */ + @Test + public void testMatchingImplicitAndExplicitValueTypesAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = new QueryEntity() + .setFields(fields("val", Person.class)) + .setValueFieldName("val"); // valueType is null + + QueryEntity second = new QueryEntity().setValueType(Person.class.getName()); + + ccfg.setQueryEntities(Collections.singleton(first)); + ccfg.setQueryEntities(Collections.singleton(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals("val", entity.getValueFieldName()); + assertEquals(Person.class.getName(), entity.getFields().get("val")); + assertEquals(Person.class.getName(), entity.getValueType()); + } + + /** A missing table name in the first entity must be filled from the second entity. */ + @Test + public void testTableNameIsFilledFromSecondEntity() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String tblName = "PERSON"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + first.setTableName(null); + + QueryEntity second = personEntity(fields(AGE_FIELD, Integer.class)); + second.setTableName(tblName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + SqlTableView tbl = cacheTable(node); + assertNotNull(tbl); + + assertEquals(tblName, tbl.tableName()); + } + + /** Same table names for one query entity are merged. */ + @Test + public void testSameTableNamesAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String tblName = "PERSON"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + first.setTableName(tblName); + + QueryEntity second = personEntity(fields(AGE_FIELD, Integer.class)); + second.setTableName(tblName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + SqlTableView tbl = cacheTable(node); + assertNotNull(tbl); + + assertEquals(tblName, tbl.tableName()); + } + + /** Different configured table names are a conflict. */ + @Test + public void testConflictingTableNamesFails() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String tableA = "TABLE_A"; + String tableB = "TABLE_B"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + first.setTableName(tableA); + + QueryEntity second = personEntity(fields(AGE_FIELD, Integer.class)); + second.setTableName(tableB); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=tableName, existingValue=%s, incomingValue=%s]", + CACHE_NAME, tableA, tableB); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** A missing key field name in the first entity must be filled from the second entity. */ + @Test + public void testKeyFieldNameIsFilledFromSecondEntity() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String keyFieldName = "key"; + + QueryEntity first = personEntity(fields(keyFieldName, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(keyFieldName, Integer.class, NAME_FIELD, String.class)); + + second.setKeyFieldName(keyFieldName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(keyFieldName, entity.getKeyFieldName()); + } + + /** Same key field names must be merged. */ + @Test + public void testSameKeyFieldNameIsMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String keyFieldName = "key"; + + QueryEntity first = personEntity(fields(keyFieldName, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(keyFieldName, Integer.class, NAME_FIELD, String.class)); + + first.setKeyFieldName(keyFieldName); + second.setKeyFieldName(keyFieldName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(keyFieldName, entity.getKeyFieldName()); + } + + /** Different key field names for the same query entity are a conflict. */ + @Test + public void testConflictingKeyFieldNamesFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String key1 = "key1"; + String key2 = "key2"; + + QueryEntity first = personEntity(fields(key1, Integer.class, key2, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(key1, Integer.class, key2, Integer.class, NAME_FIELD, String.class)); + + first.setKeyFieldName(key1); + second.setKeyFieldName(key2); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=keyFieldName, existingValue=%s, incomingValue=%s]", + CACHE_NAME, key1, key2); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** A missing value field name in the first entity must be filled from the second entity. */ + @Test + public void testValueFieldNameIsFilledFromSecondEntity() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String valFieldName = "value"; + + QueryEntity first = personEntity(fields(valFieldName, Person.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(valFieldName, Person.class, NAME_FIELD, String.class)); + + second.setValueFieldName(valFieldName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(valFieldName, entity.getValueFieldName()); + } + + /** Same value field names must be merged. */ + @Test + public void testSameValueFieldNameIsMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String valFieldName = "value"; + + QueryEntity first = personEntity(fields(valFieldName, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(valFieldName, Integer.class, NAME_FIELD, String.class)); + + first.setValueFieldName(valFieldName); + second.setValueFieldName(valFieldName); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(valFieldName, entity.getValueFieldName()); + } + + /** Different value field names for the same query entity are a conflict. */ + @Test + public void testConflictingValueFieldNamesFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String val1 = "value1"; + String val2 = "value2"; + + QueryEntity first = personEntity(fields(val1, Person.class, val2, Person.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(val1, Person.class, val2, Person.class, NAME_FIELD, String.class)); + + first.setValueFieldName(val1); + second.setValueFieldName(val2); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=valueFieldName, existingValue=%s, incomingValue=%s]", + CACHE_NAME, val1, val2); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** Same field with the same type must be merged. */ + @Test + public void testSameFieldWithSameTypeIsMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setQueryEntities(Collections.singletonList(personEntity(fields(NAME_FIELD, String.class)))); + ccfg.setQueryEntities(Collections.singletonList(personEntity(fields(NAME_FIELD, String.class)))); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals( + Map.of(NAME_FIELD, String.class.getName()), + entity.getFields() + ); + } + + /** Same field with different types is a conflict. */ + @Test + public void testSameFieldWithDifferentTypesFails() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, Integer.class)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata " + + "[cacheName=%s, property=fieldType[%s], existingValue=%s, incomingValue=%s]", + CACHE_NAME, NAME_FIELD, String.class.getName(), Integer.class.getName()); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** Different key fields for the same entity must be merged. */ + @Test + public void testDifferentKeyFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String key1 = "id"; + String key2 = "otherId"; + + QueryEntity first = personEntity(fields(key1, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(key2, Integer.class, NAME_FIELD, String.class)); + + first.setKeyFields(Collections.singleton(key1)); + second.setKeyFields(Collections.singleton(key2)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(new LinkedHashSet<>(Arrays.asList(key1, key2)), entity.getKeyFields()); + } + + /** Same key fields for the same entity must be merged. */ + @Test + public void testSameKeyFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String key = "id"; + + QueryEntity first = personEntity(fields(key, Integer.class, NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(key, Integer.class, NAME_FIELD, String.class)); + + first.setKeyFields(Collections.singleton(key)); + second.setKeyFields(Collections.singleton(key)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(Collections.singleton("id"), entity.getKeyFields()); + } + + /** NOT NULL fields from annotation and explicit configuration must be merged. */ + @Test + public void testNotNullFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(Integer.class, AnnotatedPerson.class); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(AnnotatedPerson.class) + .setNotNullFields(Collections.singleton(AGE_FIELD)) + )); + + node.createCache(ccfg); + + List cols = cacheColumns(node); + + SqlTableColumnView nameCol = findColumn(cols, NAME_FIELD); + SqlTableColumnView ageCol = findColumn(cols, AGE_FIELD); + + assertNotNull(nameCol); + assertNotNull(ageCol); + + assertFalse(nameCol.nullable()); + assertFalse(ageCol.nullable()); + } + + /** Same NOT NULL fields from annotation and explicit configuration must be merged. */ + @Test + public void testSameNotNullFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(Integer.class, AnnotatedPerson.class); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(AnnotatedPerson.class) + .setNotNullFields(Collections.singleton(NAME_FIELD)) + )); + + node.createCache(ccfg); + + List cols = cacheColumns(node); + + SqlTableColumnView nameCol = findColumn(cols, NAME_FIELD); + assertNotNull(nameCol); + + assertFalse(nameCol.nullable()); + } + + /** Aliases for different entity fields must be merged. */ + @Test + public void testAliasesForDifferentFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String nameAlias = "name-alias"; + String ageAlias = "age-alias"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(AGE_FIELD, Integer.class)); + + first.setAliases(Collections.singletonMap(NAME_FIELD, nameAlias)); + second.setAliases(Collections.singletonMap(AGE_FIELD, ageAlias)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(2, entity.getAliases().size()); + + String actualNameAlias = entity.getAliases().get(NAME_FIELD); + + assertNotNull(actualNameAlias); + assertTrue(nameAlias.equalsIgnoreCase(actualNameAlias)); + + String actualAgeAlias = entity.getAliases().get(AGE_FIELD); + + assertNotNull(actualAgeAlias); + assertTrue(ageAlias.equalsIgnoreCase(actualAgeAlias)); + } + + /** Different aliases for the same field are a conflict. */ + @Test + public void testDifferentAliasesForSameFieldFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String alias1 = "alias1"; + String alias2 = "alias2"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, String.class)); + + first.setAliases(Collections.singletonMap(NAME_FIELD, alias1)); + second.setAliases(Collections.singletonMap(NAME_FIELD, alias2)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=aliases[%s], existingValue=%s, incomingValue=%s]", + CACHE_NAME, NAME_FIELD, alias1, alias2); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** Default field values for different fields must be merged. */ + @Test + public void testDefaultFieldValuesAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String defName = "John Doe"; + int defAge = 30; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(AGE_FIELD, Integer.class)); + + first.setDefaultFieldValues(Collections.singletonMap(NAME_FIELD, defName)); + second.setDefaultFieldValues(Collections.singletonMap(AGE_FIELD, defAge)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals(2, entity.getDefaultFieldValues().size()); + + String actualDefName = (String)entity.getDefaultFieldValues().get(NAME_FIELD); + assertNotNull(actualDefName); + + assertEquals(defName, actualDefName); + + int actualDefAge = (int)entity.getDefaultFieldValues().get(AGE_FIELD); + + assertEquals(defAge, actualDefAge); + } + + /** Different aliases for the same field are a conflict. */ + @Test + public void testDifferentDefaultValuesForSameFieldFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String defName1 = "NAME_A"; + String defName2 = "NAME_B"; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, String.class)); + + first.setDefaultFieldValues(Collections.singletonMap(NAME_FIELD, defName1)); + second.setDefaultFieldValues(Collections.singletonMap(NAME_FIELD, defName2)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=defaultFieldValues[%s], existingValue=%s, incomingValue=%s]", + CACHE_NAME, NAME_FIELD, defName1, defName2); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** A missing precision definition in the first entity must be filled from the second entity. */ + @Test + public void testPrecisionIsMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + int precision = 50; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, String.class)); + + second.setFieldsPrecision(Collections.singletonMap(NAME_FIELD, precision)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + ccfg.setQueryEntities(Collections.singletonList(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals( + Map.of(NAME_FIELD, precision), + entity.getFieldsPrecision() + ); + } + + /** Equal precision definitions are merged. */ + @Test + public void testSamePrecisionIsMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + int precision = 50; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, String.class)); + + first.setFieldsPrecision(Collections.singletonMap(NAME_FIELD, precision)); + second.setFieldsPrecision(Collections.singletonMap(NAME_FIELD, precision)); + + ccfg.setQueryEntities(Collections.singleton(first)); + ccfg.setQueryEntities(Collections.singleton(second)); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + assertEquals( + Map.of(NAME_FIELD, precision), + entity.getFieldsPrecision() + ); + } + + /** Different precision definitions for the same field are a conflict. */ + @Test + public void testDifferentPrecisionDefinitionsFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + int precision1 = 50; + int precision2 = 100; + + QueryEntity first = personEntity(fields(NAME_FIELD, String.class)); + QueryEntity second = personEntity(fields(NAME_FIELD, String.class)); + + first.setFieldsPrecision(Collections.singletonMap(NAME_FIELD, precision1)); + second.setFieldsPrecision(Collections.singletonMap(NAME_FIELD, precision2)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=fieldsPrecision[%s], existingValue=%s, incomingValue=%s]", + CACHE_NAME, NAME_FIELD, precision1, precision2); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** Scale definitions for different entity fields are merged. */ + @Test + public void testScaleDefinitionsForDifferentFieldsAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(Integer.class, AnnotatedPerson.class); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(AnnotatedPerson.class) + .setFieldsScale(Collections.singletonMap("weight", 2)) + )); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + assertNotNull(entity); + + int weightScale = entity.getFieldsScale().get("weight"); + assertEquals(2, weightScale); + + int heightScale = entity.getFieldsScale().get("height"); + assertEquals(2, heightScale); + } + + /** Different scale definitions for the same entity field are a conflict. */ + @Test + public void testConflictingScaleDefinitionsFail() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setIndexedTypes(Integer.class, AnnotatedPerson.class); + + String heightField = "height"; + int heightScale = 3; + + QueryEntity configured = configuredEntity(AnnotatedPerson.class) + .setFieldsScale(Collections.singletonMap(heightField, heightScale)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=fieldsScale[%s], existingValue=2, incomingValue=%s]", + CACHE_NAME, heightField, heightScale); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(configured)), + CacheException.class, + msg + ); + } + + /** Configured indexedTypes must not prevent configured composite index from being created. */ + @Test + public void testCompositeIndexIsCreatedWithIndexedTypes() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setIndexedTypes(Integer.class, Person.class) + .setQueryEntities(Collections.singletonList( + configuredEntity(Person.class, index(COMPOSITE_IDX, NAME_FIELD, AGE_FIELD)) + )); + + node.createCache(ccfg); + + List indexes = cacheIndexes(node); + + assertTrue(hasIndex(indexes, COMPOSITE_IDX)); + + assertEquals(1, indexes.stream().filter(idx -> !idx.isPk()).count()); + } + + /** Annotation index and configured composite index must coexist. */ + @Test + public void testAnnotationAndConfiguredIndexesAreCreated() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setIndexedTypes(Integer.class, AnnotatedPerson.class) + .setQueryEntities(Collections.singletonList( + configuredEntity(AnnotatedPerson.class, index(COMPOSITE_IDX, NAME_FIELD, AGE_FIELD)) + )); + + node.createCache(ccfg); + + List indexes = cacheIndexes(node); + + String annotationIdxName = annotationIndexName(); + + assertTrue(hasIndex(indexes, annotationIdxName)); + assertTrue(hasIndex(indexes, COMPOSITE_IDX)); + + assertEquals(2, indexes.stream().filter(idx -> !idx.isPk()).count()); + } + + /** Reverse order must work as well: queryEntities first, indexedTypes second. */ + @Test + public void testReverseConfigurationOrder() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME) + .setQueryEntities(Collections.singletonList( + configuredEntity(AnnotatedPerson.class, index(COMPOSITE_IDX, NAME_FIELD, AGE_FIELD)) + )) + .setIndexedTypes(Integer.class, AnnotatedPerson.class); + + node.createCache(ccfg); + + List indexes = cacheIndexes(node); + + assertTrue(hasIndex(indexes, annotationIndexName())); + assertTrue(hasIndex(indexes, COMPOSITE_IDX)); + + assertEquals(2, indexes.stream().filter(idx -> !idx.isPk()).count()); + } + + /** Metadata added by several consecutive setQueryEntities calls must accumulate. */ + @Test + public void testSeveralSetQueryEntitiesCreateAllIndexes() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(Person.class, index(NAME_IDX, NAME_FIELD)) + )); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(Person.class, index(AGE_IDX, AGE_FIELD)) + )); + + ccfg.setQueryEntities(Collections.singletonList( + configuredEntity(Person.class, index(COMPOSITE_IDX, NAME_FIELD, AGE_FIELD)) + )); + + node.createCache(ccfg); + + List indexes = cacheIndexes(node); + + assertTrue(hasIndex(indexes, NAME_IDX)); + assertTrue(hasIndex(indexes, AGE_IDX)); + assertTrue(hasIndex(indexes, COMPOSITE_IDX)); + + assertEquals(3, indexes.stream().filter(idx -> !idx.isPk()).count()); + } + + /** Identical index definitions must be deduplicated. */ + @Test + public void testSameIndexDefinitionIsDeduplicated() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String idxName = "DUP_IDX"; + + QueryIndex idx = index(idxName, NAME_FIELD); + + ccfg.setQueryEntities(Collections.singletonList(personEntity(fields(NAME_FIELD, String.class), idx))); + ccfg.setQueryEntities(Collections.singletonList(personEntity(fields(NAME_FIELD, String.class), idx))); + + node.createCache(ccfg); + + List indexes = cacheIndexes(node); + + assertTrue(hasIndex(indexes, idxName)); + + assertEquals(1, indexes.stream().filter(i -> !i.isPk()).count()); + } + + /** Same index name with different definition is a conflict. */ + @Test + public void testSameIndexNameWithDifferentDefinitionFails() { + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + String ageField = AGE_FIELD; + String conflictIdx = "CONFLICT_IDX"; + + QueryEntity first = configuredEntity(Person.class, index(conflictIdx, NAME_FIELD)); + QueryEntity second = configuredEntity(Person.class, index(conflictIdx, ageField)); + + ccfg.setQueryEntities(Collections.singletonList(first)); + + String msg = String.format("Failed to merge query entities due to conflicting metadata [" + + "cacheName=%s, property=index[%s], " + + "existingValue=QueryIndex [name=%s, fields=LinkedHashMap {%s=true}, type=SORTED, inlineSize=-1], " + + "incomingValue=QueryIndex [name=%s, fields=LinkedHashMap {%s=true}, type=SORTED, inlineSize=-1]]", + CACHE_NAME, conflictIdx, conflictIdx, NAME_FIELD, conflictIdx, ageField); + + assertThrows( + log, + () -> ccfg.setQueryEntities(Collections.singletonList(second)), + CacheException.class, + msg + ); + } + + /** Indexes without explicitly configured names are merged correctly. */ + @Test + public void testIndexesWithoutExplicitNamesAreMerged() throws Exception { + IgniteEx node = startGrid(0); + + CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); + + QueryIndex firstIdx = new QueryIndex(Collections.singletonList(NAME_FIELD), QueryIndexType.SORTED); + assertNull(firstIdx.getName()); + + QueryIndex secondIdx = new QueryIndex(Arrays.asList(NAME_FIELD, AGE_FIELD), QueryIndexType.SORTED); + assertNull(secondIdx.getName()); + + QueryIndex thirdIdx = index(AGE_IDX, AGE_FIELD); + assertNotNull(thirdIdx.getName()); + + ccfg.setQueryEntities(Collections.singletonList(configuredEntity(Person.class, firstIdx))); + ccfg.setQueryEntities(Collections.singletonList(configuredEntity(Person.class, secondIdx))); + ccfg.setQueryEntities(Collections.singletonList(configuredEntity(Person.class, thirdIdx))); + + node.createCache(ccfg); + + QueryEntity entity = singleQueryEntity(node); + + assertEquals(3, entity.getIndexes().size()); + + for (QueryIndex idx : List.of(firstIdx, secondIdx, thirdIdx)) + assertTrue(entity.getIndexes().stream().anyMatch(i -> i.getFields().equals(idx.getFields()))); + } + + /** */ + private static QueryEntity personEntity(LinkedHashMap fields, QueryIndex... indexes) { + QueryEntity entity = new QueryEntity() + .setKeyType(Integer.class.getName()) + .setValueType(Person.class.getName()) + .setTableName(Person.class.getSimpleName()) + .setFields(fields); + + if (indexes.length != 0) + entity.setIndexes(Arrays.asList(indexes)); + + return entity; + } + + /** */ + 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 QueryIndex index(String name, String... fields) { + return new QueryIndex(Arrays.asList(fields), QueryIndexType.SORTED).setName(name); + } + + /** */ + private static List cacheIndexes(IgniteEx node) { + SystemView indexes = node.context().systemView().view("indexes"); + assertNotNull(indexes); + + List res = new ArrayList<>(); + + for (SqlIndexView idx : indexes) { + if (CACHE_NAME.equals(idx.cacheName())) + res.add(idx); + } + + return res; + } + + /** */ + private static boolean hasIndex(List indexes, String idxName) { + return indexes.stream().anyMatch(idx -> idxName.equalsIgnoreCase(idx.indexName())); + } + + /** */ + private static String annotationIndexName() { + QueryEntity entity = new QueryEntity(Integer.class, AnnotatedPerson.class); + + QueryIndex idx = entity.getIndexes().iterator().next(); + + return QueryUtils.indexName(entity, idx); + } + + /** */ + 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 SqlTableView cacheTable(IgniteEx node) { + SystemView tables = node.context().systemView().view(SQL_TBLS_VIEW); + assertNotNull(tables); + + SqlTableView res = null; + + for (SqlTableView tbl : tables) { + if (CACHE_NAME.equals(tbl.cacheName())) + res = tbl; + } + + return res; + } + + /** */ + private static List cacheColumns(IgniteEx node) { + SystemView cols = node.context().systemView().view(SQL_TBL_COLS_VIEW); + assertNotNull(cols); + + List res = new ArrayList<>(); + + SqlTableView tbl = cacheTable(node); + assertNotNull(tbl); + + for (SqlTableColumnView col : cols) { + if (tbl.tableName().equals(col.tableName())) + res.add(col); + } + + return res; + } + + /** */ + private static SqlTableColumnView findColumn(List cols, String colName) { + SqlTableColumnView res = null; + + for (SqlTableColumnView col : cols) { + if (col.columnName().equalsIgnoreCase(colName)) + res = col; + } + + return res; + } + + /** */ + private static Collection entities(IgniteEx node) { + return (Collection)node.context().cache().cacheConfiguration(CACHE_NAME).getQueryEntities(); + } + + /** */ + private static QueryEntity singleQueryEntity(IgniteEx node) { + Collection entities = entities(node); + + 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; + } +} diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java deleted file mode 100644 index ac005e59c89e5..0000000000000 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/query/MixedIndexConfigurationTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.apache.ignite.internal.processors.query; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import org.apache.ignite.IgniteCache; -import org.apache.ignite.cache.QueryEntity; -import org.apache.ignite.cache.QueryIndex; -import org.apache.ignite.cache.QueryIndexType; -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.spi.systemview.view.SystemView; -import org.apache.ignite.spi.systemview.view.sql.SqlIndexView; -import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; -import org.junit.Test; - -/** */ -public class MixedIndexConfigurationTest extends GridCommonAbstractTest { - /** */ - private static final String CACHE_NAME = "mixed-index-cache"; - - /** */ - private static final String COMPOSITE_IDX_NAME = "PERSON_NAME_AGE_IDX"; - - /** */ - private static final String INDEXES_VIEW = "indexes"; - - /** {@inheritDoc} */ - @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { - IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); - - CacheConfiguration ccfg = new CacheConfiguration<>(CACHE_NAME); - - ccfg.setSqlSchema("PUBLIC") - .setIndexedTypes(Integer.class, Person.class) - .setQueryEntities(Collections.singletonList(configuredPersonEntity())); - - cfg.setCacheConfiguration(ccfg); - - return cfg; - } - - /** {@inheritDoc} */ - @Override protected void afterTest() throws Exception { - stopAllGrids(); - - super.afterTest(); - } - - /** - * Verifies that the node starts and the cache works, but the composite index configured through QueryEntity is - * silently omitted. - */ - @Test - public void testConfiguredCompositeIndexIsIgnored() throws Exception { - IgniteEx node = startGrid(0); - - awaitPartitionMapExchange(); - - // Node successfully joined the cluster - assertEquals(1, node.cluster().forServers().nodes().size()); - - IgniteCache cache = node.cache(CACHE_NAME); - - assertNotNull(cache); - - String name = "Alice"; - int age = 22; - - // Cache is operational despite partially ignored SQL configuration - cache.put(1, new Person(name, age)); - - Person person = cache.get(1); - - assertNotNull(person); - assertEquals(name, person.name); - assertEquals(age, person.age); - - List indexes = cacheIndexes(node); - - String annotationIdxName = QueryUtils.normalizeObjectName(annotationIndexName(), false); - - assertTrue("Annotation-based index was not created", - indexes.stream().anyMatch(idx -> annotationIdxName.equals(idx.indexName()))); - - assertFalse("Configured composite index unexpectedly exists", - indexes.stream().anyMatch(idx -> COMPOSITE_IDX_NAME.equals(idx.indexName()))); - } - - /** */ - private static QueryEntity configuredPersonEntity() { - LinkedHashMap fields = new LinkedHashMap<>(); - - fields.put("name", String.class.getName()); - fields.put("age", Integer.class.getName()); - - QueryIndex compositeIdx = new QueryIndex( - Arrays.asList("name", "age"), - QueryIndexType.SORTED - ).setName(COMPOSITE_IDX_NAME); - - return new QueryEntity() - .setKeyType(Integer.class.getName()) - .setValueType(Person.class.getName()) - .setTableName(Person.class.getSimpleName()) - .setFields(fields) - .setIndexes(Collections.singletonList(compositeIdx)); - } - - /** */ - private static List cacheIndexes(IgniteEx node) { - SystemView indexes = node.context().systemView().view(INDEXES_VIEW); - - assertNotNull(indexes); - - List res = new ArrayList<>(); - - for (SqlIndexView idx : indexes) { - if (CACHE_NAME.equals(idx.cacheName())) - res.add(idx); - } - - return res; - } - - /** */ - private static String annotationIndexName() { - QueryEntity entity = new QueryEntity(Integer.class, Person.class); - - QueryIndex idx = entity.getIndexes().iterator().next(); - - return QueryUtils.indexName(entity, idx); - } - - /** */ - public static class Person { - /** */ - @QuerySqlField(index = true) - private final String name; - - /** */ - @QuerySqlField - private final int age; - - /** */ - private Person(String name, int age) { - this.name = name; - this.age = age; - } - } -} 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..4053387f73e04 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.CacheConfigurationQueryEntityMergeTest; 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, + + CacheConfigurationQueryEntityMergeTest.class, }) public class IgniteCacheWithIndexingTestSuite { } From 62c7bf6f945649244a8bd4575a4d4aab56458f46 Mon Sep 17 00:00:00 2001 From: "Valuyskiy.O.Y" Date: Fri, 18 Sep 2026 08:11:47 +1000 Subject: [PATCH 3/5] IGNITE-28907 Add corrections to DuplicateKeyValueClassesSelfTest --- .../DuplicateKeyValueClassesSelfTest.java | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java index eb03236714a98..8fe36977a6a1f 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java @@ -17,14 +17,20 @@ package org.apache.ignite.internal.processors.cache.index; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; +import javax.cache.CacheException; +import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.query.annotations.QuerySqlField; import org.apache.ignite.configuration.CacheConfiguration; import org.junit.Test; -/** - * Make sure that cache can start with multiple key-value classes of the same type. - */ +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; + +/** Tests handling of duplicate key and value classes configured through {@link CacheConfiguration#setIndexedTypes}. */ @SuppressWarnings("unchecked") public class DuplicateKeyValueClassesSelfTest extends AbstractIndexingCommonTest { /** Cache name. */ @@ -43,31 +49,52 @@ public class DuplicateKeyValueClassesSelfTest extends AbstractIndexingCommonTest } /** - * Test duplicate key class. + * Checks that the same key class can be used with different value classes. * * @throws Exception If failed. */ @Test - public void testDuplicateKeyClass() throws Exception { + public void testDuplicateKeyClass() { CacheConfiguration ccfg = new CacheConfiguration() .setName(CACHE_NAME) .setIndexedTypes(UUID.class, Clazz1.class, UUID.class, Clazz2.class); grid(0).createCache(ccfg); + + Collection entities = grid(0).context().cache().cacheConfiguration(CACHE_NAME).getQueryEntities(); + + assertEquals(2, entities.size()); + + Set valTypes = new HashSet<>(); + + for (QueryEntity entity : entities) { + assertEquals(UUID.class.getName(), entity.getKeyType()); + + valTypes.add(entity.getValueType()); + } + + assertEquals(new HashSet<>(Arrays.asList(Clazz1.class.getName(), Clazz2.class.getName())), valTypes); } /** - * Test duplicate value class. - * - * @throws Exception If failed. + * Checks that conflicting key types configured for the same value class are rejected instead of silently + * discarding one of the query entity configurations. */ @Test - public void testDuplicateValueClass() throws Exception { + public void testConflictingKeyTypesForSameValueClass() { CacheConfiguration ccfg = new CacheConfiguration() - .setName(CACHE_NAME) - .setIndexedTypes(UUID.class, Clazz1.class, String.class, Clazz1.class); + .setName(CACHE_NAME); - grid(0).createCache(ccfg); + String msg = String.format("Failed to merge query entities due to conflicting metadata " + + "[cacheName=%s, property=keyType, existingValue=%s, incomingValue=%s]", + CACHE_NAME, UUID.class.getName(), String.class.getName()); + + assertThrows( + log, + () -> ccfg.setIndexedTypes(UUID.class, Clazz1.class, String.class, Clazz1.class), + CacheException.class, + msg + ); } /** From c33d5751a83a8c65be07c5e8ac939a5455239d87 Mon Sep 17 00:00:00 2001 From: "Valuyskiy.O.Y" Date: Fri, 18 Sep 2026 08:19:12 +1000 Subject: [PATCH 4/5] IGNITE-28907 Add corrections to DuplicateKeyValueClassesSelfTest --- .../cache/index/DuplicateKeyValueClassesSelfTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java index 8fe36977a6a1f..654b87389d167 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/DuplicateKeyValueClassesSelfTest.java @@ -48,11 +48,7 @@ public class DuplicateKeyValueClassesSelfTest extends AbstractIndexingCommonTest grid(0).destroyCache(CACHE_NAME); } - /** - * Checks that the same key class can be used with different value classes. - * - * @throws Exception If failed. - */ + /** Checks that the same key class can be used with different value classes. */ @Test public void testDuplicateKeyClass() { CacheConfiguration ccfg = new CacheConfiguration() @@ -73,7 +69,9 @@ public void testDuplicateKeyClass() { valTypes.add(entity.getValueType()); } - assertEquals(new HashSet<>(Arrays.asList(Clazz1.class.getName(), Clazz2.class.getName())), valTypes); + Set expValTypes = new HashSet<>(Arrays.asList(Clazz1.class.getName(), Clazz2.class.getName())); + + assertEquals(expValTypes, valTypes); } /** From 9dfe880974ac57dc52a999c564d6b927f248b519 Mon Sep 17 00:00:00 2001 From: "Valuyskiy.O.Y" Date: Fri, 18 Sep 2026 12:01:47 +1000 Subject: [PATCH 5/5] IGNITE-28907 Add corrections to IgnitePdsCorruptedIndexTest and IgnitePdsIndexingDefragmentationTest --- .../IgnitePdsCorruptedIndexTest.java | 2 +- .../IgnitePdsIndexingDefragmentationTest.java | 52 ++++++++++++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCorruptedIndexTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCorruptedIndexTest.java index 31d23a201a53c..a158a9aa6db41 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCorruptedIndexTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsCorruptedIndexTest.java @@ -90,7 +90,7 @@ public class IgnitePdsCorruptedIndexTest extends GridCommonAbstractTest { CacheConfiguration ccfg = new CacheConfiguration<>(CACHE) .setBackups(1) .setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC) - .setIndexedTypes(Integer.class, IndexedObject.class, Long.class, IndexedObject.class) + .setIndexedTypes(Integer.class, IndexedObject.class) .setAffinity(new RendezvousAffinityFunction(false, 32)); cfg.setCacheConfiguration(ccfg); diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsIndexingDefragmentationTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsIndexingDefragmentationTest.java index 9a25794a2de75..b7e3ac5ae3d67 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsIndexingDefragmentationTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/persistence/IgnitePdsIndexingDefragmentationTest.java @@ -19,10 +19,13 @@ import java.io.File; import java.math.BigDecimal; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; +import javax.cache.Cache; import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; import org.apache.ignite.cache.query.SqlFieldsQuery; import org.apache.ignite.cluster.ClusterState; @@ -52,6 +55,9 @@ * Defragmentation tests with enabled ignite-indexing. */ public class IgnitePdsIndexingDefragmentationTest extends IgnitePdsDefragmentationTest { + /** Key type used to configure indexed cache. */ + private Class indexedKeyType = Integer.class; + /** {@inheritDoc} */ @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); @@ -73,19 +79,13 @@ public class IgnitePdsIndexingDefragmentationTest extends IgnitePdsDefragmentati CacheConfiguration cache1Cfg = new CacheConfiguration<>(DEFAULT_CACHE_NAME) .setAtomicityMode(TRANSACTIONAL) .setGroupName(GRP_NAME) - .setIndexedTypes( - IgniteCacheUpdateSqlQuerySelfTest.AllTypes.class, byte[].class, - Integer.class, byte[].class - ) + .setIndexedTypes(indexedKeyType, byte[].class) .setAffinity(new RendezvousAffinityFunction(false, PARTS)); CacheConfiguration cache2Cfg = new CacheConfiguration<>(CACHE_2_NAME) .setAtomicityMode(TRANSACTIONAL) .setGroupName(GRP_NAME) - .setIndexedTypes( - IgniteCacheUpdateSqlQuerySelfTest.AllTypes.class, byte[].class, - Integer.class, byte[].class - ) + .setIndexedTypes(indexedKeyType, byte[].class) .setAffinity(new RendezvousAffinityFunction(false, PARTS)); cache2Cfg.setExpiryPolicyFactory(new PolicyFactory()); @@ -110,15 +110,32 @@ public class IgnitePdsIndexingDefragmentationTest extends IgnitePdsDefragmentati * * @throws Exception If failed. */ - private void test(Function keyMapper) throws Exception { + private void test(Class keyType, Function keyMapper) throws Exception { + indexedKeyType = keyType; + IgniteEx ig = startGrid(0); ig.cluster().state(ClusterState.ACTIVE); CacheConfiguration dfltCacheCfg = ig.cachex(DEFAULT_CACHE_NAME).configuration(); - CacheFileTree cft = ig.context().pdsFolderResolver().fileTree().cacheTree(dfltCacheCfg); - fillCache(keyMapper, ig.cache(DEFAULT_CACHE_NAME)); + Collection qryEntities = dfltCacheCfg.getQueryEntities(); + + assertEquals(1, qryEntities.size()); + + QueryEntity qryEntity = qryEntities.iterator().next(); + + assertEquals(keyType.getName(), qryEntity.getKeyType()); + + IgniteCache cache = ig.cache(DEFAULT_CACHE_NAME); + + fillCache(keyMapper, cache); + + Cache.Entry entry = cache.iterator().next(); + + assertEquals(keyType, entry.getKey().getClass()); + + CacheFileTree cft = ig.context().pdsFolderResolver().fileTree().cacheTree(dfltCacheCfg); forceCheckpoint(ig); @@ -155,7 +172,7 @@ private void test(Function keyMapper) throws Exception { assertFalse(idxRebuild.didRebuildIndexes()); - IgniteCache cache = node.cache(DEFAULT_CACHE_NAME); + cache = node.cache(DEFAULT_CACHE_NAME); assertFalse(completionMarkerFile.exists()); @@ -163,6 +180,10 @@ private void test(Function keyMapper) throws Exception { for (int k = 0; k < ADDED_KEYS_COUNT; k++) cache.get(keyMapper.apply(k)); + + Cache.Entry entryAfterDefragmentation = cache.iterator().next(); + + assertEquals(keyType, entryAfterDefragmentation.getKey().getClass()); } /** @@ -195,7 +216,7 @@ private static void validateIndexes(IgniteEx node) throws Exception { */ @Test public void testIndexingWithIntegerKey() throws Exception { - test(Function.identity()); + test(Integer.class, Function.identity()); } /** @@ -205,7 +226,10 @@ public void testIndexingWithIntegerKey() throws Exception { */ @Test public void testIndexingWithComplexKey() throws Exception { - test(integer -> new IgniteCacheUpdateSqlQuerySelfTest.AllTypes((long)integer)); + test( + IgniteCacheUpdateSqlQuerySelfTest.AllTypes.class, + integer -> new IgniteCacheUpdateSqlQuerySelfTest.AllTypes((long)integer) + ); } /**