Browse Source

Use consistently ParameterBindingDocumentCodec to parse queries and aggregations in AOT-generated code.

We now use ParameterBindingDocumentCodec instead of Document.parse(…) to reinstate lenient MQL parsing and to align with reflective behavior. Previously, we've used Document.parse(…) requiring a stricter syntax for e.g. values.

Closes #5018
pull/5026/head
Mark Paluch 5 months ago
parent
commit
50de1d6a0e
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java
  2. 47
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
  3. 11
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java
  4. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/VectorSearchBlocks.java
  5. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java
  6. 19
      spring-data-mongodb/src/test/java/example/aot/UserRepository.java
  7. 11
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java
  8. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java
  9. 8
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java

@ -101,6 +101,10 @@ public class MongoAotRepositoryFragmentSupport {
it -> valueExpressions.createValueContextProvider(mongoParameters.get().get(it)))); it -> valueExpressions.createValueContextProvider(mongoParameters.get().get(it))));
} }
protected Document parse(String json) {
return CODEC.decode(json);
}
protected Document bindParameters(Method method, String source, Object... args) { protected Document bindParameters(Method method, String source, Object... args) {
expandGeoShapes(args); expandGeoShapes(args);

47
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java

@ -15,9 +15,6 @@
*/ */
package org.springframework.data.mongodb.repository.aot; package org.springframework.data.mongodb.repository.aot;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bson.Document; import org.bson.Document;
@ -171,10 +168,10 @@ class MongoCodeBlocks {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) { if (!StringUtils.hasText(source)) {
builder.add("new $T()", Document.class); builder.add("new $T()", Document.class);
} else if (!containsPlaceholder(source)) { } else if (containsPlaceholder(source)) {
builder.add("$T.parse($S)", Document.class, source);
} else {
builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", source, argNames); builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", source, argNames);
} else {
builder.add("parse($S)", source);
} }
return builder.build(); return builder.build();
} }
@ -185,47 +182,15 @@ class MongoCodeBlocks {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) { if (!StringUtils.hasText(source)) {
builder.addStatement("$1T $2L = new $1T()", Document.class, variableName); builder.addStatement("$1T $2L = new $1T()", Document.class, variableName);
} else if (!containsPlaceholder(source)) { } else if (containsPlaceholder(source)) {
builder.addStatement("$1T $2L = $1T.parse($3S)", Document.class, variableName, source);
} else {
builder.add("$T $L = bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", Document.class, builder.add("$T $L = bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", Document.class,
variableName, source, argNames); variableName, source, argNames);
} else {
builder.addStatement("$1T $2L = parse($3S)", Document.class, variableName, source);
} }
return builder.build(); return builder.build();
} }
static CodeBlock renderArgumentMap(Map<String, CodeBlock> arguments) {
Builder builder = CodeBlock.builder();
builder.add("argumentMap(");
Iterator<Entry<String, CodeBlock>> iterator = arguments.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, CodeBlock> next = iterator.next();
builder.add("$S, ", next.getKey());
builder.add(next.getValue());
if (iterator.hasNext()) {
builder.add(", ");
}
}
builder.add(")");
return builder.build();
}
static CodeBlock renderArgumentArray(Map<String, CodeBlock> arguments) {
Builder builder = CodeBlock.builder();
builder.add("arguments(");
Iterator<CodeBlock> iterator = arguments.values().iterator();
while (iterator.hasNext()) {
builder.add(iterator.next());
if (iterator.hasNext()) {
builder.add(", ");
}
}
builder.add(")");
return builder.build();
}
static CodeBlock evaluateNumberPotentially(String value, Class<? extends Number> targetType, static CodeBlock evaluateNumberPotentially(String value, Class<? extends Number> targetType,
AotQueryMethodGenerationContext context) { AotQueryMethodGenerationContext context) {
try { try {

11
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java

@ -258,13 +258,14 @@ class QueryBlocks {
String source = this.source.getQuery().getQueryString(); String source = this.source.getQuery().getQueryString();
if (!StringUtils.hasText(source)) { if (!StringUtils.hasText(source)) {
return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class); return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class);
} else if (MongoCodeBlocks.containsPlaceholder(source)) {
Builder builder = CodeBlock.builder();
builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, parameterNames);
return builder.build();
} }
if (!MongoCodeBlocks.containsPlaceholder(source)) { else {
return CodeBlock.of("new $T($T.parse($S))", BasicQuery.class, Document.class, source); return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source);
} }
Builder builder = CodeBlock.builder();
builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, parameterNames);
return builder.build();
} }
} }
} }

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/VectorSearchBlocks.java

