diff --git a/pom.xml b/pom.xml index 3fa54424af..d45de05d01 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index a3dc49f892..b5a14bf06c 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index acdc13437d..8078eb0d42 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index fafe9c8793..6c33055163 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java new file mode 100644 index 0000000000..57a42b2108 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java @@ -0,0 +1,138 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.mongodb.core.index; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a class to use compound wildcard indexes.
+ * + *
+ * @Document
+ * @CompoundWildcardIndexed(wildcardFieldName = "address", fields = "{'firstname': 1}")
+ * class Person {
+ * 	String firstname;
+ * 	Address address;
+ * }
+ *
+ * db.product.createIndex({"address.$**": 1, "firstname": 1})
+ * 
+ * + * {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index. + * + *
+ *
+ * @Document
+ * @CompoundWildcardIndexed(wildcardProjection = "{'address.zip': 0}", fields = "{'firstname': 1}")
+ * class Person {
+ * 	String firstname;
+ * 	Address address;
+ * }
+ *
+ * db.user.createIndex({"$**": 1, "firstname": 1}, {"wildcardProjection": {"address.zip": 0}})
+ * 
+ * + * @author Julia Lee + * @author Marcin Grzejszczak + * @since 4.4 + */ +@Target({ ElementType.TYPE }) +@Documented +@CompoundIndex +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(CompoundWildcardIndexes.class) +public @interface CompoundWildcardIndex { + + /** + * Represents wildcard for all fields starting from the root od the document. + */ + String ALL_FIELDS = "$**"; + + /** + * The name of the sub-field to which a wildcard index is applied. The default value scans all fields. + * + * @return {@link #ALL_FIELDS} by default. + */ + String wildcardFieldName() default ALL_FIELDS; + + /** + * Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) parsable} String. + *
+ * NOTE: Can only be applied on when wildcard term is {@link #ALL_FIELDS} + * + * @return empty by default. + */ + String wildcardProjection() default ""; + + /** + * Definition of non-wildcard index(es) in JSON format, wherein the keys are the fields to be indexed and the values + * define the index direction (1 for ascending, -1 for descending).
+ * + *
+	 * @Document
+	 * @CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
+	 * class Person {
+	 * 	String firstname;
+	 * 	Address address;
+	 * }
+	 * 
+ * + * @return empty String by default. + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "def") + String fields(); + + /** + * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template + * expression}.
+ * + * @return empty by default. + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "name") + String name() default ""; + + /** + * If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults + * to {@literal false}. + * + * @return {@literal false} by default + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "useGeneratedName") + boolean useGeneratedName() default false; + + /** + * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}.
+ * + * @return empty by default. + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "partialFilter") + String partialFilter() default ""; + + /** + * Defines the collation to apply. + * + * @return an empty {@link String} by default. + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "collation") + String collation() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java new file mode 100644 index 0000000000..a30323ef17 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.mongodb.core.index; + +import org.bson.Document; +import org.springframework.util.Assert; + +/** + * {@link CompoundWildcardIndexDefinition} is a specific {@link Index} that includes one {@link WildcardIndex} and + * one or more non-wildcard fields. + * + * @author Julia Lee + * @since 4.4 + */ +public class CompoundWildcardIndexDefinition extends WildcardIndex { + + private final Document indexKeys; + + /** + * Creates a new {@link CompoundWildcardIndexDefinition} for the given {@literal wildcardPath} and {@literal keys}. + * If {@literal wildcardPath} is empty, the wildcard index will apply to the root entity, using {@code $**}. + *
+ * + * @param wildcardPath can be a {@literal empty} {@link String}. + */ + public CompoundWildcardIndexDefinition(String wildcardPath, Document indexKeys) { + + super(wildcardPath); + this.indexKeys = indexKeys; + } + + @Override + public Document getIndexKeys() { + + Document document = new Document(); + document.putAll(indexKeys); + document.putAll(super.getIndexKeys()); + return document; + } + + @Override + public Document getIndexOptions() { + + Document options = super.getIndexOptions(); + return options; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java new file mode 100644 index 0000000000..df3f398212 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.mongodb.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that allows to collect multiple {@link CompoundWildcardIndex} annotations. + *

+ * Can be used natively, declaring several nested {@link CompoundWildcardIndex} annotations. Can also be used in conjunction + * with Java 8's support for repeatable annotations, where {@link CompoundWildcardIndex} can simply be declared several + * times on the same {@linkplain ElementType#TYPE type}, implicitly generating this container annotation. + * + * @author Marcin Grzejszczak + * @since 4.4 + */ +@Target({ ElementType.TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface CompoundWildcardIndexes { + + CompoundWildcardIndex[] value(); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 86c896e7ff..25de1d3fc9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -79,6 +79,7 @@ * @author Mark Paluch * @author Dave Perryman * @author Stefan Tirea + * @author Julia Lee * @since 1.5 */ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -129,6 +130,7 @@ public List resolveIndexForEntity(MongoPersistentEntity) property -> this .potentiallyAddIndexForProperty(root, property, indexInformation, new CycleGuard())); @@ -154,6 +156,36 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { } } }); + + if (entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { + CompoundWildcardIndexes indexes = entity.getRequiredAnnotation(CompoundWildcardIndexes.class); + for (CompoundWildcardIndex compoundWildcardIndex : indexes.value()) { + checkSingleIndex(compoundWildcardIndex); + } + + } + if (entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + checkSingleIndex(entity.getRequiredAnnotation(CompoundWildcardIndex.class)); + } + } + + private static void checkSingleIndex(CompoundWildcardIndex indexed) { + + if (!isWildcardFromRoot(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException( + String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s", + indexed.wildcardFieldName())); + } + + if (isWildcardFromRoot(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException("CompoundWildcardIndex.wildcardProjection is required on \"$**\""); + } + } + + private static boolean isWildcardFromRoot(String fieldName) { + return CompoundWildcardIndex.ALL_FIELDS.equals(fieldName); } private void potentiallyAddIndexForProperty(MongoPersistentEntity root, MongoPersistentProperty persistentProperty, @@ -182,23 +214,6 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo } } - /** - * Recursively resolve and inspect properties of given {@literal type} for indexes to be created. - * - * @param type - * @param dotPath The {@literal "dot} path. - * @param path {@link PersistentProperty} path for cycle detection. - * @param collection - * @param guard - * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property - * types. Will never be {@code null}. - */ - private List resolveIndexForClass(TypeInformation type, String dotPath, Path path, - String collection, CycleGuard guard) { - - return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard); - } - private List resolveIndexForEntity(MongoPersistentEntity entity, String dotPath, Path path, String collection, CycleGuard guard) { @@ -280,7 +295,8 @@ private List createIndexDefinitionHolderForProperty(Strin private List potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection, MongoPersistentEntity entity) { - if (entity.findAnnotation(CompoundIndexes.class) == null && entity.findAnnotation(CompoundIndex.class) == null) { + if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class)) + || entity.isAnnotationPresent(CompoundWildcardIndex.class) || entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { return Collections.emptyList(); } @@ -290,13 +306,23 @@ private List potentiallyCreateCompoundIndexDefinitions(St private List potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection, MongoPersistentEntity entity) { - if (!entity.isAnnotationPresent(WildcardIndexed.class)) { + if ((!entity.isAnnotationPresent(WildcardIndexed.class) && !entity.isAnnotationPresent(WildcardIndexes.class)) + || entity.isAnnotationPresent(CompoundWildcardIndex.class) || entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { return Collections.emptyList(); } - return Collections.singletonList(new IndexDefinitionHolder(dotPath, - createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity), - collection)); + WildcardIndexes wildcardIndexes = entity.findAnnotation(WildcardIndexes.class); + if (wildcardIndexes == null) { + return Collections.singletonList(new IndexDefinitionHolder(dotPath, + createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity), + collection)); + } + List holders = new ArrayList<>(); + for (WildcardIndexed indexed : wildcardIndexes.value()) { + holders.add(new IndexDefinitionHolder(dotPath, + createWildcardIndexDefinition(dotPath, collection, indexed, entity), collection)); + } + return holders; } private Collection potentiallyCreateTextIndexDefinition( @@ -345,6 +371,31 @@ private Collection potentiallyCreateTextIndexDe } + private Collection potentiallyCreateCompoundWildcardDefinition( + MongoPersistentEntity entity, String collection) { + + boolean singleIndexAnnotationPresent = entity.isAnnotationPresent(CompoundWildcardIndex.class); + boolean indexesAnnotationPresent = entity.isAnnotationPresent(CompoundWildcardIndexes.class); + if (!singleIndexAnnotationPresent && !indexesAnnotationPresent) { + return Collections.emptyList(); + } + + List definitions = new ArrayList<>(); + if (indexesAnnotationPresent) { + CompoundWildcardIndexes annotation = entity.getRequiredAnnotation(CompoundWildcardIndexes.class); + for (CompoundWildcardIndex index : annotation.value()) { + definitions.add(createCompoundWildcardIndexDefinition(collection, index, entity)); + } + + } + if (singleIndexAnnotationPresent) { + CompoundWildcardIndex compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndex.class); + definitions.add(createCompoundWildcardIndexDefinition(collection, + compoundWildcardIndex, entity)); + } + return definitions; + } + private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) { @@ -483,6 +534,30 @@ protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, St return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } + protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndex index, + @Nullable MongoPersistentEntity entity) { + + String wildcardField = index.wildcardFieldName(); + org.bson.Document indexKeys = resolveCompoundIndexKeyFromStringDefinition("", index.fields(), entity); + + CompoundWildcardIndexDefinition indexDefinition = new CompoundWildcardIndexDefinition(wildcardField, indexKeys); + + if (StringUtils.hasText(index.wildcardProjection()) && isWildcardFromRoot(wildcardField)) { + indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity)); + } + + if (StringUtils.hasText(index.partialFilter())) { + indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); + } + + if (!index.useGeneratedName()) { + indexDefinition.named(pathAwareIndexName(index.name(), "", entity, null)); + } + + indexDefinition.collation(resolveCollation(index, entity)); + return new IndexDefinitionHolder("", indexDefinition, collection); + } + private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString, PersistentEntity entity) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index 0668bd5926..4a64131820 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -179,7 +179,7 @@ public WildcardIndex wildcardProjection(Map includeExclude) { } private String getTargetFieldName() { - return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**"; + return (StringUtils.hasText(fieldName) && !CompoundWildcardIndex.ALL_FIELDS.equals(fieldName)) ? (fieldName + ".$**") : "$**"; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java index 042f6f4f53..2c095f0752 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java @@ -17,6 +17,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -86,6 +87,7 @@ @Documented @Target({ ElementType.TYPE, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) +@Repeatable(WildcardIndexes.class) public @interface WildcardIndexed { /** @@ -117,7 +119,7 @@ String partialFilter() default ""; /** - * Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String. + * Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String. *
* NOTE: Can only be applied on root level documents. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java new file mode 100644 index 0000000000..fc2d56b1f2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.mongodb.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that allows to collect multiple {@link WildcardIndexed} annotations. + *

+ * Can be used natively, declaring several nested {@link WildcardIndexed} annotations. Can also be used in conjunction + * with Java 8's support for repeatable annotations, where {@link WildcardIndexed} can simply be declared several + * times on the same {@linkplain ElementType#TYPE type}, implicitly generating this container annotation. + * + * @author Marcin Grzejszczak + * @since 4.4 + */ +@Target({ ElementType.TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface WildcardIndexes { + + WildcardIndexed[] value(); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 0cfb8bd09f..4067ef4952 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -29,6 +29,8 @@ import java.util.Map; import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @@ -40,6 +42,7 @@ import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundWildcardIndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests; @@ -54,6 +57,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.StringUtils; /** * Tests for {@link MongoPersistentEntityIndexResolver}. @@ -62,10 +66,11 @@ * @author Mark Paluch * @author Dave Perryman * @author Stefan Tirea + * @author Julia Lee */ @RunWith(Suite.class) @SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class, - TextIndexedResolutionTests.class, MixedIndexResolutionTests.class }) + CompoundWildcardIndexResolutionTests.class, TextIndexedResolutionTests.class, MixedIndexResolutionTests.class }) @SuppressWarnings("unused") public class MongoPersistentEntityIndexResolverUnitTests { @@ -601,7 +606,7 @@ public void compoundIndexOnSuperClassResolvedCorrectly() { public void compoundIndexDoesNotSpecifyNameWhenUsingGenerateName() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( - ComountIndexWithAutogeneratedName.class); + CompoundIndexWithAutogeneratedName.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); assertThat(indexDefinition.getIndexOptions()) @@ -766,10 +771,10 @@ class SingleCompoundIndex {} class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero {} - @Document("ComountIndexWithAutogeneratedName") + @Document("CompoundIndexWithAutogeneratedName") @CompoundIndexes({ @CompoundIndex(useGeneratedName = true, def = "{'foo': 1, 'bar': -1}", background = true, sparse = true, unique = true) }) - class ComountIndexWithAutogeneratedName {} + class CompoundIndexWithAutogeneratedName {} @Document("WithComposedAnnotation") @ComposedCompoundIndex @@ -829,6 +834,173 @@ class WithCompoundCollationFromDocument {} class WithEvaluatedCollationFromCompoundIndex {} } + /** + * Test resolution of {@link CompoundWildcardIndex}. + * + * @author Julia Lee + */ + public static class CompoundWildcardIndexResolutionTests { + + @Test // GH-4471 + public void compoundWildcardIndexOnSingleField() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexOnFields.class); + + assertThat(indexDefinitions).hasSize(1); + assertIndexPathAndCollection(new String[] { "foo.$**", "bar", "baz" }, "CompoundWildcardIndexOnSingleField", + indexDefinitions.get(0)); + } + + @ParameterizedTest // GH-4471 + @ValueSource(classes = {RepeatableCompoundWildcardIndex.class, RepeatableCompoundWildcardIndexThroughIndexes.class}) + public void compoundWildcardIndexOnSingleField(Class clazz) { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(clazz); + + assertThat(indexDefinitions).hasSize(2); + assertIndexPathAndCollection(new String[] { "foo1.$**", "bar1", "baz1" }, StringUtils.uncapitalize(clazz.getSimpleName()), + indexDefinitions.get(0)); + assertIndexPathAndCollection(new String[] { "foo2.$**", "bar2", "baz2" }, StringUtils.uncapitalize(clazz.getSimpleName()), + indexDefinitions.get(1)); + } + + @Test // GH-4471 + public void compoundWildcardIndexOnEntityWithProjection() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexOnEntity.class); + + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("bar", -1)); + assertThat(it.getIndexOptions()).containsEntry("wildcardProjection", + org.bson.Document.parse("{'foo.something' : 0}")); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithOptions() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithOptions.class); + + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("foo", 1)); + + org.bson.Document indexOptions = it.getIndexOptions(); + assertThat(indexOptions).containsEntry("name", "my_index_name"); + assertThat(indexOptions).containsEntry("wildcardProjection", org.bson.Document.parse("{'bar.something' : 1}")); + assertThat(indexOptions).containsEntry("collation", + new org.bson.Document().append("locale", "en_US").append("strength", 2)); + assertThat(indexOptions).containsEntry("partialFilterExpression", + org.bson.Document.parse("{'value': {'$exists': true}}")); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithCollationFromDocumentAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithCollationOnDocument.class); + + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1)); + assertThat(it.getIndexOptions()).containsEntry("collation", + new org.bson.Document().append("locale", "en_US").append("strength", 2)); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithEvaluatedCollationFromAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithEvaluatedCollation.class); + + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1)); + assertThat(it.getIndexOptions()).containsEntry("collation", new org.bson.Document().append("locale", "de_AT")); + }); + } + + @Test // GH-4471 + public void rejectsWildcardProjectionOnSingleField() { + + assertThatExceptionOfType(MappingException.class).isThrownBy(() -> + prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnFieldWithProjection.class)); + } + + @Test // GH-4471 + public void requiresWildcardProjectionOnEntireEntity() { + + assertThatExceptionOfType(MappingException.class).isThrownBy(() -> + prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnEntityWithoutProjection.class)); + } + + @Test // GH-4471 + public void resolvesMultipleIndexesWithCompoundWildcardIndex() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + MultipleIndexes.class); + + assertThat(indexDefinitions).hasSize(2); + + assertThat(indexDefinitions.get(0).getIndexDefinition()).isInstanceOf(CompoundWildcardIndexDefinition.class); + assertThat(indexDefinitions.get(1).getIndexDefinition()).isInstanceOf(Index.class); + + assertThat(indexDefinitions.get(0).getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1) + .append("bar", 1)); + assertThat(indexDefinitions.get(1).getIndexKeys()).isEqualTo(new org.bson.Document().append("one", 1)); + } + + @Document("CompoundWildcardIndexOnSingleField") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}") + class CompoundWildcardIndexOnFields {} + + @Document + @CompoundWildcardIndex(wildcardFieldName = "foo1", fields = "{'bar1': 1, 'baz1': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo2", fields = "{'bar2': 1, 'baz2': 1}") + class RepeatableCompoundWildcardIndex {} + + @Document + @CompoundWildcardIndexes({@CompoundWildcardIndex(wildcardFieldName = "foo1", fields = "{'bar1': 1, 'baz1': 1}"), + @CompoundWildcardIndex(wildcardFieldName = "foo2", fields = "{'bar2': 1, 'baz2': 1}")}) + class RepeatableCompoundWildcardIndexThroughIndexes {} + + @Document + @CompoundWildcardIndex(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}") + class IncorrectCompoundWildcardIndexOnFieldWithProjection {} + + @Document + @CompoundWildcardIndex(fields = "{ 'bar': 1 }") + class IncorrectCompoundWildcardIndexOnEntityWithoutProjection {} + + @Document + @CompoundWildcardIndex(wildcardProjection = "{'foo.something' : 0}", fields = "{'bar': -1}") + class CompoundWildcardIndexOnEntity {} + + @Document + @CompoundWildcardIndex(fields = "{'foo': 1}", wildcardProjection = "{'bar.something': 1}", name = "my_index_name", + collation = "{'locale': 'en_US', 'strength': 2}", partialFilter = "{'value': {'$exists': true}}") + class CompoundWildcardIndexWithOptions {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}") + class CompoundWildcardIndexWithCollationOnDocument {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}", collation = "#{{'locale' : 'de' + '_' + 'AT'}}") + class CompoundWildcardIndexWithEvaluatedCollation {} + + @Document + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}") + class MultipleIndexes { + @Indexed String one; + } + + } + public static class TextIndexedResolutionTests { @Test // DATAMONGO-937 @@ -1424,6 +1596,26 @@ public void resolvesWildcardOnProperty() { }); } + @ParameterizedTest // GH-4471 + @ValueSource(classes = { WithRepeatableWildcardIndex.class, WithWildcardIndexes.class}) + public void resolvesRepeatableWildcards(Class clazz) { + + List indices = prepareMappingContextAndResolveIndexForType(clazz); + assertThat(indices).hasSize(2); + assertThat(indices.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("$**", 1); + assertThat(it.getIndexOptions()).containsEntry("name", "foo") + .containsEntry("collation", new org.bson.Document("locale", "en_US")) + .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 1)); + }); + assertThat(indices.get(1)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("$**", 1); + assertThat(it.getIndexOptions()).containsEntry("name", "bar") + .containsEntry("collation", new org.bson.Document("locale", "en_UK")) + .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 0)); + }); + } + @Test // GH-3225 public void resolvesWildcardTypeOfNestedProperty() { @@ -1778,6 +1970,28 @@ class WithWildCardIndexOnProperty { } + @WildcardIndexed(name = "foo", partialFilter = "{ '$eq' : 1 }", collation = "en_US") + @WildcardIndexed(name = "bar", partialFilter = "{ '$eq' : 0 }", collation = "en_UK") + @Document + class WithRepeatableWildcardIndex { + + Map value; + + Map value2; + + } + + @WildcardIndexes({ @WildcardIndexed(name = "foo", partialFilter = "{ '$eq' : 1 }", collation = "en_US"), + @WildcardIndexed(name = "bar", partialFilter = "{ '$eq' : 0 }", collation = "en_UK") }) + @Document + class WithWildcardIndexes { + + Map value; + + Map value2; + + } + @Document class WildcardIndexedProjectionOnNestedPath {