diff --git a/pom.xml b/pom.xml
index 3daab4d790..63a1959063 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-5004-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index fc88571622..66a68de39f 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
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-5004-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 6f34da5660..102427d19a 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-5004-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
index 43c0d521c3..47fea8a02f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
@@ -225,6 +225,14 @@ interface TerminatingFindNear {
* @return never {@literal null}.
*/
GeoResults all();
+
+ /**
+ * Count matching elements.
+ *
+ * @return number of elements matching the query.
+ * @since 5.0
+ */
+ long count();
}
/**
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
index 46289ecfa4..39f4affd35 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
@@ -243,6 +243,11 @@ public TerminatingFindNear map(QueryResultConverter super G, ? extends
public GeoResults all() {
return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter);
}
+
+ @Override
+ public long count() {
+ return template.doGeoNearCount(nearQuery, domainType, getCollectionName());
+ }
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
index 8682f77ec8..03c0bb7682 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
@@ -48,6 +48,7 @@
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
@@ -1044,6 +1045,31 @@ public GeoResults geoNear(NearQuery near, Class> domainType, String col
return doGeoNear(near, domainType, collectionName, returnType, QueryResultConverter.entity());
}
+ long doGeoNearCount(NearQuery near, Class> domainType, String collectionName) {
+
+ Builder optionsBuilder = AggregationOptions.builder().collation(near.getCollation());
+
+ if (near.hasReadPreference()) {
+ optionsBuilder.readPreference(near.getReadPreference());
+ }
+
+ if (near.hasReadConcern()) {
+ optionsBuilder.readConcern(near.getReadConcern());
+ }
+
+ String distanceField = operations.nearQueryDistanceFieldName(domainType);
+ Aggregation $geoNear = TypedAggregation.newAggregation(domainType,
+ Aggregation.geoNear(near, distanceField).skip(-1).limit(-1), Aggregation.count().as("_totalCount"))
+ .withOptions(optionsBuilder.build());
+
+ AggregationResults results = doAggregate($geoNear, collectionName, Document.class,
+ queryOperations.createAggregation($geoNear, (AggregationOperationContext) null));
+ Iterator iterator = results.iterator();
+ return iterator.hasNext()
+ ? NumberUtils.convertNumberToTargetClass(iterator.next().get("_totalCount", Integer.class), Long.class)
+ : 0L;
+ }
+
GeoResults doGeoNear(NearQuery near, Class> domainType, String collectionName, Class returnType,
QueryResultConverter super T, ? extends R> resultConverter) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
index bcfc64f2b4..04b793f839 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
@@ -42,6 +42,8 @@ public class GeoNearOperation implements AggregationOperation {
private final NearQuery nearQuery;
private final String distanceField;
private final @Nullable String indexKey;
+ private final @Nullable Long skip;
+ private final @Nullable Integer limit;
/**
* Creates a new {@link GeoNearOperation} from the given {@link NearQuery} and the given distance field. The
@@ -51,7 +53,7 @@ public class GeoNearOperation implements AggregationOperation {
* @param distanceField must not be {@literal null}.
*/
public GeoNearOperation(NearQuery nearQuery, String distanceField) {
- this(nearQuery, distanceField, null);
+ this(nearQuery, distanceField, null, nearQuery.getSkip(), null);
}
/**
@@ -63,7 +65,8 @@ public GeoNearOperation(NearQuery nearQuery, String distanceField) {
* @param indexKey can be {@literal null};
* @since 2.1
*/
- private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable String indexKey) {
+ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable String indexKey, @Nullable Long skip,
+ @Nullable Integer limit) {
Assert.notNull(nearQuery, "NearQuery must not be null");
Assert.hasLength(distanceField, "Distance field must not be null or empty");
@@ -71,6 +74,8 @@ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable St
this.nearQuery = nearQuery;
this.distanceField = distanceField;
this.indexKey = indexKey;
+ this.skip = skip;
+ this.limit = limit;
}
/**
@@ -83,7 +88,30 @@ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable St
*/
@Contract("_ -> new")
public GeoNearOperation useIndex(String key) {
- return new GeoNearOperation(nearQuery, distanceField, key);
+ return new GeoNearOperation(nearQuery, distanceField, key, skip, limit);
+ }
+
+ /**
+ * Override potential skip applied via {@link NearQuery#getSkip()}. Adds an additional {@link SkipOperation} if value
+ * is non negative.
+ *
+ * @param skip
+ * @return new instance of {@link GeoNearOperation}.
+ * @since 5.0
+ */
+ public GeoNearOperation skip(long skip) {
+ return new GeoNearOperation(nearQuery, distanceField, indexKey, skip, limit);
+ }
+
+ /**
+ * Override potential limit value. Adds an additional {@link LimitOperation} if value is non negative.
+ *
+ * @param limit
+ * @return new instance of {@link GeoNearOperation}.
+ * @since 5.0
+ */
+ public GeoNearOperation limit(Integer limit) {
+ return new GeoNearOperation(nearQuery, distanceField, indexKey, skip, limit);
}
@Override
@@ -92,7 +120,13 @@ public Document toDocument(AggregationOperationContext context) {
Document command = context.getMappedObject(nearQuery.toDocument());
if (command.containsKey("query")) {
- command.replace("query", context.getMappedObject(command.get("query", Document.class)));
+ Document query = command.get("query", Document.class);
+ if (query == null || query.isEmpty()) {
+ command.remove("query");
+ } else {
+ command.replace("query", context.getMappedObject(query));
+ }
+
}
command.remove("collation");
@@ -115,15 +149,18 @@ public List toPipelineStages(AggregationOperationContext context) {
Document command = toDocument(context);
Number limit = (Number) command.get("$geoNear", Document.class).remove("num");
+ if (limit != null && this.limit != null) {
+ limit = this.limit;
+ }
List stages = new ArrayList<>(3);
stages.add(command);
- if (nearQuery.getSkip() != null && nearQuery.getSkip() > 0) {
- stages.add(new Document("$skip", nearQuery.getSkip()));
+ if (this.skip != null && this.skip > 0) {
+ stages.add(new Document("$skip", this.skip));
}
- if (limit != null) {
+ if (limit != null && limit.longValue() > 0) {
stages.add(new Document("$limit", limit.longValue()));
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
index 7777e5f554..4400baa6d6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
@@ -46,9 +46,7 @@ public interface CriteriaDefinition {
* @since 5.0
* @author Christoph Strobl
*/
- class Placeholder {
-
- private final Object expression;
+ interface Placeholder {
/**
* Create a new placeholder for index bindable parameter.
@@ -56,23 +54,29 @@ class Placeholder {
* @param position the index of the parameter to bind.
* @return new instance of {@link Placeholder}.
*/
- public static Placeholder indexed(int position) {
- return new Placeholder("?%s".formatted(position));
+ static Placeholder indexed(int position) {
+ return new PlaceholderImpl("?%s".formatted(position));
}
- public static Placeholder placeholder(String expression) {
- return new Placeholder(expression);
+ static Placeholder placeholder(String expression) {
+ return new PlaceholderImpl(expression);
}
- Placeholder(Object value) {
- this.expression = value;
+ Object getValue();
+ }
+
+ static class PlaceholderImpl implements Placeholder {
+ private final Object expression;
+
+ public PlaceholderImpl(Object expression) {
+ this.expression = expression;
}
+ @Override
public Object getValue() {
return expression;
}
- @Override
public String toString() {
return getValue().toString();
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
index 19ecd94e23..4b8f81ef2b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
@@ -22,6 +22,7 @@
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
+import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.util.Assert;
@@ -75,6 +76,9 @@ private String getCommand(Shape shape) {
Assert.notNull(shape, "Shape must not be null");
+ if(shape instanceof GeoJson>) {
+ return "$geometry";
+ }
if (shape instanceof Box) {
return "$box";
} else if (shape instanceof Circle) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
index 88d7dc5c1d..4f42437704 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
@@ -671,7 +671,7 @@ public Document toDocument() {
document.put("distanceMultiplier", getDistanceMultiplier());
}
- if (limit != null) {
+ if (limit != null && limit > 0) {
document.put("num", limit);
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java
new file mode 100644
index 0000000000..37f24cd849
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2025 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.repository.aot;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.bson.Document;
+import org.jspecify.annotations.NullUnmarked;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
+import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
+import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.repository.Hint;
+import org.springframework.data.mongodb.repository.ReadPreference;
+import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
+import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
+import org.springframework.data.util.ReflectionUtils;
+import org.springframework.javapoet.CodeBlock;
+import org.springframework.javapoet.CodeBlock.Builder;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+class AggregationBlocks {
+
+ @NullUnmarked
+ static class AggregationExecutionCodeBlockBuilder {
+
+ private final AotQueryMethodGenerationContext context;
+ private final MongoQueryMethod queryMethod;
+ private String aggregationVariableName;
+
+ AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+ this.context = context;
+ this.queryMethod = queryMethod;
+ }
+
+ AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) {
+
+ this.aggregationVariableName = aggregationVariableName;
+ return this;
+ }
+
+ CodeBlock build() {
+
+ String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
+ Builder builder = CodeBlock.builder();
+
+ builder.add("\n");
+
+ Class> outputType = queryMethod.getReturnedObjectType();
+ if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) {
+ outputType = Document.class;
+ } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) {
+ outputType = queryMethod.getReturnType().getComponentType().getType();
+ }
+
+ if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
+ builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
+ return builder.build();
+ }
+
+ if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) {
+ builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
+ return builder.build();
+ }
+
+ if (outputType == Document.class) {
+
+ Class> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
+
+ if (queryMethod.isStreamQuery()) {
+
+ builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class,
+ context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+
+ builder.addStatement("return $1L.map(it -> ($2T) convertSimpleRawResult($2T.class, it))",
+ context.localVariable("results"), returnType);
+ } else {
+
+ builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
+ context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+
+ if (!queryMethod.isCollectionQuery()) {
+ builder.addStatement(
+ "return $1T.<$2T>firstElement(convertSimpleRawResults($2T.class, $3L.getMappedResults()))",
+ CollectionUtils.class, returnType, context.localVariable("results"));
+ } else {
+ builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType,
+ context.localVariable("results"));
+ }
+ }
+ } else {
+ if (queryMethod.isSliceQuery()) {
+ builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
+ context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
+ builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()",
+ context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName());
+ builder.addStatement(
+ "return new $1T<>($2L ? $3L.getMappedResults().subList(0, $4L.getPageSize()) : $3L.getMappedResults(), $4L, $2L)",
+ SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"),
+ context.getPageableParameterName());
+ } else {
+
+ if (queryMethod.isStreamQuery()) {
+ builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
+ outputType);
+ } else {
+
+ builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
+ aggregationVariableName, outputType);
+ }
+ }
+ }
+
+ return builder.build();
+ }
+ }
+
+ @NullUnmarked
+ static class AggregationCodeBlockBuilder {
+
+ private final AotQueryMethodGenerationContext context;
+ private final MongoQueryMethod queryMethod;
+ private final Map arguments;
+
+ private AggregationInteraction source;
+
+ private String aggregationVariableName;
+ private boolean pipelineOnly;
+
+ AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
+
+ this.context = context;
+ this.arguments = new LinkedHashMap<>();
+ context.getBindableParameterNames().forEach(it -> arguments.put(it, CodeBlock.of(it)));
+ this.queryMethod = queryMethod;
+ }
+
+ AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
+
+ this.source = aggregation;
+ return this;
+ }
+
+ AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) {
+
+ this.aggregationVariableName = aggregationVariableName;
+ return this;
+ }
+
+ AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) {
+
+ this.pipelineOnly = pipelineOnly;
+ return this;
+ }
+
+ CodeBlock build() {
+
+ Builder builder = CodeBlock.builder();
+ builder.add("\n");
+
+ String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline"));
+ builder.add(pipeline(pipelineName));
+
+ if (!pipelineOnly) {
+
+ builder.addStatement("$1T<$2T> $3L = $4T.newAggregation($2T.class, $5L.getOperations())",
+ TypedAggregation.class, context.getRepositoryInformation().getDomainType(), aggregationVariableName,
+ Aggregation.class, pipelineName);
+
+ builder.add(aggregationOptions(aggregationVariableName));
+ }
+
+ return builder.build();
+ }
+
+ private CodeBlock pipeline(String pipelineVariableName) {
+
+ String sortParameter = context.getSortParameterName();
+ String limitParameter = context.getLimitParameterName();
+ String pageableParameter = context.getPageableParameterName();
+
+ boolean mightBeSorted = StringUtils.hasText(sortParameter);
+ boolean mightBeLimited = StringUtils.hasText(limitParameter);
+ boolean mightBePaged = StringUtils.hasText(pageableParameter);
+
+ int stageCount = source.stages().size();
+ if (mightBeSorted) {
+ stageCount++;
+ }
+ if (mightBeLimited) {
+ stageCount++;
+ }
+ if (mightBePaged) {
+ stageCount += 3;
+ }
+
+ Builder builder = CodeBlock.builder();
+ builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments));
+
+ if (mightBeSorted) {
+ builder.add(sortingStage(sortParameter));
+ }
+
+ if (mightBeLimited) {
+ builder.add(limitingStage(limitParameter));
+ }
+
+ if (mightBePaged) {
+ builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery()));
+ }
+
+ builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
+ context.localVariable("stages"));
+ return builder.build();
+ }
+
+ private CodeBlock aggregationOptions(String aggregationVariableName) {
+
+ Builder builder = CodeBlock.builder();
+ List options = new ArrayList<>(5);
+ if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
+ options.add(CodeBlock.of(".skipOutput()"));
+ }
+
+ MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class);
+ String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
+ if (StringUtils.hasText(hint)) {
+ options.add(CodeBlock.of(".hint($S)", hint));
+ }
+
+ MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
+ String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
+ if (StringUtils.hasText(readPreference)) {
+ options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference));
+ }
+
+ if (queryMethod.hasAnnotatedCollation()) {
+ options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation()));
+ }
+
+ if (!options.isEmpty()) {
+
+ Builder optionsBuilder = CodeBlock.builder();
+ optionsBuilder.add("$1T $2L = $1T.builder()\n", AggregationOptions.class,
+ context.localVariable("aggregationOptions"));
+ optionsBuilder.indent();
+ for (CodeBlock optionBlock : options) {
+ optionsBuilder.add(optionBlock);
+ optionsBuilder.add("\n");
+ }
+ optionsBuilder.add(".build();\n");
+ optionsBuilder.unindent();
+ builder.add(optionsBuilder.build());
+
+ builder.addStatement("$1L = $1L.withOptions($2L)", aggregationVariableName,
+ context.localVariable("aggregationOptions"));
+ }
+ return builder.build();
+ }
+
+ private CodeBlock aggregationStages(String stageListVariableName, Iterable stages, int stageCount,
+ Map arguments) {
+
+ Builder builder = CodeBlock.builder();
+ builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class,
+ stageCount);
+ int stageCounter = 0;
+
+ for (String stage : stages) {
+ String stageName = context.localVariable("stage_%s".formatted(stageCounter++));
+ builder.add(MongoCodeBlocks.renderExpressionToDocument(stage, stageName, arguments));
+ builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName);
+ }
+
+ return builder.build();
+ }
+
+ private CodeBlock sortingStage(String sortProvider) {
+
+ Builder builder = CodeBlock.builder();
+
+ builder.beginControlFlow("if ($L.isSorted())", sortProvider);
+ builder.addStatement("$1T $2L = new $1T()", Document.class, context.localVariable("sortDocument"));
+ builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), sortProvider);
+ builder.addStatement("$1L.append($2L.getProperty(), $2L.isAscending() ? 1 : -1);",
+ context.localVariable("sortDocument"), context.localVariable("order"));
+ builder.endControlFlow();
+ builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort",
+ context.localVariable("sortDocument"));
+ builder.endControlFlow();
+
+ return builder.build();
+ }
+
+ private CodeBlock pagingStage(String pageableProvider, boolean slice) {
+
+ Builder builder = CodeBlock.builder();
+
+ builder.add(sortingStage(pageableProvider + ".getSort()"));
+
+ builder.beginControlFlow("if ($L.isPaged())", pageableProvider);
+ builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider);
+ builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class,
+ pageableProvider);
+ builder.endControlFlow();
+ if (slice) {
+ builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"),
+ Aggregation.class, pageableProvider);
+ } else {
+ builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class,
+ pageableProvider);
+ }
+ builder.endControlFlow();
+
+ return builder.build();
+ }
+
+ private CodeBlock limitingStage(String limitProvider) {
+
+ Builder builder = CodeBlock.builder();
+
+ builder.beginControlFlow("if ($L.isLimited())", limitProvider);
+ builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class,
+ limitProvider);
+ builder.endControlFlow();
+
+ return builder.build();
+ }
+
+ }
+}
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 17c19ad951..219f90348c 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
@@ -15,10 +15,10 @@
*/
package org.springframework.data.mongodb.repository.aot;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import org.bson.conversions.Bson;
import org.jspecify.annotations.NullUnmarked;
@@ -29,10 +29,17 @@
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Vector;
+import org.springframework.data.geo.Box;
+import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
+import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
+import org.springframework.data.geo.Polygon;
+import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoWriter;
+import org.springframework.data.mongodb.core.geo.GeoJson;
+import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Collation;
@@ -40,11 +47,17 @@
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.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
+import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.TypeInformation;
+import org.springframework.util.ClassUtils;
import com.mongodb.DBRef;
@@ -68,11 +81,16 @@ public AotQueryCreator() {
}
@SuppressWarnings("NullAway")
- StringQuery createQuery(PartTree partTree, int parameterCount) {
+ StringQuery createQuery(PartTree partTree, QueryMethod queryMethod, Method source) {
+
+ boolean geoNear = queryMethod instanceof MongoQueryMethod mqm ? mqm.isGeoNearQuery() : false;
+ boolean searchQuery = queryMethod instanceof MongoQueryMethod mqm
+ ? mqm.isSearchQuery() || source.isAnnotationPresent(VectorSearch.class)
+ : source.isAnnotationPresent(VectorSearch.class);
Query query = new MongoQueryCreator(partTree,
- new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext)
- .createQuery();
+ new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext,
+ geoNear, searchQuery).createQuery();
if (partTree.isLimiting()) {
query.limit(partTree.getMaxResults());
@@ -118,17 +136,35 @@ static class PlaceholderParameterAccessor implements MongoParameterAccessor {
private final List placeholders;
- public PlaceholderParameterAccessor(int parameterCount) {
- if (parameterCount == 0) {
+ public PlaceholderParameterAccessor(QueryMethod queryMethod) {
+ if (queryMethod.getParameters().getNumberOfParameters() == 0) {
placeholders = List.of();
} else {
- placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList());
+ placeholders = new ArrayList<>();
+ Parameters, ?> parameters = queryMethod.getParameters();
+ for (Parameter parameter : parameters.toList()) {
+ if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new GeoJsonPlaceholder(parameter.getIndex(), ""));
+ } else if (ClassUtils.isAssignable(Point.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new PointPlaceholder(parameter.getIndex()));
+ } else if (ClassUtils.isAssignable(Circle.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new CirclePlaceholder(parameter.getIndex()));
+ } else if (ClassUtils.isAssignable(Box.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new BoxPlaceholder(parameter.getIndex()));
+ } else if (ClassUtils.isAssignable(Sphere.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new SpherePlaceholder(parameter.getIndex()));
+ } else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) {
+ placeholders.add(parameter.getIndex(), new PolygonPlaceholder(parameter.getIndex()));
+ } else {
+ placeholders.add(parameter.getIndex(), Placeholder.indexed(parameter.getIndex()));
+ }
+ }
}
}
@Override
public Range getDistanceRange() {
- return null;
+ return Range.unbounded();
}
@Override
@@ -207,4 +243,134 @@ public Iterator