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 { @@ -101,6 +101,10 @@ public class MongoAotRepositoryFragmentSupport {
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) {
expandGeoShapes(args);

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

@ -15,9 +15,6 @@ @@ -15,9 +15,6 @@
*/
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 org.bson.Document;
@ -171,10 +168,10 @@ class MongoCodeBlocks { @@ -171,10 +168,10 @@ class MongoCodeBlocks {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.add("new $T()", Document.class);
} else if (!containsPlaceholder(source)) {
builder.add("$T.parse($S)", Document.class, source);
} else {
} else if (containsPlaceholder(source)) {
builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", source, argNames);
} else {
builder.add("parse($S)", source);
}
return builder.build();
}
@ -185,47 +182,15 @@ class MongoCodeBlocks { @@ -185,47 +182,15 @@ class MongoCodeBlocks {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$1T $2L = new $1T()", Document.class, variableName);
} else if (!containsPlaceholder(source)) {
builder.addStatement("$1T $2L = $1T.parse($3S)", Document.class, variableName, source);
} else {
} else if (containsPlaceholder(source)) {
builder.add("$T $L = bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L);\n", Document.class,
variableName, source, argNames);
} else {
builder.addStatement("$1T $2L = parse($3S)", Document.class, variableName, source);
}
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,
AotQueryMethodGenerationContext context) {
try {

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

@ -258,13 +258,14 @@ class QueryBlocks { @@ -258,13 +258,14 @@ class QueryBlocks {
String source = this.source.getQuery().getQueryString();
if (!StringUtils.hasText(source)) {
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)) {
return CodeBlock.of("new $T($T.parse($S))", BasicQuery.class, Document.class, source);
else {
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 { @@ -165,7 +165,7 @@ class VectorSearchBlocks {
builder.add("($T) ($L) -> {\n", AggregationOperation.class, ctx);
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);
builder.add("return new $1T($2S, $3L.append(\"__score__\", -1));\n", Document.class, "$sort", mappedSort);
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> @@ -168,7 +168,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
}
// 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(),
() -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
@ -221,7 +221,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document> @@ -221,7 +221,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
return document;
} else if (bindingReader.currentValue instanceof String stringValue) {
try {
return decode(stringValue, new Object[0]);
return decode(stringValue);
} catch (JsonParseException 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; @@ -26,18 +26,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Limit;
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.domain.*;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
@ -297,6 +286,9 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -297,6 +286,9 @@ public interface UserRepository extends CrudRepository<User, String> {
"{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation")
List<String> findAllLastnamesWithCollation();
@Aggregation("{ $group : { _id : $customerId, total : { $sum : 1 } } }")
List<OrdersPerCustomer> totalOrdersPerCustomer(Sort sort);
// Vector Search
@VectorSearch(indexName = "embedding.vector_cos", filter = "{lastname: ?0}", numCandidates = "#{10+10}",
@ -362,4 +354,7 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -362,4 +354,7 @@ public interface UserRepository extends CrudRepository<User, String> {
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; @@ -30,6 +30,7 @@ import java.util.regex.Pattern;
import org.bson.BsonString;
import org.bson.Document;
import org.bson.json.JsonParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@ -688,6 +689,16 @@ class MongoRepositoryContributorTests { @@ -688,6 +689,16 @@ class MongoRepositoryContributorTests {
.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
void testNear() {

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

@ -388,7 +388,7 @@ class QueryMethodContributionUnitTests { @@ -388,7 +388,7 @@ class QueryMethodContributionUnitTests {
.containsSubsequence("var $sort = ", //
"(ctx) -> {", //
"mappedSort = ctx.getMappedObject(", //
"Document.parse(\"{\\\"firstname\\\": 1}\")", //
"parse(\"{\\\"firstname\\\": 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 @@ @@ -15,8 +15,7 @@
*/
package org.springframework.data.mongodb.util.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@ -31,6 +30,7 @@ import org.bson.BsonRegularExpression; @@ -31,6 +30,7 @@ import org.bson.BsonRegularExpression;
import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.junit.jupiter.api.Test;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
@ -635,9 +635,7 @@ class ParameterBindingJsonReaderUnitTests { @@ -635,9 +635,7 @@ class ParameterBindingJsonReaderUnitTests {
}
private static Document parse(String json, Object... args) {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);
return new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
return new ParameterBindingDocumentCodec().decode(json, args);
}
// DATAMONGO-2545

Loading…
Cancel
Save