diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java index 6c69eb2ba..40143356b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -26,6 +27,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond.OtherwiseBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond.ThenBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.AsBuilder; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.query.CriteriaDefinition; @@ -1984,6 +1986,28 @@ public interface AggregationExpressions { public static Map.AsBuilder mapItemsOf(AggregationExpression expression) { return Map.itemsOf(expression); } + + /** + * Start creating new {@link Let} that allows definition of {@link ExpressionVariable} that can be used within a + * nested {@link AggregationExpression}. + * + * @param variables must not be {@literal null}. + * @return + */ + public static Let.LetBuilder define(ExpressionVariable... variables) { + return Let.define(variables); + } + + /** + * Start creating new {@link Let} that allows definition of {@link ExpressionVariable} that can be used within a + * nested {@link AggregationExpression}. + * + * @param variables must not be {@literal null}. + * @return + */ + public static Let.LetBuilder define(Collection variables) { + return Let.define(variables); + } } /** @@ -6694,4 +6718,185 @@ public interface AggregationExpressions { } } } + + /** + * {@link AggregationExpression} for {@code $let} that binds {@link AggregationExpression} to variables for use in the + * specified {@code in} expression, and returns the result of the expression. + * + * @author Christoph Strobl + * @since 1.10 + */ + class Let implements AggregationExpression { + + private final List vars; + private final AggregationExpression expression; + + private Let(List vars, AggregationExpression expression) { + + this.vars = vars; + this.expression = expression; + } + + /** + * Start creating new {@link Let} by defining the variables for {@code $vars}. + * + * @param variables must not be {@literal null}. + * @return + */ + public static LetBuilder define(final Collection variables) { + + Assert.notNull(variables, "Variables must not be null!"); + + return new LetBuilder() { + @Override + public Let andApply(final AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Let(new ArrayList(variables), expression); + } + }; + } + + /** + * Start creating new {@link Let} by defining the variables for {@code $vars}. + * + * @param variables must not be {@literal null}. + * @return + */ + public static LetBuilder define(final ExpressionVariable... variables) { + + Assert.notNull(variables, "Variables must not be null!"); + + return new LetBuilder() { + @Override + public Let andApply(final AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Let(Arrays.asList(variables), expression); + } + }; + } + + public interface LetBuilder { + + /** + * Define the {@link AggregationExpression} to evaluate. + * + * @param expression must not be {@literal null}. + * @return + */ + Let andApply(AggregationExpression expression); + } + + @Override + public Document toDocument(final AggregationOperationContext context) { + + return toLet(new ExposedFieldsAggregationOperationContext( + ExposedFields.synthetic(Fields.fields(getVariableNames())), context) { + + @Override + public FieldReference getReference(Field field) { + + FieldReference ref = null; + try { + ref = context.getReference(field); + } catch (Exception e) { + // just ignore that one. + } + return ref != null ? ref : super.getReference(field); + } + }); + } + + private String[] getVariableNames() { + + String[] varNames = new String[this.vars.size()]; + for (int i = 0; i < this.vars.size(); i++) { + varNames[i] = this.vars.get(i).variableName; + } + return varNames; + } + + private Document toLet(AggregationOperationContext context) { + + Document letExpression = new Document(); + + Document mappedVars = new Document(); + for (ExpressionVariable var : this.vars) { + mappedVars.putAll(getMappedVariable(var, context)); + } + + letExpression.put("vars", mappedVars); + letExpression.put("in", getMappedIn(context)); + + return new Document("$let", letExpression); + } + + private Document getMappedVariable(ExpressionVariable var, AggregationOperationContext context) { + + return new Document(var.variableName, var.expression instanceof AggregationExpression + ? ((AggregationExpression) var.expression).toDocument(context) : var.expression); + } + + private Object getMappedIn(AggregationOperationContext context) { + return expression.toDocument(new NestedDelegatingExpressionAggregationOperationContext(context)); + } + + /** + * @author Christoph Strobl + */ + public static class ExpressionVariable { + + private final String variableName; + private final Object expression; + + /** + * Creates new {@link ExpressionVariable}. + * + * @param variableName can be {@literal null}. + * @param expression can be {@literal null}. + */ + private ExpressionVariable(String variableName, Object expression) { + + this.variableName = variableName; + this.expression = expression; + } + + /** + * Create a new {@link ExpressionVariable} with given name. + * + * @param variableName must not be {@literal null}. + * @return never {@literal null}. + */ + public static ExpressionVariable newVariable(String variableName) { + + Assert.notNull(variableName, "VariableName must not be null!"); + return new ExpressionVariable(variableName, null); + } + + /** + * Create a new {@link ExpressionVariable} with current name and given {@literal expression}. + * + * @param expression must not be {@literal null}. + * @return never {@literal null}. + */ + public ExpressionVariable forExpression(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ExpressionVariable(variableName, expression); + } + + /** + * Create a new {@link ExpressionVariable} with current name and given {@literal expressionObject}. + * + * @param expressionObject must not be {@literal null}. + * @return never {@literal null}. + */ + public ExpressionVariable forExpression(Document expressionObject) { + + Assert.notNull(expressionObject, "Expression must not be null!"); + return new ExpressionVariable(variableName, expressionObject); + } + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java index 2804ca0bf..695e874fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java @@ -35,7 +35,7 @@ import org.springframework.util.Assert; @Deprecated public enum AggregationFunctionExpressions { - SIZE, CMP, EQ, GT, GTE, LT, LTE, NE, SUBTRACT, ADD; + SIZE, CMP, EQ, GT, GTE, LT, LTE, NE, SUBTRACT, ADD, MULTIPLY; /** * Returns an {@link AggregationExpression} build from the current {@link Enum} name and the given parameters. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 1bf6d9967..e735c155c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -17,10 +17,12 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.IfNull; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; @@ -850,7 +852,7 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { /** * Generates a {@code $setIsSubset} expression that takes array of the previously mentioned field and returns * {@literal true} if it is a subset of the given {@literal array}. - * + * * @param array must not be {@literal null}. * @return never {@literal null}. * @since 1.10 @@ -1193,7 +1195,35 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation { return this.operation.and(AggregationExpressions.DateToString.dateOf(name).toString(format)); } - /* + /** + * Generates a {@code $let} expression that binds variables for use in the specified expression, and returns the + * result of the expression. + * + * @param valueExpression The {@link AggregationExpression} bound to {@literal variableName}. + * @param variableName The variable name to be used in the {@literal in} {@link AggregationExpression}. + * @param in The {@link AggregationExpression} to evaluate. + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder let(AggregationExpression valueExpression, String variableName, + AggregationExpression in) { + return this.operation.and(AggregationExpressions.Let.define(ExpressionVariable.newVariable(variableName).forExpression(valueExpression)).andApply(in)); + } + + /** + * Generates a {@code $let} expression that binds variables for use in the specified expression, and returns the + * result of the expression. + * + * @param variables The bound {@link ExpressionVariable}s. + * @param in The {@link AggregationExpression} to evaluate. + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder let(Collection variables, AggregationExpression in) { + return this.operation.and(AggregationExpressions.Let.define(variables).andApply(in)); + } + + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 81128b808..3636bedcd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -60,6 +60,8 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ConditionalOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; import org.springframework.data.mongodb.core.index.GeospatialIndex; import org.springframework.data.mongodb.core.query.Criteria; @@ -647,9 +649,9 @@ public class AggregationTests { mongoTemplate.insert(new LineItem("idonly", null, 0)); TypedAggregation aggregation = newAggregation(LineItem.class, // -project("id") // - .and("caption")// - .applyCondition(ConditionalOperators.ifNull("caption").then("unknown")), + project("id") // + .and("caption")// + .applyCondition(ConditionalOperators.ifNull("caption").then("unknown")), sort(ASC, "id")); assertThat(aggregation.toString(), is(notNullValue())); @@ -1541,6 +1543,36 @@ project("id") // Sales.builder().id("2").items(Collections. emptyList()).build())); } + /** + * @see DATAMONGO-1538 + */ + @Test + public void letShouldBeAppliedCorrectly() { + + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO)); + + Sales2 sales1 = Sales2.builder().id("1").price(10).tax(0.5F).applyDiscount(true).build(); + Sales2 sales2 = Sales2.builder().id("2").price(10).tax(0.25F).applyDiscount(false).build(); + + mongoTemplate.insert(Arrays.asList(sales1, sales2), Sales2.class); + + ExpressionVariable total = ExpressionVariable.newVariable("total") + .forExpression(AggregationFunctionExpressions.ADD.of(Fields.field("price"), Fields.field("tax"))); + ExpressionVariable discounted = ExpressionVariable.newVariable("discounted") + .forExpression(Cond.when("applyDiscount").then(0.9D).otherwise(1.0D)); + + TypedAggregation agg = Aggregation.newAggregation(Sales2.class, + Aggregation.project() + .and(Let.define(total, discounted).andApply( + AggregationFunctionExpressions.MULTIPLY.of(Fields.field("total"), Fields.field("discounted")))) + .as("finalTotal")); + + AggregationResults result = mongoTemplate.aggregate(agg, Document.class); + assertThat(result.getMappedResults(), + contains(new Document("_id", "1").append("finalTotal", 9.450000000000001D), + new Document("_id", "2").append("finalTotal", 10.25D))); + } + private void createUsersWithReferencedPersons() { mongoTemplate.dropCollection(User.class); @@ -1782,6 +1814,9 @@ project("id") // } } + /** + * @DATAMONGO-1491 + */ @lombok.Data @Builder static class Sales { @@ -1790,6 +1825,9 @@ project("id") // List items; } + /** + * @DATAMONGO-1491 + */ @lombok.Data @Builder static class Item { @@ -1799,4 +1837,17 @@ project("id") // Integer quantity; Long price; } + + /** + * @DATAMONGO-1538 + */ + @lombok.Data + @Builder + static class Sales2 { + + String id; + Integer price; + Float tax; + boolean applyDiscount; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 2f0432e8e..655c9397e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.core.aggregation; import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable.*; import static org.springframework.data.mongodb.core.aggregation.AggregationFunctionExpressions.*; import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; @@ -25,22 +27,14 @@ import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.util.Arrays; import java.util.List; -import com.mongodb.DBObject; -import com.mongodb.util.JSON; import org.bson.Document; import org.junit.Test; import org.springframework.data.mongodb.core.DocumentTestUtils; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArithmeticOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArrayOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.BooleanOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ComparisonOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ConditionalOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.DateOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.LiteralOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.SetOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.StringOperators; -import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.VariableOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.*; + +import com.mongodb.util.JSON; /** * Unit tests for {@link ProjectionOperation}. @@ -286,9 +280,8 @@ public class ProjectionOperationUnitTests { .and("foo").as("bar"); // Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat( - document, - is(Document.parse("{ \"$project\" : { \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"bar\" : \"$foo\"}}"))); + assertThat(document, is(Document.parse( + "{ \"$project\" : { \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"bar\" : \"$foo\"}}"))); } /** @@ -393,8 +386,7 @@ public class ProjectionOperationUnitTests { Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); Document projected = exctractOperation("$project", document); - assertThat(projected.get("renamed"), - is((Object) new Document("$slice", Arrays. asList("$field", 10)))); + assertThat(projected.get("renamed"), is((Object) new Document("$slice", Arrays. asList("$field", 10)))); } /** @@ -408,8 +400,7 @@ public class ProjectionOperationUnitTests { Document document = operation.toDocument(Aggregation.DEFAULT_CONTEXT); Document projected = exctractOperation("$project", document); - assertThat(projected.get("renamed"), - is((Object) new Document("$slice", Arrays. asList("$field", 5, 10)))); + assertThat(projected.get("renamed"), is((Object) new Document("$slice", Arrays. asList("$field", 5, 10)))); } /** @@ -602,7 +593,8 @@ public class ProjectionOperationUnitTests { Document agg = project("A", "B").and("A").subsetOfArray("B").as("aIsSubsetOfB") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { A: 1, B: 1, aIsSubsetOfB: { $setIsSubset: [ \"$A\", \"$B\" ] }}}"))); + assertThat(agg, + is(Document.parse("{ $project: { A: 1, B: 1, aIsSubsetOfB: { $setIsSubset: [ \"$A\", \"$B\" ] }}}"))); } /** @@ -614,7 +606,8 @@ public class ProjectionOperationUnitTests { Document agg = project("A", "B").and(SetOperators.arrayAsSet("A").isSubsetOf("B")).as("aIsSubsetOfB") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { A: 1, B: 1, aIsSubsetOfB: { $setIsSubset: [ \"$A\", \"$B\" ] }}}"))); + assertThat(agg, + is(Document.parse("{ $project: { A: 1, B: 1, aIsSubsetOfB: { $setIsSubset: [ \"$A\", \"$B\" ] }}}"))); } /** @@ -626,7 +619,8 @@ public class ProjectionOperationUnitTests { Document agg = project("responses").and("responses").anyElementInArrayTrue().as("isAnyTrue") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { responses: 1, isAnyTrue: { $anyElementTrue: [ \"$responses\" ] }}}"))); + assertThat(agg, + is(Document.parse("{ $project: { responses: 1, isAnyTrue: { $anyElementTrue: [ \"$responses\" ] }}}"))); } /** @@ -638,7 +632,8 @@ public class ProjectionOperationUnitTests { Document agg = project("responses").and(SetOperators.arrayAsSet("responses").anyElementTrue()).as("isAnyTrue") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { responses: 1, isAnyTrue: { $anyElementTrue: [ \"$responses\" ] }}}"))); + assertThat(agg, + is(Document.parse("{ $project: { responses: 1, isAnyTrue: { $anyElementTrue: [ \"$responses\" ] }}}"))); } /** @@ -739,8 +734,8 @@ public class ProjectionOperationUnitTests { .divide(AggregationFunctionExpressions.SUBTRACT.of(field("start"), field("end"))).as("result") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $project: { result: { $divide: [ \"$value\", { $subtract: [ \"$start\", \"$end\" ] }] } }}"))); + assertThat(agg, is(Document + .parse("{ $project: { result: { $divide: [ \"$value\", { $subtract: [ \"$start\", \"$end\" ] }] } }}"))); } /** @@ -852,7 +847,8 @@ public class ProjectionOperationUnitTests { ArithmeticOperators.valueOf(AggregationFunctionExpressions.SUBTRACT.of(field("start"), field("end"))).log(2)) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { result: { $log: [ { $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); + assertThat(agg, + is(Document.parse("{ $project: { result: { $log: [ { $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); } /** @@ -902,7 +898,8 @@ public class ProjectionOperationUnitTests { ArithmeticOperators.valueOf(AggregationFunctionExpressions.SUBTRACT.of(field("start"), field("end"))).mod(2)) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { result: { $mod: [{ $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); + assertThat(agg, + is(Document.parse("{ $project: { result: { $mod: [{ $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); } /** @@ -915,8 +912,8 @@ public class ProjectionOperationUnitTests { .multiply(AggregationFunctionExpressions.SUBTRACT.of(field("start"), field("end"))).as("result") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is( - Document.parse("{ $project: { result: { $multiply: [\"$value\", { $subtract: [ \"$start\", \"$end\" ] }] } }}"))); + assertThat(agg, is(Document + .parse("{ $project: { result: { $multiply: [\"$value\", { $subtract: [ \"$start\", \"$end\" ] }] } }}"))); } /** @@ -955,7 +952,8 @@ public class ProjectionOperationUnitTests { ArithmeticOperators.valueOf(AggregationFunctionExpressions.SUBTRACT.of(field("start"), field("end"))).pow(2)) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { result: { $pow: [{ $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); + assertThat(agg, + is(Document.parse("{ $project: { result: { $pow: [{ $subtract: [ \"$start\", \"$end\" ] }, 2] } }}"))); } /** @@ -991,8 +989,8 @@ public class ProjectionOperationUnitTests { Document agg = project().and("numericField").minus(AggregationFunctionExpressions.SIZE.of(field("someArray"))) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $project: { result: { $subtract: [ \"$numericField\", { $size : [\"$someArray\"]}] } } }"))); + assertThat(agg, is( + Document.parse("{ $project: { result: { $subtract: [ \"$numericField\", { $size : [\"$someArray\"]}] } } }"))); } /** @@ -1006,8 +1004,8 @@ public class ProjectionOperationUnitTests { .subtract(AggregationFunctionExpressions.SIZE.of(field("someArray")))) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $project: { result: { $subtract: [ \"$numericField\", { $size : [\"$someArray\"]}] } } }"))); + assertThat(agg, is( + Document.parse("{ $project: { result: { $subtract: [ \"$numericField\", { $size : [\"$someArray\"]}] } } }"))); } /** @@ -1140,7 +1138,8 @@ public class ProjectionOperationUnitTests { Document agg = project().and("quarter").strCaseCmp("13q4").as("comparisonResult") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { comparisonResult: { $strcasecmp: [ \"$quarter\", \"13q4\" ] } } }"))); + assertThat(agg, + is(Document.parse("{ $project: { comparisonResult: { $strcasecmp: [ \"$quarter\", \"13q4\" ] } } }"))); } /** @@ -1152,7 +1151,8 @@ public class ProjectionOperationUnitTests { Document agg = project().and(StringOperators.valueOf("quarter").strCaseCmp("13q4")).as("comparisonResult") .toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is(Document.parse("{ $project: { comparisonResult: { $strcasecmp: [ \"$quarter\", \"13q4\" ] } } }"))); + assertThat(agg, + is(Document.parse("{ $project: { comparisonResult: { $strcasecmp: [ \"$quarter\", \"13q4\" ] } } }"))); } /** @@ -1644,8 +1644,8 @@ public class ProjectionOperationUnitTests { .and(ComparisonOperators.valueOf("qty").lessThanValue(250))) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is( - Document.parse("{ $project: { result: { $and: [ { $gt: [ \"$qty\", 100 ] }, { $lt: [ \"$qty\", 250 ] } ] } } }"))); + assertThat(agg, is(Document + .parse("{ $project: { result: { $and: [ { $gt: [ \"$qty\", 100 ] }, { $lt: [ \"$qty\", 250 ] } ] } } }"))); } /** @@ -1659,8 +1659,8 @@ public class ProjectionOperationUnitTests { .or(ComparisonOperators.valueOf("qty").lessThanValue(200))) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, is( - Document.parse("{ $project: { result: { $or: [ { $gt: [ \"$qty\", 250 ] }, { $lt: [ \"$qty\", 200 ] } ] } } }"))); + assertThat(agg, is(Document + .parse("{ $project: { result: { $or: [ { $gt: [ \"$qty\", 250 ] }, { $lt: [ \"$qty\", 200 ] } ] } } }"))); } /** @@ -1711,11 +1711,12 @@ public class ProjectionOperationUnitTests { @Test public void shouldRenderIfNullConditionAggregationExpression() { - Document agg = project().and(ConditionalOperators.ifNull(ArrayOperators.arrayOf("array").elementAt(1)).then("a more sophisticated value")) + Document agg = project().and( + ConditionalOperators.ifNull(ArrayOperators.arrayOf("array").elementAt(1)).then("a more sophisticated value")) .as("result").toDocument(Aggregation.DEFAULT_CONTEXT); - assertThat(agg, - is(Document.parse("{ $project: { result: { $ifNull: [ { $arrayElemAt: [\"$array\", 1] }, \"a more sophisticated value\" ] } } }"))); + assertThat(agg, is(Document.parse( + "{ $project: { result: { $ifNull: [ { $arrayElemAt: [\"$array\", 1] }, \"a more sophisticated value\" ] } } }"))); } /** @@ -1744,6 +1745,58 @@ public class ProjectionOperationUnitTests { assertThat(agg, is(Document.parse("{ $project: { result: { $ifNull: [ \"$optional\", \"$never-null\" ] } } }"))); } + /** + * @see DATAMONGO-1538 + */ + @Test + public void shouldRenderLetExpressionCorrectly() { + + Document agg = Aggregation.project() + .and(VariableOperators + .define( + newVariable("total") + .forExpression(AggregationFunctionExpressions.ADD.of(Fields.field("price"), Fields.field("tax"))), + newVariable("discounted").forExpression(Cond.when("applyDiscount").then(0.9D).otherwise(1.0D))) + .andApply(AggregationFunctionExpressions.MULTIPLY.of(Fields.field("total"), Fields.field("discounted")))) // + .as("finalTotal").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, + is(Document.parse("{ $project:{ \"finalTotal\" : { \"$let\": {" + // + "\"vars\": {" + // + "\"total\": { \"$add\": [ \"$price\", \"$tax\" ] }," + // + "\"discounted\": { \"$cond\": { \"if\": \"$applyDiscount\", \"then\": 0.9, \"else\": 1.0 } }" + // + "}," + // + "\"in\": { \"$multiply\": [ \"$$total\", \"$$discounted\" ] }" + // + "}}}}"))); + } + + /** + * @see DATAMONGO-1538 + */ + @Test + public void shouldRenderLetExpressionCorrectlyWhenUsingLetOnProjectionBuilder() { + + ExpressionVariable var1 = newVariable("total") + .forExpression(AggregationFunctionExpressions.ADD.of(Fields.field("price"), Fields.field("tax"))); + + ExpressionVariable var2 = newVariable("discounted") + .forExpression(Cond.when("applyDiscount").then(0.9D).otherwise(1.0D)); + + Document agg = Aggregation.project().and("foo") + .let(Arrays.asList(var1, var2), + AggregationFunctionExpressions.MULTIPLY.of(Fields.field("total"), Fields.field("discounted"))) + .as("finalTotal").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, + is(Document.parse("{ $project:{ \"finalTotal\" : { \"$let\": {" + // + "\"vars\": {" + // + "\"total\": { \"$add\": [ \"$price\", \"$tax\" ] }," + // + "\"discounted\": { \"$cond\": { \"if\": \"$applyDiscount\", \"then\": 0.9, \"else\": 1.0 } }" + // + "}," + // + "\"in\": { \"$multiply\": [ \"$$total\", \"$$discounted\" ] }" + // + "}}}}"))); + } + private static Document exctractOperation(String field, Document fromProjectClause) { return (Document) fromProjectClause.get(field); }