Skip to content

Add support for $expr operator #3790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
import java.util.LinkedHashMap;
import java.util.Map;

import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder.ValueAppender;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.GroupOperation.Keyword;
import org.springframework.data.mongodb.core.aggregation.GroupOperation.Operation;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;

import org.springframework.lang.Nullable;

/**
Expand Down Expand Up @@ -123,14 +129,16 @@ private AddFieldsOperationBuilder() {
private AddFieldsOperationBuilder(Map<Object, Object> source) {
this.valueMap = new LinkedHashMap<>(source);
}

public AddFieldsOperationBuilder addFieldWithValue(String field, @Nullable Object value) {
return addField(field).withValue(value);
}

public AddFieldsOperationBuilder addFieldWithValueOf(String field, Object value) {
return addField(field).withValueOf(value);
}



/**
* Define the field to add.
Expand Down Expand Up @@ -168,7 +176,7 @@ public AddFieldsOperationBuilder withValueOfExpression(String operation, Object.
public AddFieldsOperation build() {
return new AddFieldsOperation(valueMap);
}

/**
* @author Christoph Strobl
* @since 3.0
Expand Down Expand Up @@ -201,4 +209,6 @@ public interface ValueAppender {
AddFieldsOperationBuilder withValueOfExpression(String operation, Object... values);
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,17 @@ public static MatchOperation match(Criteria criteria) {
public static MatchOperation match(CriteriaDefinition criteria) {
return new MatchOperation(criteria);
}


/**
* Creates a new {@link MatchOperation}
*
* @return new instance of {@link MatchOperation}.
* @since 1.10
*/
public static MatchOperation match() {
return new MatchOperation();
}

/**
* Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the {@code distanceField}. The
* {@code distanceField} defines output field that contains the calculated distance.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.springframework.data.mongodb.core.aggregation;

import org.springframework.util.Assert;

public class EvaluationOperators {

/**
* Take the value resulting from the given fieldReference.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link EvaluationOperatorFactory}.
*/
public static EvaluationOperatorFactory valueOf(String fieldReference) {
return new EvaluationOperatorFactory(fieldReference);
}

/**
* Take the value resulting from the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link EvaluationOperatorFactory}.
*/
public static EvaluationOperatorFactory valueOf(AggregationExpression expression) {
return new EvaluationOperatorFactory(expression);
}

public static class EvaluationOperatorFactory {

private final String fieldReference;
private final AggregationExpression expression;

/**
* Creates new {@link EvaluationOperatorFactory} for given {@literal fieldReference}.
*
* @param fieldReference must not be {@literal null}.
*/
public EvaluationOperatorFactory(String fieldReference) {

Assert.notNull(fieldReference, "FieldReference must not be null!");
this.fieldReference = fieldReference;
this.expression = null;
}


/**
* Creates new {@link EvaluationOperatorFactory} for given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
*/
public EvaluationOperatorFactory(AggregationExpression expression) {

Assert.notNull(expression, "Expression must not be null!");
this.fieldReference = null;
this.expression = expression;
}

/**
* Creates new {@link AggregationExpression} that is a valid aggregation expression.
*
* @return new instance of {@link Expr}.
*/
public Expr expr() {
return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression);
}


public static class Expr extends AbstractAggregationExpression {

private Expr(Object value) {
super(value);
}

@Override
protected String getMongoMethod() {
return "$expr";
}

/**
* Creates new {@link Expr}.
*
* @param fieldReference must not be {@literal null}.
* @return new instance of {@link Expr}.
*/
public static Expr valueOf(String fieldReference) {

Assert.notNull(fieldReference, "FieldReference must not be null!");
return new Expr(Fields.field(fieldReference));
}

/**
* Creates new {@link Expr}.
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Expr}.
*/
public static Expr valueOf(AggregationExpression expression) {

Assert.notNull(expression, "Expression must not be null!");
return new Expr(expression);
}

}

