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
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", collection, root));
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions("", collection, root));
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root, collection));
+ indexInformation.addAll(potentiallyCreateCompoundWildcardDefinition(root, collection));
root.doWithProperties((PropertyHandler) 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 extends IndexDefinitionHolder> potentiallyCreateTextIndexDefinition(
@@ -345,6 +371,31 @@ private Collection extends IndexDefinitionHolder> potentiallyCreateTextIndexDe
}
+ private Collection extends IndexDefinitionHolder> 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 {