Skip to content

Compound Wildcard Indexes #4790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.4.0-SNAPSHOT</version>
<version>4.4.0-4471-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
2 changes: 1 addition & 1 deletion spring-data-mongodb-benchmarks/pom.xml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.4.0-SNAPSHOT</version>
<version>4.4.0-4471-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.4.0-SNAPSHOT</version>
<version>4.4.0-4471-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.4.0-SNAPSHOT</version>
<version>4.4.0-4471-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Original file line number Diff line number Diff line change
@@ -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. <br />
*
* <pre class="code">
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardFieldName = "address", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
*
* db.product.createIndex({"address.$**": 1, "firstname": 1})
* </pre>
*
* {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
*
* <pre class="code">
*
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardProjection = "{'address.zip': 0}", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
*
* db.user.createIndex({"$**": 1, "firstname": 1}, {"wildcardProjection": {"address.zip": 0}})
* </pre>
*
* @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.
* <br />
* <strong>NOTE:</strong> 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). <br />
*
* <pre class="code">
* &#64;Document
* &#64;CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
* class Person {
* String firstname;
* Address address;
* }
* </pre>
*
* @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}. <br />
*
* @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}. <br />
*
* @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 "";
}
Original file line number Diff line number Diff line change
@@ -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 $**}.
* <br />
*
* @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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Can be used natively, declaring several nested {@link CompoundWildcardIndex} annotations. Can also be used in conjunction
* with Java 8's support for <em>repeatable annotations</em>, 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();

}
Original file line number Diff line number Diff line change
@@ -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<IndexDefinitionHolder> 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<MongoPersistentProperty>) 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<IndexDefinitionHolder> resolveIndexForClass(TypeInformation<?> type, String dotPath, Path path,
String collection, CycleGuard guard) {

return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard);
}

private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, String dotPath, Path path,
String collection, CycleGuard guard) {

@@ -280,7 +295,8 @@ private List<IndexDefinitionHolder> createIndexDefinitionHolderForProperty(Strin
private List<IndexDefinitionHolder> 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<IndexDefinitionHolder> potentiallyCreateCompoundIndexDefinitions(St
private List<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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) {

Original file line number Diff line number Diff line change
@@ -179,7 +179,7 @@ public WildcardIndex wildcardProjection(Map<String, Object> includeExclude) {
}

private String getTargetFieldName() {
return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**";
return (StringUtils.hasText(fieldName) && !CompoundWildcardIndex.ALL_FIELDS.equals(fieldName)) ? (fieldName + ".$**") : "$**";
}

@Override
Original file line number Diff line number Diff line change
@@ -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.
* <br />
* <strong>NOTE:</strong> Can only be applied on root level documents.
*
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Can be used natively, declaring several nested {@link WildcardIndexed} annotations. Can also be used in conjunction
* with Java 8's support for <em>repeatable annotations</em>, 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();

}
Original file line number Diff line number Diff line change
@@ -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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<IndexDefinitionHolder> 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<String, String> value;

Map<String, String> value2;

}

@WildcardIndexes({ @WildcardIndexed(name = "foo", partialFilter = "{ '$eq' : 1 }", collation = "en_US"),
@WildcardIndexed(name = "bar", partialFilter = "{ '$eq' : 0 }", collation = "en_UK") })
@Document
class WithWildcardIndexes {

Map<String, String> value;

Map<String, String> value2;

}

@Document
class WildcardIndexedProjectionOnNestedPath {