@ -165,7 +165,7 @@ class VectorSearchBlocks {
builder.add("($T) ($L) -> {\n", AggregationOperation.class, ctx); builder.add("($T) ($L) -> {\n", AggregationOperation.class, ctx);
builder.indent(); builder.indent();
builder.add("$1T $4L = $5L.getMappedObject($1T.parse($2S), $3T.class);\n", Document.class, filter.getSortString(), builder.add("$1T $4L = $5L.getMappedObject(parse($2S), $3T.class);\n", Document.class, filter.getSortString(),
context.getActualReturnType().getType(), mappedSort, ctx); context.getActualReturnType().getType(), mappedSort, ctx);
builder.add("return new $1T($2S, $3L.append(\"__score__\", -1));\n", Document.class, "$sort", mappedSort); builder.add("return new $1T($2S, $3L.append(\"__score__\", -1));\n", Document.class, "$sort", mappedSort);
builder.unindent(); builder.unindent();

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java

@ -168,7 +168,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
} }
// Spring Data Customization START // Spring Data Customization START
public Document decode(@Nullable String json, Object[] values) { public Document decode(@Nullable String json, Object... values) {
return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(), return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(),
() -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values))); () -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
@ -221,7 +221,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
return document; return document;
} else if (bindingReader.currentValue instanceof String stringValue) { } else if (bindingReader.currentValue instanceof String stringValue) {
try { try {
return decode(stringValue, new Object[0]); return decode(stringValue);
} catch (JsonParseException jsonParseException) { } catch (JsonParseException jsonParseException) {
throw new IllegalArgumentException("Expression result is not a valid json document", jsonParseException); throw new IllegalArgumentException("Expression result is not a valid json document", jsonParseException);
} }

19
spring-data-mongodb/src/test/java/example/aot/UserRepository.java

@ -26,18 +26,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Limit; import org.springframework.data.domain.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.SearchResults;
import org.springframework.data.domain.Similarity;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Vector;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Box; import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle; import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance; import org.springframework.data.geo.Distance;
@ -297,6 +286,9 @@ public interface UserRepository extends CrudRepository<User, String> {
"{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation") "{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation")
List<String> findAllLastnamesWithCollation(); List<String> findAllLastnamesWithCollation();
@Aggregation("{ $group : { _id : $customerId, total : { $sum : 1 } } }")
List<OrdersPerCustomer> totalOrdersPerCustomer(Sort sort);
// Vector Search // Vector Search
@VectorSearch(indexName = "embedding.vector_cos", filter = "{lastname: ?0}", numCandidates = "#{10+10}", @VectorSearch(indexName = "embedding.vector_cos", filter = "{lastname: ?0}", numCandidates = "#{10+10}",
@ -362,4 +354,7 @@ public interface UserRepository extends CrudRepository<User, String> {
return Objects.hash(lastname, names); return Objects.hash(lastname, names);
} }
} }
record OrdersPerCustomer(Object id, long total) {
}
} }

11
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java

@ -30,6 +30,7 @@ import java.util.regex.Pattern;
import org.bson.BsonString; import org.bson.BsonString;
import org.bson.Document; import org.bson.Document;
import org.bson.json.JsonParseException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -688,6 +689,16 @@ class MongoRepositoryContributorTests {
.withMessageContaining("'locale' is invalid"); .withMessageContaining("'locale' is invalid");
} }
@Test // GH-5018
void aggregationIsParsedLeniently() {
List<UserRepository.OrdersPerCustomer> result = fragment.totalOrdersPerCustomer(Sort.by("_id"));
assertThat(result).hasSize(1);
assertThatExceptionOfType(JsonParseException.class)
.isThrownBy(() -> Document.parse("{ $group : { _id : $customerId, total : { $sum : 1 } } }"));
}
@Test // GH-5004 @Test // GH-5004
void testNear() { void testNear() {

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java

@ -388,7 +388,7 @@ class QueryMethodContributionUnitTests {
.containsSubsequence("var $sort = ", // .containsSubsequence("var $sort = ", //
"(ctx) -> {", // "(ctx) -> {", //
"mappedSort = ctx.getMappedObject(", // "mappedSort = ctx.getMappedObject(", //
"Document.parse(\"{\\\"firstname\\\": 1}\")", // "parse(\"{\\\"firstname\\\": 1}\")", //
"Document(\"$sort\", mappedSort.append(\"__score__\", -1))"); "Document(\"$sort\", mappedSort.append(\"__score__\", -1))");
} }

8
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java

@ -15,8 +15,7 @@
*/ */
package org.springframework.data.mongodb.util.json; package org.springframework.data.mongodb.util.json;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
@ -31,6 +30,7 @@ import org.bson.BsonRegularExpression;
import org.bson.Document; import org.bson.Document;
import org.bson.codecs.DecoderContext; import org.bson.codecs.DecoderContext;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.spel.ExpressionDependencies;
@ -635,9 +635,7 @@ class ParameterBindingJsonReaderUnitTests {
} }
private static Document parse(String json, Object... args) { private static Document parse(String json, Object... args) {
return new ParameterBindingDocumentCodec().decode(json, args);
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);
return new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
} }
// DATAMONGO-2545 // DATAMONGO-2545

Loading…
Cancel
Save