private boolean usesFieldRef() {
return fieldReference != null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.aggregation;

import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.EvaluationOperatorFactory.Expr;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;

Expand All @@ -36,7 +37,16 @@
public class MatchOperation implements AggregationOperation {

private final CriteriaDefinition criteriaDefinition;

private final AggregationExpression expression;

/**
* Creates a new {@link MatchOperation}
*/
public MatchOperation() {
this.criteriaDefinition = null;
this.expression = null;
}

/**
* Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}.
*
Expand All @@ -46,14 +56,39 @@ public MatchOperation(CriteriaDefinition criteriaDefinition) {

Assert.notNull(criteriaDefinition, "Criteria must not be null!");
this.criteriaDefinition = criteriaDefinition;
this.expression = null;
}


/**
* Creates a new {@link MatchOperation} for the given {@link Expression}.
*
* @param criteriaDefinition must not be {@literal null}.
*/
private MatchOperation(Expr expression) {
Assert.notNull(expression, "Expression must not be null!");
this.criteriaDefinition = null;
this.expression = expression;
}

/**
* Creates a new {@link MatchOperation} for the given {@link AggregationExpression}.
*
* @param expression must not be {@literal null}.
*/
public MatchOperation withValueOf(AggregationExpression expression) {
Assert.notNull(expression, "Expression must not be null!");
return new MatchOperation(EvaluationOperators.valueOf(expression).expr());
}

/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
@Override
public Document toDocument(AggregationOperationContext context) {
if(expression != null) {
return new Document(getOperator(), expression.toDocument());
}
return new Document(getOperator(), context.getMappedObject(criteriaDefinition.getCriteriaObject()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1306,7 +1306,7 @@ public ProjectionOperationBuilder let(AggregationExpression valueExpression, Str
public ProjectionOperationBuilder let(Collection<ExpressionVariable> variables, AggregationExpression in) {
return this.operation.and(VariableOperators.Let.define(variables).andApply(in));
}

private String getRequiredName() {

Assert.state(name != null, "Projection field name must not be null!");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.List;

import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.expression.spel.ast.Projection;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -112,7 +113,8 @@ public ExposedFields getFields() {
protected Replacement getReplacement() {
return replacement;
}



/**
* Builder for {@link ReplaceRootOperation}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender;
import org.springframework.lang.Nullable;

Expand Down Expand Up @@ -193,5 +194,7 @@ public interface ValueAppender {
*/
SetOperation withValueOfExpression(String operation, Object... values);
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ void exposesFieldsCorrectly() {
assertThat(fields.getField("computed")).isNotNull();
assertThat(fields.getField("does-not-exist")).isNull();
}

@Test // DATAMONGO - 3729
void rendersStdDevPopCorrectly() {
assertThat(AddFieldsOperation.builder().addField("totalQuiz")
.withValue(ArithmeticOperators.valueOf("quiz").stdDevPop()).build()
.toPipelineStages(contextFor(ScoresWrapper.class))).containsExactly(
Document.parse("{\"$addFields\" : {\"totalQuiz\": { \"$stdDevPop\" : \"$quiz\" } }}"));
}

@Test // DATAMONGO - 3729
void rendersStdDevSampCorrectly() {
assertThat(AddFieldsOperation.builder().addField("totalQuiz")
.withValue(ArithmeticOperators.valueOf("quiz").stdDevSamp()).build()
.toPipelineStages(contextFor(ScoresWrapper.class)))
.containsExactly(Document.parse(
"{\"$addFields\" : {\"totalQuiz\": { \"$stdDevSamp\" : \"$quiz\" } }}"));
}

private static AggregationOperationContext contextFor(@Nullable Class<?> type) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ void unwindOperationWithPreserveNullShouldBuildCorrectClause() {
.doesNotContainKey("$unwind.includeArrayIndex") //
.containsEntry("$unwind.preserveNullAndEmptyArrays", true);
}


@Test // DATAMONGO-1550
void replaceRootOperationShouldBuildCorrectClause() {
Expand All @@ -145,7 +146,7 @@ void replaceRootOperationShouldBuildCorrectClause() {
Document unwind = ((List<Document>) agg.get("pipeline")).get(0);
assertThat(unwind).containsEntry("$replaceRoot.newRoot", new Document("field", "value"));
}

@Test // DATAMONGO-753
void matchOperationShouldNotChangeAvailableFields() {

Expand All @@ -155,7 +156,7 @@ void matchOperationShouldNotChangeAvailableFields() {
project("a", "b") // b should still be available
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
}

@Test // DATAMONGO-788
void referencesToGroupIdsShouldBeRenderedAsReferences() {

Expand Down Expand Up @@ -597,7 +598,7 @@ void projectOnIdIsAlwaysValid() {

assertThat(extractPipelineElement(target, 1, "$project")).isEqualTo(Document.parse(" { \"_id\" : \"$_id\" }"));
}

private Document extractPipelineElement(Document agg, int index, String operation) {

List<Document> pipeline = (List<Document>) agg.get("pipeline");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.springframework.data.mongodb.core.aggregation;

import static org.assertj.core.api.Assertions.*;

import org.bson.Document;
import org.junit.jupiter.api.Test;

class MatchOperationUnitTests {

@Test // DATAMONGO - 3729
public void shouldRenderStdDevPopCorrectly() {
MatchOperation operation = Aggregation.match().withValueOf(ArithmeticOperators.valueOf("quiz").stdDevPop());
assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT)).
isEqualTo(Document.parse("{ $match: { \"$expr\" : { \"$stdDevPop\" : \"$quiz\" } } } "));

}

@Test // DATAMONGO - 3729
public void shouldRenderStdDevSampCorrectly() {
MatchOperation operation = Aggregation.match().withValueOf(ArithmeticOperators.valueOf("quiz").stdDevSamp());
assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT)).
isEqualTo(Document.parse("{ $match: { \"$expr\" : { \"$stdDevSamp\" : \"$quiz\" } } } "));

}

}
Loading