From f8c2206e834254c33b99d01c0b709743525fbd01 Mon Sep 17 00:00:00 2001 From: dragonfsky Date: Tue, 5 May 2026 19:22:19 +0800 Subject: [PATCH] Introduce explicit index field matching methods. Closes #5187 Signed-off-by: dragonfsky --- .../data/mongodb/core/index/IndexInfo.java | 70 ++++++++++++++++++- .../core/index/IndexInfoUnitTests.java | 43 +++++++++++- ...positoryIndexCreationIntegrationTests.java | 3 +- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index 6714317b24..23168d2d10 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.bson.Document; @@ -41,6 +42,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Mark Paluch + * @author dragonfsky */ public class IndexInfo { @@ -182,16 +184,80 @@ public List getIndexFields() { } /** - * Returns whether the index is covering exactly the fields given independently of the order. + * Returns whether the index contains all given field keys independently of their position. Field keys are compared as + * a set and therefore repeated input keys are ignored. * * @param keys must not be {@literal null}. * @return + * @since 5.1 */ + public boolean containsAllFields(Collection keys) { + + Assert.notNull(keys, "Collection of keys must not be null"); + + return getIndexFieldKeys().containsAll(keys); + } + + /** + * Returns whether the index contains all given field keys independently of their position. Field keys are compared as + * a set and therefore repeated input keys are ignored. + * + * @param keys must not be {@literal null}. + * @return + * @deprecated since 5.1. Use {@link #containsAllFields(Collection)}, {@link #isIndexForFieldsExactly(Collection)}, or + * {@link #coversFields(Collection)} to express the intended index field matching semantics. + */ + @Deprecated(since = "5.1") public boolean isIndexForFields(Collection keys) { + return containsAllFields(keys); + } + + /** + * Returns whether the index matches exactly the given field keys independently of their order. Field keys are compared + * as a set and therefore repeated input keys are ignored. + * + * @param keys must not be {@literal null}. + * @return + * @since 5.1 + */ + public boolean isIndexForFieldsExactly(Collection keys) { Assert.notNull(keys, "Collection of keys must not be null"); - return this.indexFields.stream().map(IndexField::getKey).collect(Collectors.toSet()).containsAll(keys); + Set indexFieldKeys = getIndexFieldKeys(); + Set keysToCheck = keys.stream().collect(Collectors.toSet()); + + return indexFieldKeys.size() == keysToCheck.size() && indexFieldKeys.containsAll(keysToCheck); + } + + /** + * Returns whether the given field keys are covered by this index according to compound-index prefix field matching. + * Field keys are compared as a set and therefore repeated input keys are ignored. This method matches + * {@link IndexField#getKey() index field keys} only and does not evaluate special query semantics of text, geo, or + * wildcard indexes. + * + * @param keys must not be {@literal null}. + * @return + * @since 5.1 + */ + public boolean coversFields(Collection keys) { + + Assert.notNull(keys, "Collection of keys must not be null"); + + Set keysToCheck = keys.stream().collect(Collectors.toSet()); + + if (keysToCheck.size() > indexFields.size()) { + return false; + } + + Set indexFieldPrefix = indexFields.stream().limit(keysToCheck.size()).map(IndexField::getKey) + .collect(Collectors.toSet()); + + return indexFieldPrefix.containsAll(keysToCheck); + } + + private Set getIndexFieldKeys() { + return this.indexFields.stream().map(IndexField::getKey).collect(Collectors.toSet()); } public String getName() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java index a9bae5ec64..0f2691254e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java @@ -30,6 +30,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Stefan Tirea + * @author dragonfsky */ class IndexInfoUnitTests { @@ -50,14 +51,52 @@ class IndexInfoUnitTests { } """; - @Test - void isIndexForFieldsCorrectly() { + @Test // GH-5187 + @SuppressWarnings("deprecation") + void isIndexForFieldsRetainsContainsAllBehavior() { IndexField fooField = IndexField.create("foo", Direction.ASC); IndexField barField = IndexField.create("bar", Direction.DESC); IndexInfo info = new IndexInfo(Arrays.asList(fooField, barField), "myIndex", false, false, ""); assertThat(info.isIndexForFields(Arrays.asList("foo", "bar"))).isTrue(); + assertThat(info.isIndexForFields(Arrays.asList("foo"))).isTrue(); + } + + @Test // GH-5187 + void containsAllFieldsReturnsTrueForFieldSubset() { + + IndexInfo info = new IndexInfo(Arrays.asList(IndexField.create("foo", Direction.ASC), + IndexField.create("bar", Direction.DESC)), "myIndex", false, false, ""); + + assertThat(info.containsAllFields(Arrays.asList("foo"))).isTrue(); + assertThat(info.containsAllFields(Arrays.asList("bar", "foo"))).isTrue(); + assertThat(info.containsAllFields(Arrays.asList("foo", "baz"))).isFalse(); + } + + @Test // GH-5187 + void isIndexForFieldsExactlyRequiresSameFields() { + + IndexInfo info = new IndexInfo(Arrays.asList(IndexField.create("foo", Direction.ASC), + IndexField.create("bar", Direction.DESC)), "myIndex", false, false, ""); + + assertThat(info.isIndexForFieldsExactly(Arrays.asList("foo", "bar"))).isTrue(); + assertThat(info.isIndexForFieldsExactly(Arrays.asList("bar", "foo"))).isTrue(); + assertThat(info.isIndexForFieldsExactly(Arrays.asList("foo"))).isFalse(); + assertThat(info.isIndexForFieldsExactly(Arrays.asList("foo", "bar", "baz"))).isFalse(); + } + + @Test // GH-5187 + void coversFieldsOnlyMatchesIndexPrefixes() { + + IndexInfo info = new IndexInfo(Arrays.asList(IndexField.create("foo", Direction.ASC), + IndexField.create("bar", Direction.DESC), IndexField.create("baz", Direction.ASC)), "myIndex", false, + false, ""); + + assertThat(info.coversFields(Arrays.asList("foo"))).isTrue(); + assertThat(info.coversFields(Arrays.asList("bar", "foo"))).isTrue(); + assertThat(info.coversFields(Arrays.asList("bar"))).isFalse(); + assertThat(info.coversFields(Arrays.asList("foo", "baz"))).isFalse(); } @Test // DATAMONGO-2170 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java index a0f12d56c4..dcbf4ea219 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java @@ -41,6 +41,7 @@ * Integration test for index creation for query methods. * * @author Oliver Gierke + * @author dragonfsky */ @RunWith(SpringRunner.class) @ContextConfiguration @@ -84,7 +85,7 @@ public void testname() { private static void assertHasIndexForField(List indexInfo, String... fields) { for (IndexInfo info : indexInfo) { - if (info.isIndexForFields(Arrays.asList(fields))) { + if (info.containsAllFields(Arrays.asList(fields))) { return; } }