Compare commits
10 Commits
issue/4426
...
issue/4379
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7103ca2228 | ||
|
|
b46aab2c28 | ||
|
|
ff137eca8a | ||
|
|
a93c870b45 | ||
|
|
af26bb6b31 | ||
|
|
d78f47f035 | ||
|
|
8cd956e90a | ||
|
|
49cc6a708d | ||
|
|
0bf472a29b | ||
|
|
2de00cdb2f |
4
.mvn/wrapper/maven-wrapper.properties
vendored
4
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1,2 +1,2 @@
|
||||
#Tue Jun 13 08:54:58 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
|
||||
#Mon Jul 03 09:49:43 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Java versions
|
||||
java.main.tag=17.0.6_10-jdk-focal
|
||||
java.main.tag=17.0.7_7-jdk-focal
|
||||
java.next.tag=20-jdk-jammy
|
||||
|
||||
# Docker container images - standard
|
||||
@@ -7,15 +7,15 @@ docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/ecli
|
||||
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
|
||||
|
||||
# Supported versions of MongoDB
|
||||
docker.mongodb.4.4.version=4.4.18
|
||||
docker.mongodb.5.0.version=5.0.14
|
||||
docker.mongodb.6.0.version=6.0.4
|
||||
docker.mongodb.4.4.version=4.4.22
|
||||
docker.mongodb.5.0.version=5.0.18
|
||||
docker.mongodb.6.0.version=6.0.7
|
||||
|
||||
# Supported versions of Redis
|
||||
docker.redis.6.version=6.2.10
|
||||
docker.redis.6.version=6.2.12
|
||||
|
||||
# Supported versions of Cassandra
|
||||
docker.cassandra.3.version=3.11.14
|
||||
docker.cassandra.3.version=3.11.15
|
||||
|
||||
# Docker environment settings
|
||||
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<version>4.2.x-4379-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Data MongoDB</name>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<version>4.2.x-4379-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<version>4.2.x-4379-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.2.x-4426-SNAPSHOT</version>
|
||||
<version>4.2.x-4379-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.bson.Document;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
import org.springframework.data.mongodb.MongoCollectionUtils;
|
||||
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -79,7 +80,30 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
|
||||
FieldReference getReference(String name);
|
||||
|
||||
/**
|
||||
* Returns the {@link Fields} exposed by the type. May be a {@literal class} or an {@literal interface}. The default
|
||||
* Obtain the target field name for a given field/type combination.
|
||||
*
|
||||
* @param type The type containing the field.
|
||||
* @param field The property/field name
|
||||
* @return never {@literal null}.
|
||||
* @since 4.2
|
||||
*/
|
||||
default String getMappedFieldName(Class<?> type, String field) {
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the collection name for a given {@link Class type} combination.
|
||||
*
|
||||
* @param type
|
||||
* @return never {@literal null}.
|
||||
* @since 4.2
|
||||
*/
|
||||
default String getCollection(Class<?> type) {
|
||||
return MongoCollectionUtils.getPreferredCollectionName(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Fields} exposed by the type. Can be a {@literal class} or an {@literal interface}. The default
|
||||
* implementation uses {@link BeanUtils#getPropertyDescriptors(Class) property descriptors} discover fields from a
|
||||
* {@link Class}.
|
||||
*
|
||||
@@ -109,7 +133,7 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
|
||||
|
||||
/**
|
||||
* This toggle allows the {@link AggregationOperationContext context} to use any given field name without checking for
|
||||
* its existence. Typically the {@link AggregationOperationContext} fails when referencing unknown fields, those that
|
||||
* its existence. Typically, the {@link AggregationOperationContext} fails when referencing unknown fields, those that
|
||||
* are not present in one of the previous stages or the input source, throughout the pipeline.
|
||||
*
|
||||
* @return a more relaxed {@link AggregationOperationContext}.
|
||||
|
||||
@@ -46,7 +46,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
private static final Set<Class<?>> ALLOWED_START_TYPES = new HashSet<Class<?>>(
|
||||
Arrays.<Class<?>> asList(AggregationExpression.class, String.class, Field.class, Document.class));
|
||||
|
||||
private final String from;
|
||||
private final Object from;
|
||||
private final List<Object> startWith;
|
||||
private final Field connectFrom;
|
||||
private final Field connectTo;
|
||||
@@ -55,7 +55,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
private final @Nullable Field depthField;
|
||||
private final @Nullable CriteriaDefinition restrictSearchWithMatch;
|
||||
|
||||
private GraphLookupOperation(String from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
|
||||
private GraphLookupOperation(Object from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
|
||||
@Nullable Long maxDepth, @Nullable Field depthField, @Nullable CriteriaDefinition restrictSearchWithMatch) {
|
||||
|
||||
this.from = from;
|
||||
@@ -82,7 +82,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
|
||||
Document graphLookup = new Document();
|
||||
|
||||
graphLookup.put("from", from);
|
||||
graphLookup.put("from", getCollectionName(context));
|
||||
|
||||
List<Object> mappedStartWith = new ArrayList<>(startWith.size());
|
||||
|
||||
@@ -99,7 +99,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
|
||||
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
|
||||
|
||||
graphLookup.put("connectFromField", connectFrom.getTarget());
|
||||
graphLookup.put("connectFromField", getForeignFieldName(context));
|
||||
graphLookup.put("connectToField", connectTo.getTarget());
|
||||
graphLookup.put("as", as.getName());
|
||||
|
||||
@@ -118,6 +118,16 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
return new Document(getOperator(), graphLookup);
|
||||
}
|
||||
|
||||
String getCollectionName(AggregationOperationContext context) {
|
||||
return from instanceof Class<?> type ? context.getCollection(type) : from.toString();
|
||||
}
|
||||
|
||||
String getForeignFieldName(AggregationOperationContext context) {
|
||||
|
||||
return from instanceof Class<?> type ? context.getMappedFieldName(type, connectFrom.getTarget())
|
||||
: connectFrom.getTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOperator() {
|
||||
return "$graphLookup";
|
||||
@@ -128,7 +138,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
|
||||
List<ExposedField> fields = new ArrayList<>(2);
|
||||
fields.add(new ExposedField(as, true));
|
||||
if(depthField != null) {
|
||||
if (depthField != null) {
|
||||
fields.add(new ExposedField(depthField, true));
|
||||
}
|
||||
return ExposedFields.from(fields.toArray(new ExposedField[0]));
|
||||
@@ -146,6 +156,17 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
StartWithBuilder from(String collectionName);
|
||||
|
||||
/**
|
||||
* Use the given type to determine name of the foreign collection and map
|
||||
* {@link ConnectFromBuilder#connectFrom(String)} against it to consider eventually present
|
||||
* {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
* @since 4.2
|
||||
*/
|
||||
StartWithBuilder from(Class<?> type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +239,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
static final class GraphLookupOperationFromBuilder
|
||||
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
|
||||
|
||||
private @Nullable String from;
|
||||
private @Nullable Object from;
|
||||
private @Nullable List<? extends Object> startWith;
|
||||
private @Nullable String connectFrom;
|
||||
|
||||
@@ -231,6 +252,14 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartWithBuilder from(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
this.from = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectFromBuilder startWith(String... fieldReferences) {
|
||||
|
||||
@@ -321,7 +350,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
*/
|
||||
public static final class GraphLookupOperationBuilder {
|
||||
|
||||
private final String from;
|
||||
private final Object from;
|
||||
private final List<Object> startWith;
|
||||
private final Field connectFrom;
|
||||
private final Field connectTo;
|
||||
@@ -329,7 +358,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
|
||||
private @Nullable Field depthField;
|
||||
private @Nullable CriteriaDefinition restrictSearchWithMatch;
|
||||
|
||||
protected GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
|
||||
protected GraphLookupOperationBuilder(Object from, List<? extends Object> startWith, String connectFrom,
|
||||
String connectTo) {
|
||||
|
||||
this.from = from;
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
|
||||
|
||||
private final String from;
|
||||
private Object from;
|
||||
|
||||
@Nullable //
|
||||
private final Field localField;
|
||||
@@ -97,6 +97,22 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
*/
|
||||
public LookupOperation(String from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
|
||||
@Nullable AggregationPipeline pipeline, Field as) {
|
||||
this((Object) from, localField, foreignField, let, pipeline, as);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
|
||||
* pipeline}.
|
||||
*
|
||||
* @param from must not be {@literal null}. Can be eiter the target collection name or a {@link Class}.
|
||||
* @param localField can be {@literal null} if {@literal pipeline} is present.
|
||||
* @param foreignField can be {@literal null} if {@literal pipeline} is present.
|
||||
* @param let can be {@literal null} if {@literal localField} and {@literal foreignField} are present.
|
||||
* @param as must not be {@literal null}.
|
||||
* @since 4.2
|
||||
*/
|
||||
private LookupOperation(Object from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
|
||||
@Nullable AggregationPipeline pipeline, Field as) {
|
||||
|
||||
Assert.notNull(from, "From must not be null");
|
||||
if (pipeline == null) {
|
||||
@@ -125,12 +141,14 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
|
||||
Document lookupObject = new Document();
|
||||
|
||||
lookupObject.append("from", from);
|
||||
lookupObject.append("from", getCollectionName(context));
|
||||
|
||||
if (localField != null) {
|
||||
lookupObject.append("localField", localField.getTarget());
|
||||
}
|
||||
|
||||
if (foreignField != null) {
|
||||
lookupObject.append("foreignField", foreignField.getTarget());
|
||||
lookupObject.append("foreignField", getForeignFieldName(context));
|
||||
}
|
||||
if (let != null) {
|
||||
lookupObject.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
|
||||
@@ -144,6 +162,16 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
return new Document(getOperator(), lookupObject);
|
||||
}
|
||||
|
||||
String getCollectionName(AggregationOperationContext context) {
|
||||
return from instanceof Class<?> type ? context.getCollection(type) : from.toString();
|
||||
}
|
||||
|
||||
String getForeignFieldName(AggregationOperationContext context) {
|
||||
|
||||
return from instanceof Class<?> type ? context.getMappedFieldName(type, foreignField.getTarget())
|
||||
: foreignField.getTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOperator() {
|
||||
return "$lookup";
|
||||
@@ -158,16 +186,28 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
return new LookupOperationBuilder();
|
||||
}
|
||||
|
||||
public static interface FromBuilder {
|
||||
public interface FromBuilder {
|
||||
|
||||
/**
|
||||
* @param name the collection in the same database to perform the join with, must not be {@literal null} or empty.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
LocalFieldBuilder from(String name);
|
||||
|
||||
/**
|
||||
* Use the given type to determine name of the foreign collection and map
|
||||
* {@link ForeignFieldBuilder#foreignField(String)} against it to consider eventually present
|
||||
* {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
|
||||
*
|
||||
* @param type the type of the target collection in the same database to perform the join with, must not be
|
||||
* {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
* @since 4.2
|
||||
*/
|
||||
LocalFieldBuilder from(Class<?> type);
|
||||
}
|
||||
|
||||
public static interface LocalFieldBuilder extends PipelineBuilder {
|
||||
public interface LocalFieldBuilder extends PipelineBuilder {
|
||||
|
||||
/**
|
||||
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
|
||||
@@ -177,7 +217,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
ForeignFieldBuilder localField(String name);
|
||||
}
|
||||
|
||||
public static interface ForeignFieldBuilder {
|
||||
public interface ForeignFieldBuilder {
|
||||
|
||||
/**
|
||||
* @param name the field from the documents in the {@code from} collection, must not be {@literal null} or empty.
|
||||
@@ -246,7 +286,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
LookupOperation as(String name);
|
||||
}
|
||||
|
||||
public static interface AsBuilder extends PipelineBuilder {
|
||||
public interface AsBuilder extends PipelineBuilder {
|
||||
|
||||
/**
|
||||
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
|
||||
@@ -264,7 +304,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
public static final class LookupOperationBuilder
|
||||
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
|
||||
|
||||
private @Nullable String from;
|
||||
private @Nullable Object from;
|
||||
private @Nullable Field localField;
|
||||
private @Nullable Field foreignField;
|
||||
private @Nullable ExposedField as;
|
||||
@@ -288,6 +328,14 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalFieldBuilder from(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "'From' must not be null");
|
||||
from = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsBuilder foreignField(String name) {
|
||||
|
||||
|
||||
@@ -30,9 +30,8 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
|
||||
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
|
||||
* <br />
|
||||
* Fields to be excluded from prefixing my be added to a {@literal denylist}.
|
||||
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose. <br />
|
||||
* Fields to be excluded from prefixing can be added to a {@literal denylist}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
|
||||
@@ -92,6 +92,20 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
|
||||
return getReferenceFor(field(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCollection(Class<?> type) {
|
||||
|
||||
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(type);
|
||||
return persistentEntity != null ? persistentEntity.getCollection() : AggregationOperationContext.super.getCollection(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMappedFieldName(Class<?> type, String field) {
|
||||
|
||||
PersistentPropertyPath<MongoPersistentProperty> persistentPropertyPath = mappingContext.getPersistentPropertyPath(field, type);
|
||||
return persistentPropertyPath.getLeafProperty().getFieldName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fields getFields(Class<?> type) {
|
||||
|
||||
|
||||
@@ -1089,6 +1089,7 @@ public class QueryMapper {
|
||||
protected static class MetadataBackedField extends Field {
|
||||
|
||||
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
|
||||
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
|
||||
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s; Associations can only be pointed to directly or via their id property";
|
||||
|
||||
private final MongoPersistentEntity<?> entity;
|
||||
@@ -1338,22 +1339,24 @@ public class QueryMapper {
|
||||
return source;
|
||||
}
|
||||
|
||||
List<String> path = new ArrayList<>();
|
||||
List<String> path = new ArrayList<>(segments.length);
|
||||
|
||||
/* always start from a property, so we can skip the first segment.
|
||||
from there remove any position placeholder */
|
||||
for (String segment : Arrays.copyOfRange(segments, 1, segments.length)) {
|
||||
for(int i=1; i < segments.length; i++) {
|
||||
String segment = segments[i];
|
||||
if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||
continue;
|
||||
}
|
||||
if (segment.matches("\\d+")) {
|
||||
if (NUMERIC_SEGMENT.matcher(segment).matches()) {
|
||||
continue;
|
||||
}
|
||||
path.add(segment);
|
||||
}
|
||||
|
||||
// when property is followed only by placeholders eg. 'values.0.3.90'
|
||||
if (path.isEmpty()) {
|
||||
// or when there is no difference in the number of segments
|
||||
if (path.isEmpty() || segments.length == path.size() + 1) {
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.util.Set;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.domain.KeysetScrollPosition;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.OffsetScrollPosition;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
@@ -66,7 +67,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
private @Nullable Field fieldSpec = null;
|
||||
private Sort sort = Sort.unsorted();
|
||||
private long skip;
|
||||
private int limit;
|
||||
private Limit limit = Limit.unlimited();
|
||||
|
||||
private KeysetScrollPosition keysetScrollPosition;
|
||||
private @Nullable ReadConcern readConcern;
|
||||
@@ -155,10 +156,30 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @return this.
|
||||
*/
|
||||
public Query limit(int limit) {
|
||||
this.limit = limit;
|
||||
this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of returned documents to {@link Limit}.
|
||||
*
|
||||
* @param limit number of documents to return.
|
||||
* @return this.
|
||||
* @since 4.2
|
||||
*/
|
||||
public Query limit(Limit limit) {
|
||||
|
||||
Assert.notNull(limit, "Limit must not be null");
|
||||
|
||||
if (limit.isUnlimited()) {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
// retain zero/negative semantics for unlimited.
|
||||
return limit(limit.max());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the query to use the given hint when being executed. The {@code hint} can either be an index name or a
|
||||
* json {@link Document} representation.
|
||||
@@ -254,7 +275,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.limit = pageable.getPageSize();
|
||||
this.limit = pageable.toLimit();
|
||||
this.skip = pageable.getOffset();
|
||||
|
||||
return with(pageable.getSort());
|
||||
@@ -457,7 +478,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @since 4.1
|
||||
*/
|
||||
public boolean isLimited() {
|
||||
return this.limit > 0;
|
||||
return this.limit.isLimited();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -468,7 +489,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
* @see #isLimited()
|
||||
*/
|
||||
public int getLimit() {
|
||||
return this.limit;
|
||||
return limit.isUnlimited() ? 0 : this.limit.max();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,7 +704,8 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
};
|
||||
|
||||
target.skip = source.getSkip();
|
||||
target.limit = source.getLimit();
|
||||
|
||||
target.limit = source.isLimited() ? Limit.of(source.getLimit()) : Limit.unlimited();
|
||||
target.hint = source.getHint();
|
||||
target.collation = source.getCollation();
|
||||
target.restrictedTypes = new HashSet<>(source.getRestrictedTypes());
|
||||
@@ -746,7 +768,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
|
||||
result += 31 * nullSafeHashCode(sort);
|
||||
result += 31 * nullSafeHashCode(hint);
|
||||
result += 31 * skip;
|
||||
result += 31 * limit;
|
||||
result += 31 * limit.hashCode();
|
||||
result += 31 * nullSafeHashCode(meta);
|
||||
result += 31 * nullSafeHashCode(collation.orElse(null));
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
@@ -117,6 +118,11 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor {
|
||||
return delegate.getUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Limit getLimit() {
|
||||
return delegate.getLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value with the underlying {@link MongoWriter}.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2023. 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.core.aggregation;
|
||||
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.convert.QueryMapper;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public final class AggregationTestUtils {
|
||||
|
||||
public static AggregationContextBuilder<TypeBasedAggregationOperationContext> strict(Class<?> type) {
|
||||
|
||||
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
|
||||
builder.strict = true;
|
||||
return builder.forType(type);
|
||||
}
|
||||
|
||||
public static AggregationContextBuilder<TypeBasedAggregationOperationContext> relaxed(Class<?> type) {
|
||||
|
||||
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
|
||||
builder.strict = false;
|
||||
return builder.forType(type);
|
||||
}
|
||||
|
||||
public static class AggregationContextBuilder<T extends AggregationOperationContext> {
|
||||
|
||||
Class<?> targetType;
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
|
||||
QueryMapper queryMapper;
|
||||
boolean strict;
|
||||
|
||||
public AggregationContextBuilder<TypeBasedAggregationOperationContext> forType(Class<?> type) {
|
||||
|
||||
this.targetType = type;
|
||||
return (AggregationContextBuilder<TypeBasedAggregationOperationContext>) this;
|
||||
}
|
||||
|
||||
public AggregationContextBuilder<T> using(
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AggregationContextBuilder<T> using(QueryMapper queryMapper) {
|
||||
|
||||
this.queryMapper = queryMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public T ctx() {
|
||||
//
|
||||
if (targetType == null) {
|
||||
return (T) Aggregation.DEFAULT_CONTEXT;
|
||||
}
|
||||
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> ctx = mappingContext != null
|
||||
? mappingContext
|
||||
: MongoTestMappingContext.newTestContext().init();
|
||||
QueryMapper qm = queryMapper != null ? queryMapper
|
||||
: new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, ctx));
|
||||
return (T) (strict ? new TypeBasedAggregationOperationContext(targetType, ctx, qm)
|
||||
: new RelaxedTypeBasedAggregationOperationContext(targetType, ctx, qm));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import java.util.Arrays;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.mongodb.core.Person;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
|
||||
/**
|
||||
@@ -34,7 +35,7 @@ public class GraphLookupOperationUnitTests {
|
||||
|
||||
@Test // DATAMONGO-1551
|
||||
public void rejectsNullFromCollection() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from(null));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from((String) null));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1551
|
||||
@@ -158,4 +159,59 @@ public class GraphLookupOperationUnitTests {
|
||||
|
||||
assertThat(document).containsEntry("$graphLookup.depthField", "foo.bar");
|
||||
}
|
||||
|
||||
@Test // GH-4379
|
||||
void unmappedLookupWithFromExtractedFromType() {
|
||||
|
||||
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
|
||||
.from(Employee.class) //
|
||||
.startWith(LiteralOperators.Literal.asLiteral("hello")) //
|
||||
.connectFrom("manager") //
|
||||
.connectTo("name") //
|
||||
.as("reportingHierarchy");
|
||||
|
||||
assertThat(graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
|
||||
{ $graphLookup:
|
||||
{
|
||||
from: "employee",
|
||||
startWith : { $literal : "hello" },
|
||||
connectFromField: "manager",
|
||||
connectToField: "name",
|
||||
as: "reportingHierarchy"
|
||||
}
|
||||
}}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test // GH-4379
|
||||
void mappedLookupWithFromExtractedFromType() {
|
||||
|
||||
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
|
||||
.from(Employee.class) //
|
||||
.startWith(LiteralOperators.Literal.asLiteral("hello")) //
|
||||
.connectFrom("manager") //
|
||||
.connectTo("name") //
|
||||
.as("reportingHierarchy");
|
||||
|
||||
assertThat(graphLookupOperation.toDocument(AggregationTestUtils.strict(Employee.class).ctx())).isEqualTo("""
|
||||
{ $graphLookup:
|
||||
{
|
||||
from: "employees",
|
||||
startWith : { $literal : "hello" },
|
||||
connectFromField: "reportsTo",
|
||||
connectToField: "name",
|
||||
as: "reportingHierarchy"
|
||||
}
|
||||
}}
|
||||
""");
|
||||
}
|
||||
|
||||
@org.springframework.data.mongodb.core.mapping.Document("employees")
|
||||
static class Employee {
|
||||
|
||||
String id;
|
||||
|
||||
@Field("reportsTo")
|
||||
String manager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.List;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.mongodb.core.DocumentTestUtils;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,7 @@ public class LookupOperationUnitTests {
|
||||
|
||||
@Test // DATAMONGO-1326
|
||||
public void builderRejectsNullFromField() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from(null));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from((String) null));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-1326
|
||||
@@ -195,10 +196,10 @@ public class LookupOperationUnitTests {
|
||||
void buildsLookupWithLocalAndForeignFieldAsWellAsLetAndPipeline() {
|
||||
|
||||
LookupOperation lookupOperation = Aggregation.lookup().from("restaurants") //
|
||||
.localField("restaurant_name")
|
||||
.foreignField("name")
|
||||
.localField("restaurant_name") //
|
||||
.foreignField("name") //
|
||||
.let(newVariable("orders_drink").forField("drink")) //
|
||||
.pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
|
||||
.pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages"))))) //
|
||||
.as("matches");
|
||||
|
||||
assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
|
||||
@@ -216,4 +217,54 @@ public class LookupOperationUnitTests {
|
||||
}}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test // GH-4379
|
||||
void unmappedLookupWithFromExtractedFromType() {
|
||||
|
||||
LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
|
||||
.localField("restaurant_name") //
|
||||
.foreignField("name") //
|
||||
.as("restaurants");
|
||||
|
||||
assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
|
||||
{ $lookup:
|
||||
{
|
||||
from: "restaurant",
|
||||
localField: "restaurant_name",
|
||||
foreignField: "name",
|
||||
as: "restaurants"
|
||||
}
|
||||
}}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test // GH-4379
|
||||
void mappedLookupWithFromExtractedFromType() {
|
||||
|
||||
LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
|
||||
.localField("restaurant_name") //
|
||||
.foreignField("name") //
|
||||
.as("restaurants");
|
||||
|
||||
|
||||
assertThat(lookupOperation.toDocument(AggregationTestUtils.strict(Restaurant.class).ctx())).isEqualTo("""
|
||||
{ $lookup:
|
||||
{
|
||||
from: "sites",
|
||||
localField: "restaurant_name",
|
||||
foreignField: "rs_name",
|
||||
as: "restaurants"
|
||||
}
|
||||
}}
|
||||
""");
|
||||
}
|
||||
|
||||
@org.springframework.data.mongodb.core.mapping.Document("sites")
|
||||
static class Restaurant {
|
||||
|
||||
String id;
|
||||
|
||||
@Field("rs_name") //
|
||||
String name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
@@ -97,6 +98,18 @@ class QueryTests {
|
||||
assertThat(q.getQueryObject()).isEqualTo(Document
|
||||
.parse("{ \"name\" : { \"$gte\" : \"M\" , \"$lte\" : \"T\"} , \"age\" : { \"$not\" : { \"$gt\" : 22}}}"));
|
||||
assertThat(q.getLimit()).isEqualTo(50);
|
||||
|
||||
q.limit(Limit.unlimited());
|
||||
assertThat(q.getLimit()).isZero();
|
||||
assertThat(q.isLimited()).isFalse();
|
||||
|
||||
q.limit(Limit.of(10));
|
||||
assertThat(q.getLimit()).isEqualTo(10);
|
||||
assertThat(q.isLimited()).isTrue();
|
||||
|
||||
q.limit(Limit.of(-1));
|
||||
assertThat(q.getLimit()).isZero();
|
||||
assertThat(q.isLimited()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -213,6 +213,17 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
|
||||
assertThat(page).contains(carter);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void appliesLimitToScrollingCorrectly() {
|
||||
|
||||
Window<Person> page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc("*a*",
|
||||
ScrollPosition.keyset(), Limit.of(2));
|
||||
|
||||
assertThat(page.isLast()).isFalse();
|
||||
assertThat(page.size()).isEqualTo(2);
|
||||
assertThat(page).contains(carter);
|
||||
}
|
||||
|
||||
@Test // GH-4308
|
||||
void appliesScrollPositionWithProjectionCorrectly() {
|
||||
|
||||
@@ -236,6 +247,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
|
||||
assertThat(page).contains(carter, stefan);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void executesFinderCorrectlyWithSortAndLimit() {
|
||||
|
||||
List<Person> page = repository.findByLastnameLike("*a*", Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2));
|
||||
|
||||
assertThat(page).containsExactly(carter, stefan);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executesPagedFinderWithAnnotatedQueryCorrectly() {
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Range;
|
||||
@@ -126,6 +127,9 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
|
||||
ScrollPosition scrollPosition);
|
||||
|
||||
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
|
||||
ScrollPosition scrollPosition, Limit limit);
|
||||
|
||||
/**
|
||||
* Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
|
||||
* supported).
|
||||
@@ -145,6 +149,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
*/
|
||||
Page<Person> findByLastnameLike(String lastname, Pageable pageable);
|
||||
|
||||
List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit);
|
||||
|
||||
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
|
||||
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.domain.Limit;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -493,6 +494,30 @@ class AbstractMongoQueryUnitTests {
|
||||
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void limitShouldBeAppliedToQuery() {
|
||||
|
||||
createQueryForMethod("findWithLimit", String.class, Limit.class).execute(new Object[] { "dalinar", Limit.of(42) });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getLimit()).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test // GH-4397
|
||||
void sortAndLimitShouldBeAppliedToQuery() {
|
||||
|
||||
createQueryForMethod("findWithSortAndLimit", String.class, Sort.class, Limit.class)
|
||||
.execute(new Object[] { "dalinar", Sort.by("fn"), Limit.of(42) });
|
||||
|
||||
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
|
||||
verify(withQueryMock).matching(captor.capture());
|
||||
|
||||
assertThat(captor.getValue().getLimit()).isEqualTo(42);
|
||||
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1));
|
||||
}
|
||||
|
||||
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
return createQueryForMethod(Repo.class, methodName, paramTypes);
|
||||
}
|
||||
@@ -614,6 +639,10 @@ class AbstractMongoQueryUnitTests {
|
||||
|
||||
@Hint("idx-fn")
|
||||
void findWithHintByFirstname(String firstname);
|
||||
|
||||
List<Person> findWithLimit(String firstname, Limit limit);
|
||||
|
||||
List<Person> findWithSortAndLimit(String firstname, Sort sort, Limit limit);
|
||||
}
|
||||
|
||||
// DATAMONGO-1872
|
||||
|
||||
Reference in New Issue
Block a user