diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index fca6ae5b7..0e5c6501b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -111,6 +111,26 @@ class AotPlaceholders { return new RegexPlaceholder(index, options); } + /** + * Create a placeholder that indicates the value should be treated as list. + * + * @param index zero-based index referring to the bindable method parameter. + * @return new instance of {@link Placeholder}. + */ + static Placeholder asList(int index) { + return asList(indexed(index)); + } + + /** + * Create a placeholder that indicates the wrapped placeholder should be treated as list. + * + * @param source the target placeholder + * @return new instance of {@link Placeholder}. + */ + static Placeholder asList(Placeholder source) { + return new AsListPlaceholder(source); + } + /** * A placeholder expression used when rending queries to JSON. * @@ -295,4 +315,17 @@ class AotPlaceholders { } } + record AsListPlaceholder(Placeholder placeholder) implements Placeholder { + + @Override + public String toString() { + return getValue(); + } + + @Override + public String getValue() { + return "[" + placeholder.getValue() + "]"; + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index b7d4439d7..d37f78c60 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -24,7 +24,6 @@ import java.util.regex.Pattern; import org.bson.conversions.Bson; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; - import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -48,6 +47,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.AsListPlaceholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; @@ -132,6 +132,10 @@ record AotQueryCreator(MappingContext mappingContext return criteria.raw("$regex", param); } + if (param instanceof AsListPlaceholder asListPlaceholder && !property.isCollectionLike()) { + return super.createContainingCriteria(part, property, criteria, asListPlaceholder.placeholder()); + } + return super.createContainingCriteria(part, property, criteria, param); } } @@ -226,7 +230,24 @@ record AotQueryCreator(MappingContext mappingContext partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS) || partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.WHEN_POSSIBLE) ? "i" : null)); - } else { + } else if (partForIndex != null && (partForIndex.getType().equals(Type.IN) + || partForIndex.getType().equals(Type.NOT_IN) || partForIndex.getType().equals(Type.CONTAINING) + || partForIndex.getType().equals(Type.NOT_CONTAINING))) { + + if (partForIndex.getProperty().isCollection() + && !TypeInformation.of(parameter.getType()).isCollectionLike()) { + if (partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS)) { + placeholders.add(parameter.getIndex(), + AotPlaceholders.asList(AotPlaceholders.regex(parameter.getIndex(), "i"))); + } else { + placeholders.add(parameter.getIndex(), AotPlaceholders.asList(parameter.getIndex())); + } + } else { + placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + } + } + + else { placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index 0e8fae6f8..b5c87011e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java @@ -22,12 +22,12 @@ import java.util.Set; import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.AsListPlaceholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.util.StringUtils; @@ -83,7 +83,7 @@ class AotStringQuery extends Query { return false; } - return this.placeholders.get(index) instanceof RegexPlaceholder; + return obtainAndPotentiallyUnwrapPlaceholder(index) instanceof RegexPlaceholder; } @Nullable @@ -92,7 +92,21 @@ class AotStringQuery extends Query { return null; } - return this.placeholders.get(index) instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + Object placeholderValue = obtainAndPotentiallyUnwrapPlaceholder(index); + return placeholderValue instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + } + + @Nullable Object obtainAndPotentiallyUnwrapPlaceholder(int index) { + + if (this.placeholders.isEmpty()) { + return null; + } + + Object placeholerValue = this.placeholders.get(index); + if (placeholerValue instanceof AsListPlaceholder asListPlaceholder) { + placeholerValue = asListPlaceholder.placeholder(); + } + return placeholerValue; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 84025466c..b27939c1f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -195,9 +195,9 @@ class QueryBlocks { String regexOptions = source.getQuery().getRegexOptions(i); if (StringUtils.hasText(regexOptions)) { - formatted.add(CodeBlock.of("toRegex($L)", parameterName)); - } else { formatted.add(CodeBlock.of("toRegex($L, $S)", parameterName, regexOptions)); + } else { + formatted.add(CodeBlock.of("toRegex($L)", parameterName)); } } else { formatted.add(CodeBlock.of("$L", parameterName)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 5be81d930..b6cac676b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1341,6 +1341,20 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(result).hasSize(1).contains(carter); } + @Test // GH-5123 + void findBySkillsContainsSingleElement() { + + List result = repository.findBySkillsContains("Drums"); + assertThat(result).hasSize(1).contains(carter); + } + + @Test // GH-5123 + void findBySkillsContainsSingleElementWithIgnoreCase() { + + List result = repository.findBySkillsContainsIgnoreCase("drums"); + assertThat(result).hasSize(1).contains(carter); + } + @Test // DATAMONGO-1425 void findBySkillsNotContains() { @@ -1349,6 +1363,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(result).doesNotContain(carter); } + @Test // GH-5123 + void findBySkillsNotContainsSingleElement() { + + List result = repository.findBySkillsNotContains("Drums"); + assertThat(result).hasSize((int) (repository.count() - 1)); + assertThat(result).doesNotContain(carter); + } + @Test // DATAMONGO-1424 void findsPersonsByFirstnameNotLike() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index cef1af1c3..f46c9153d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -117,8 +117,11 @@ public interface PersonRepository extends MongoRepository, Query List findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort); List findBySkillsContains(List skills); + List findBySkillsContains(String skill); + List findBySkillsContainsIgnoreCase(String skill); List findBySkillsNotContains(List skills); + List findBySkillsNotContains(String skill); @Query("{'age' : { '$lt' : ?0 } }") List findByAgeLessThan(int age, Sort sort);