[ 
https://issues.apache.org/jira/browse/IGNITE-28907?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Oleg Valuyskiy updated IGNITE-28907:
------------------------------------
    Attachment: MixedIndexConfigurationTest.patch

> Support merging QueryEntity metadata configured through setIndexedTypes and 
> setQueryEntities
> --------------------------------------------------------------------------------------------
>
>                 Key: IGNITE-28907
>                 URL: https://issues.apache.org/jira/browse/IGNITE-28907
>             Project: Ignite
>          Issue Type: Task
>            Reporter: Oleg Valuyskiy
>            Assignee: Oleg Valuyskiy
>            Priority: Major
>              Labels: ise
>         Attachments: MixedIndexConfigurationTest.patch
>
>
> h2. Problem
> {\{CacheConfiguration.setIndexedTypes(...)}} and 
> \{{CacheConfiguration.setQueryEntities(...)}} both populate the same internal 
> collection of \{{QueryEntity}} definitions.
> When both methods configure the same value type, the current implementation 
> treats the second \{{QueryEntity}} as a duplicate based only on its value 
> type and silently ignores it.
> As a result, SQL metadata supplied by the second configuration method is lost.
> The issue is not specific to annotation-based indexes.
> {\{setIndexedTypes(...)}} creates a \{{QueryEntity}} for every configured 
> key/value type pair even when the value class does not contain any 
> \{{@QuerySqlField}} annotations. Therefore, merely configuring a value type 
> through \{{setIndexedTypes(...)}} is enough to prevent a subsequent 
> \{{QueryEntity}} for the same value type from being applied.
> For example:
> {code:java}
> CacheConfiguration<Integer, Person> ccfg =
> new CacheConfiguration<>("person-cache");
> ccfg.setIndexedTypes(Integer.class, Person.class);
> ccfg.setQueryEntities(Collections.singletonList(
> new QueryEntity()
> .setKeyType(Integer.class.getName())
> .setValueType(Person.class.getName())
> .setFields(fields)
> .setIndexes(Collections.singletonList(
> new QueryIndex(Arrays.asList("name", "age"))
> .setName("PERSON_NAME_AGE_IDX")
> ))
> ));
> {code}
> Even if \{{Person}} has no query annotations, \{{setIndexedTypes(...)}} 
> creates a \{{QueryEntity}} for \{{Person}} first.
> The explicit \{{QueryEntity}} supplied later through 
> \{{setQueryEntities(...)}} has the same value type and is silently skipped.
> The cache starts successfully, but \{{PERSON_NAME_AGE_IDX}} is not created.
> If \{{Person}} additionally contains:
> {code:java}
> @QuerySqlField(index = true)
> private String name;
> {code}
> the annotation-derived single-column index is created, while the explicitly 
> configured composite index is still missing.
> Therefore, the actual conflict is between two \{{CacheConfiguration}} APIs:
>  * {\{setIndexedTypes(...)}};
>  * {\{setQueryEntities(...)}};
> rather than between annotations and explicit configuration as such.
> h2. Root cause
> Both methods store query metadata in the same internal \{{qryEntities}} 
> collection.
> The existing behavior is conceptually equivalent to:
> {code:java}
> if (!containsQueryEntityWithSameValueType(newEntity))
> qryEntities.add(newEntity);
> {code}
> If a \{{QueryEntity}} with the same value type is already present:
>  * the entities are not merged;
>  * fields are not compared;
>  * indexes are not merged;
>  * aliases and constraints are not merged;
>  * conflicting metadata is not detected;
>  * the incoming entity is silently ignored.
> This makes the resulting SQL schema incomplete and dependent on the order in 
> which configuration methods are invoked.
> h2. Expected behavior
> When \{{setIndexedTypes(...)}} and \{{setQueryEntities(...)}} configure the 
> same value type, Ignite should attempt to merge the corresponding 
> \{{QueryEntity}} definitions.
> Compatible metadata should be combined.
> Conflicting metadata should result in a \{{CacheException}} instead of 
> silently selecting one definition.
> The merge should be incremental and operate on the effective, already 
> accumulated \{{QueryEntity}}.
> Conceptually:
> {code:java}
> QueryEntity existing = findQueryEntity(valueType);
> if (existing == null)
> qryEntities.add(incoming);
> else
> replaceQueryEntity(existing, mergeQueryEntities(existing, incoming));
> {code}
> This allows multiple complementary calls to \{{setQueryEntities(...)}} to 
> accumulate metadata instead of losing information from previous calls.
> h2. Merge rules
> The following merge semantics should be applied.
> h3. Scalar properties
> For properties such as:
>  * key type;
>  * value type;
>  * table name;
>  * key field name;
>  * value field name;
> the rules are:
> {noformat}
> null + X -> X
> X + null -> X
> X + X -> X
> X + Y -> CacheException
> {noformat}
> h3. Fields
> Field definitions should be merged while preserving the order of the existing 
> entity.
> Fields present only in the incoming entity should be appended.
> For the same field:
>  * equal field types are compatible;
>  * different field types must cause a \{{CacheException}}.
> Example:
> {noformat}
> existing:
> name : String
> age : Integer
> incoming:
> age : Integer
> city : String
> result:
> name : String
> age : Integer
> city : String
> {noformat}
> h3. Indexes
> Indexes with different names should be combined.
> For indexes with the same name:
>  * identical definitions should be deduplicated;
>  * different definitions must cause a \{{CacheException}}.
> The comparison must take the complete index definition into account, 
> including:
>  * indexed fields;
>  * field order;
>  * ascending/descending order;
>  * index type;
>  * inline size where applicable.
> h3. Map-based metadata
> Metadata such as:
>  * aliases;
>  * default field values;
>  * field precision;
>  * field scale;
> should be merged by key.
> For the same key:
>  * equal values are compatible;
>  * different values must cause a \{{CacheException}}.
> h3. Set-based metadata
> Metadata such as:
>  * key fields;
>  * not-null fields;
> should be merged using set union.
> h2. Multiple setQueryEntities calls
> The implementation should support consecutive complementary calls to 
> \{{setQueryEntities(...)}}.
> For example:
> {code:java}
> ccfg.setQueryEntities(Collections.singletonList(entityWithNameIndex));
> ccfg.setQueryEntities(Collections.singletonList(entityWithAgeIndex));
> ccfg.setQueryEntities(Collections.singletonList(entityWithCompositeIndex));
> {code}
> The resulting \{{QueryEntity}} must contain metadata from all three calls.
> The merge must use the current effective entity rather than reconstructing 
> the original entity from \{{indexedTypes}}, otherwise metadata accumulated by 
> previous merge operations can be lost.
> h2. setIndexedTypes state
> {\{setIndexedTypes(...)}} creates a boxed copy of the supplied key/value type 
> array, but the resulting array must also be stored in the \{{indexedTypes}} 
> field.
> The field is already exposed through \{{getIndexedTypes()}} and is also used 
> to prevent repeated \{{setIndexedTypes(...)}} calls.
> The configuration should therefore preserve the successfully applied indexed 
> types:
> {code:java}
> this.indexedTypes = newIndexedTypes;
> {code}
> The assignment should happen only after successful processing of the supplied 
> types.
> h2. Expected result
> For the following configuration:
> {code:java}
> ccfg.setIndexedTypes(Integer.class, Person.class);
> ccfg.setQueryEntities(Collections.singletonList(
> personEntityWithCompositeIndex()
> ));
> {code}
> the resulting SQL schema should contain the explicitly configured composite 
> index even if \{{Person}} has no query annotations.
> If \{{Person}} also declares:
> {code:java}
> @QuerySqlField(index = true)
> private String name;
> {code}
> both indexes should be created:
> {noformat}
> PERSON_NAME_IDX
> PERSON_NAME_AGE_IDX
> {noformat}
> along with the default primary-key index.
> h2. Acceptance criteria
>  * {\{setIndexedTypes(...)}} followed by \{{setQueryEntities(...)}} merges 
> compatible metadata for the same value type.
>  * {\{setQueryEntities(...)}} followed by \{{setIndexedTypes(...)}} also 
> merges compatible metadata.
>  * Multiple complementary \{{setQueryEntities(...)}} calls accumulate 
> metadata.
>  * Different value types remain separate \{{QueryEntity}} definitions.
>  * Fields with the same name and type are deduplicated.
>  * Fields with the same name and different types cause a \{{CacheException}}.
>  * Different indexes are combined.
>  * Identical indexes with the same name are deduplicated.
>  * Indexes with the same name but different definitions cause a 
> \{{CacheException}}.
>  * Compatible aliases, precision, scale, defaults, key fields and not-null 
> fields are merged.
>  * Conflicting scalar or map-based metadata causes a \{{CacheException}}.
>  * Annotation-derived metadata created through \{{setIndexedTypes(...)}} can 
> be combined with explicitly configured metadata from 
> \{{setQueryEntities(...)}}.
>  * SQL metadata is no longer silently discarded based only on duplicate value 
> type.
>  * {\{getIndexedTypes()}} returns the types successfully configured through 
> \{{setIndexedTypes(...)}}.
>  * Integration tests verify that merged index definitions are actually 
> registered in the SQL schema and exposed through the \{{INDEXES}} system view.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to