From 55ae80c8f9c4fbc11556793c04962125ad946eec Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 20 Sep 2022 14:31:49 +0200 Subject: [PATCH 01/21] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index a401249d79..44d406e395 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.x-GH-4139-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index c28a240d2c..e6c6c85c05 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.x-GH-4139-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 0412911f82..4ccc16c862 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.x-GH-4139-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index cb8c76ade4..d86ed0bbfd 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.0.0-SNAPSHOT + 4.0.x-GH-4139-SNAPSHOT ../pom.xml From 0c3d840bd344091c0d7a97e3f0f3c98acf00f92b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 10:24:51 +0200 Subject: [PATCH 02/21] Add support for $bottom aggregation operator --- .../AbstractAggregationExpression.java | 24 ++++- .../core/aggregation/GroupOperation.java | 11 +++ .../core/aggregation/SelectionOperators.java | 95 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 4 + .../aggregation/GroupOperationUnitTests.java | 14 +++ .../SelectionOperatorUnitTests.java | 80 ++++++++++++++++ .../SpelExpressionTransformerUnitTests.java | 5 + .../reference/aggregation-framework.adoc | 3 + 8 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index bb46ee3959..f5795bd164 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -22,8 +22,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.bson.Document; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -68,8 +72,24 @@ private Object unpack(Object value, AggregationOperationContext context) { return ((AggregationExpression) value).toDocument(context); } - if (value instanceof Field) { - return context.getReference((Field) value).toString(); + if (value instanceof Field field) { + return context.getReference(field).toString(); + } + + if(value instanceof Fields fields) { + return fields.asList().stream().map(it -> unpack(it, context)).collect(Collectors.toList()); + } + + if(value instanceof Sort sort) { + + Document sortDoc = new Document(); + for (Order order : sort) { + + // Check reference + FieldReference reference = context.getReference(order.getProperty()); + sortDoc.put(reference.getRaw(), order.isAscending() ? 1 : -1); + } + return sortDoc; } if (value instanceof List) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index d7a8887f33..d2dcb44129 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -387,6 +387,17 @@ public GroupOperationBuilder accumulate(Accumulator accumulator) { return new GroupOperationBuilder(this, new Operation(accumulator)); } + /** + * Adds a computed field to the {@link GroupOperation}. + * + * @param expression must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + public GroupOperation and(String fieldName, AggregationExpression expression) { + return new GroupOperationBuilder(this, new Operation(expression)).as(fieldName); + } + private GroupOperationBuilder newBuilder(Keyword keyword, @Nullable String reference, @Nullable Object value) { return new GroupOperationBuilder(this, new Operation(keyword, null, reference, value)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java new file mode 100644 index 0000000000..96ab86a004 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 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 java.util.Arrays; +import java.util.Collections; + +import org.springframework.data.domain.Sort; + +/** + * Gateway to {@literal selection operators} such as {@literal $bottom}. + * + * @author Christoph Strobl + * @since 4.0 + */ +public class SelectionOperators { + + /** + * {@link AbstractAggregationExpression} to return the bottom element according to the specified {@link #sortBy(Sort) + * order}. + */ + public static class Bottom extends AbstractAggregationExpression { + + private Bottom(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$bottom"; + } + + /** + * @return new instance of {@link Bottom}. + */ + public static Bottom bottom() { + return new Bottom(Collections.emptyMap()); + } + + /** + * Define result ordering. + * + * @param sort must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Bottom sortBy(Sort sort) { + return new Bottom(append("sortBy", sort)); + } + + /** + * Define result ordering. + * + * @param out must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Bottom output(Fields out) { + return new Bottom(append("output", out)); + } + + /** + * Define fields included in the output for each element. + * + * @param fieldNames must not be {@literal null}. + * @return new instance of {@link Bottom}. + * @see #output(Fields) + */ + public Bottom output(String... fieldNames) { + return output(Fields.fields(fieldNames)); + } + + /** + * Define expressions building the value included in the output for each element. + * + * @param out must not be {@literal null}. + * @return new instance of {@link Bottom}. + * @see #output(Fields) + */ + public Bottom output(AggregationExpression... out) { + return new Bottom(append("output", Arrays.asList(out))); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index bc73ac5aa8..bd2586e85a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -226,6 +226,10 @@ public class MethodReferenceNode extends ExpressionNode { map.put("toString", singleArgRef().forOperator("$toString")); map.put("degreesToRadians", singleArgRef().forOperator("$degreesToRadians")); + // SELECT OPERATORS + map.put("bottom", mapArgRef().forOperator("$bottom") // + .mappingParametersTo("output", "sortBy")); + FUNCTIONS = Collections.unmodifiableMap(map); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java index a0bdf2ddc3..48b73ac882 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java @@ -22,7 +22,10 @@ import org.bson.Document; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.DocumentTestUtils; +import org.springframework.data.mongodb.core.aggregation.SelectionOperators.Bottom; import org.springframework.data.mongodb.core.query.Criteria; /** @@ -252,6 +255,17 @@ void accumulatorShouldBeAllowedOnGroupOperation() { assertThat(accumulatedValue).containsKey("$accumulator"); } + @Test // GH-4139 + void groupOperationAllowsToAddFieldsComputedViaExpression() { + + GroupOperation groupOperation = Aggregation.group("id").and("playerId", + Bottom.bottom().output("playerId", "score").sortBy(Sort.by(Direction.DESC, "score"))); + Document groupClause = extractDocumentFromGroupOperation(groupOperation); + + assertThat(groupClause).containsEntry("playerId", + Document.parse("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}")); + } + private Document extractDocumentFromGroupOperation(GroupOperation groupOperation) { Document document = groupOperation.toDocument(Aggregation.DEFAULT_CONTEXT); Document groupClause = DocumentTestUtils.getAsDocument(document, "$group"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java new file mode 100644 index 0000000000..e580e8689d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2022 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 static org.assertj.core.api.Assertions.*; + +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +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.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +/** + * @author Christoph Strobl + */ +class SelectionOperatorUnitTests { + + @Test // GH-4139 + void bottomRenderedCorrectly() { + + Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(document).isEqualTo(Document.parse(""" + { + $bottom: + { + output: [ "$playerId", "$score" ], + sortBy: { "score": -1 } + } + } + """)); + } + + @Test // GH-4139 + void bottomMapsFieldNamesCorrectly() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext( + Player.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))); + + Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).toDocument(aggregationContext); + + assertThat(document).isEqualTo(Document.parse(""" + { + $bottom: + { + output: [ "$player_id", "$s_cor_e" ], + sortBy: { "s_cor_e": -1 } + } + } + """)); + } + + static class Player { + + @Field("player_id") String playerId; + + @Field("s_cor_e") Integer score; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 79c4935008..baddc4ff66 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1174,6 +1174,11 @@ void shouldRenderRand() { assertThat(transform("rand()")).isEqualTo("{ $rand : {} }"); } + @Test // GH-4139 + void shouldRenderBottom() { + assertThat(transform("bottom(new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } diff --git a/src/main/asciidoc/reference/aggregation-framework.adoc b/src/main/asciidoc/reference/aggregation-framework.adoc index eece93524d..f3a87228e2 100644 --- a/src/main/asciidoc/reference/aggregation-framework.adoc +++ b/src/main/asciidoc/reference/aggregation-framework.adoc @@ -120,6 +120,9 @@ At the time of this writing, we provide support for the following Aggregation Op | Script Aggregation Operators | `function`, `accumulator` +| Selection Aggregation Operators +| `bottom` + |=== +++*+++ The operation is mapped or added by Spring Data MongoDB. From 10aedad36ab246a76e0af79225e2eea07c180eee Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 10:55:19 +0200 Subject: [PATCH 03/21] Add support for $bottomN aggregation operator --- .../core/aggregation/SelectionOperators.java | 32 ++++++++++++++++++- .../core/spel/MethodReferenceNode.java | 7 ++-- .../SelectionOperatorUnitTests.java | 18 +++++++++++ .../SpelExpressionTransformerUnitTests.java | 5 +++ .../reference/aggregation-framework.adoc | 5 +-- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index 96ab86a004..25cb84fa74 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -38,9 +38,14 @@ private Bottom(Object value) { super(value); } + /** + * In case a limit value ({@literal n}) is present {@literal $bottomN} is used instead of {@literal $bottom}. + * + * @return + */ @Override protected String getMongoMethod() { - return "$bottom"; + return get("n") == null ? "$bottom" : "$bottomN"; } /** @@ -50,6 +55,31 @@ public static Bottom bottom() { return new Bottom(Collections.emptyMap()); } + /** + * Limits the number of returned elements to the given value. + * + * @param numberOfResults + * @return new instance of {@link Bottom}. + */ + public Bottom limit(int numberOfResults) { + return limit((Object) numberOfResults); + } + + /** + * Limits the number of returned elements to the value defined by the given {@link AggregationExpression + * expression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Bottom limit(AggregationExpression expression) { + return limit((Object) expression); + } + + private Bottom limit(Object value) { + return new Bottom(append("n", value)); + } + /** * Define result ordering. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index bd2586e85a..acfa253714 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -212,6 +212,10 @@ public class MethodReferenceNode extends ExpressionNode { // OBJECT OPERATORS map.put("objectToArray", singleArgRef().forOperator("$objectToArray")); map.put("mergeObjects", arrayArgRef().forOperator("$mergeObjects")); + map.put("bottom", mapArgRef().forOperator("$bottom") // + .mappingParametersTo("output", "sortBy")); + map.put("bottomN", mapArgRef().forOperator("$bottomN") // + .mappingParametersTo("n", "output", "sortBy")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // @@ -226,9 +230,6 @@ public class MethodReferenceNode extends ExpressionNode { map.put("toString", singleArgRef().forOperator("$toString")); map.put("degreesToRadians", singleArgRef().forOperator("$degreesToRadians")); - // SELECT OPERATORS - map.put("bottom", mapArgRef().forOperator("$bottom") // - .mappingParametersTo("output", "sortBy")); FUNCTIONS = Collections.unmodifiableMap(map); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java index e580e8689d..b571d9c8f8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -71,6 +71,24 @@ void bottomMapsFieldNamesCorrectly() { """)); } + @Test // GH-4139 + void bottomNRenderedCorrectly() { + + Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).limit(3).toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(document).isEqualTo(Document.parse(""" + { + $bottomN: + { + n : 3, + output: [ "$playerId", "$score" ], + sortBy: { "score": -1 } + } + } + """)); + } + static class Player { @Field("player_id") String playerId; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index baddc4ff66..94f4ee39a2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1179,6 +1179,11 @@ void shouldRenderBottom() { assertThat(transform("bottom(new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); } + @Test // GH-4139 + void shouldRenderBottomN() { + assertThat(transform("bottomN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottomN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } diff --git a/src/main/asciidoc/reference/aggregation-framework.adoc b/src/main/asciidoc/reference/aggregation-framework.adoc index f3a87228e2..67545d39cf 100644 --- a/src/main/asciidoc/reference/aggregation-framework.adoc +++ b/src/main/asciidoc/reference/aggregation-framework.adoc @@ -82,7 +82,7 @@ At the time of this writing, we provide support for the following Aggregation Op | `setEquals`, `setIntersection`, `setUnion`, `setDifference`, `setIsSubset`, `anyElementTrue`, `allElementsTrue` | Group/Accumulator Aggregation Operators -| `addToSet`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `last`, `max`, `min`, `avg`, `push`, `sum`, `count` (+++*+++), `stdDevPop`, `stdDevSamp` +| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `last`, `max`, `min`, `avg`, `push`, `sum`, `count` (+++*+++), `stdDevPop`, `stdDevSamp` | Arithmetic Aggregation Operators | `abs`, `acos`, `acosh`, `add` (+++*+++ via `plus`), `asin`, `asin`, `atan`, `atan2`, `atanh`, `ceil`, `cos`, `cosh`, `derivative`, `divide`, `exp`, `floor`, `integral`, `ln`, `log`, `log10`, `mod`, `multiply`, `pow`, `round`, `sqrt`, `subtract` (+++*+++ via `minus`), `sin`, `sinh`, `tan`, `tanh`, `trunc` @@ -120,9 +120,6 @@ At the time of this writing, we provide support for the following Aggregation Op | Script Aggregation Operators | `function`, `accumulator` -| Selection Aggregation Operators -| `bottom` - |=== +++*+++ The operation is mapped or added by Spring Data MongoDB. From 6994b861e3fb9903233729039a81e10b136bdfd3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 11:46:57 +0200 Subject: [PATCH 04/21] Add support for $firstN aggregation operator --- .../core/aggregation/SelectionOperators.java | 94 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 3 +- .../SelectionOperatorUnitTests.java | 21 +++++ .../SpelExpressionTransformerUnitTests.java | 5 + 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index 25cb84fa74..38e0e5f724 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -122,4 +122,98 @@ public Bottom output(AggregationExpression... out) { return new Bottom(append("output", Arrays.asList(out))); } } + + /** + * {@link AbstractAggregationExpression} to return the {@literal $firstN} elements. + */ + public static class First extends AbstractAggregationExpression { + + protected First(Object value) { + super(value); + } + + /** + * @return new instance of {@link First}. + */ + public static First first() { + return new First(Collections.emptyMap()).limit(1); + } + + /** + * @return new instance of {@link First}. + */ + public static First first(int numberOfResults) { + return new First(Collections.emptyMap()).limit(numberOfResults); + } + + /** + * Limits the number of returned elements to the given value. + * + * @param numberOfResults + * @return new instance of {@link Bottom}. + */ + public First limit(int numberOfResults) { + return limit((Object) numberOfResults); + } + + /** + * Limits the number of returned elements to the value defined by the given {@link AggregationExpression + * expression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public First limit(AggregationExpression expression) { + return limit((Object) expression); + } + + private First limit(Object value) { + return new First(append("n", value)); + } + + /** + * Define the field to serve as source. + * + * @param fieldName must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public First of(String fieldName) { + return input(fieldName); + } + + /** + * Define the expression building the value to serve as source. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public First of(AggregationExpression expression) { + return input(expression); + } + + /** + * Define the field to serve as source. + * + * @param fieldName must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public First input(String fieldName) { + return new First(append("input", Fields.field(fieldName))); + } + + /** + * Define the expression building the value to serve as source. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public First input(AggregationExpression expression) { + return new First(append("input", expression)); + } + + @Override + protected String getMongoMethod() { + return "$firstN"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index acfa253714..b8b376110b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -216,6 +216,8 @@ public class MethodReferenceNode extends ExpressionNode { .mappingParametersTo("output", "sortBy")); map.put("bottomN", mapArgRef().forOperator("$bottomN") // .mappingParametersTo("n", "output", "sortBy")); + map.put("firstN", mapArgRef().forOperator("$firstN") // + .mappingParametersTo("n", "input")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // @@ -230,7 +232,6 @@ public class MethodReferenceNode extends ExpressionNode { map.put("toString", singleArgRef().forOperator("$toString")); map.put("degreesToRadians", singleArgRef().forOperator("$degreesToRadians")); - FUNCTIONS = Collections.unmodifiableMap(map); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java index b571d9c8f8..cc460ecedb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -89,6 +89,27 @@ void bottomNRenderedCorrectly() { """)); } + @Test // GH-4139 + void firstNMapsFieldNamesCorrectly() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext( + Player.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))); + + Document document = SelectionOperators.First.first(3).of("score").toDocument(aggregationContext); + + assertThat(document).isEqualTo(Document.parse(""" + { + $firstN: + { + n: 3, + input: "$s_cor_e" + } + } + """)); + } + static class Player { @Field("player_id") String playerId; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 94f4ee39a2..1c1e91d3a9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1184,6 +1184,11 @@ void shouldRenderBottomN() { assertThat(transform("bottomN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottomN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); } + @Test // GH-4139 + void shouldRenderFirstN() { + assertThat(transform("firstN(3, \"$score\")")).isEqualTo("{ $firstN : { n : 3, input : \"$score\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From 563811bbf46061f9306a939ce9a9ed05c2f12459 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 12:02:04 +0200 Subject: [PATCH 05/21] Add support for $lastN aggregation operator --- .../core/aggregation/SelectionOperators.java | 94 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 2 + .../SelectionOperatorUnitTests.java | 21 +++++ .../SpelExpressionTransformerUnitTests.java | 5 + 4 files changed, 122 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index 38e0e5f724..ac1a041592 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -216,4 +216,98 @@ protected String getMongoMethod() { return "$firstN"; } } + + /** + * {@link AbstractAggregationExpression} to return the {@literal $lastN} elements. + */ + public static class Last extends AbstractAggregationExpression { + + protected Last(Object value) { + super(value); + } + + /** + * @return new instance of {@link Last}. + */ + public static Last last() { + return new Last(Collections.emptyMap()).limit(1); + } + + /** + * @return new instance of {@link Last}. + */ + public static Last last(int numberOfResults) { + return new Last(Collections.emptyMap()).limit(numberOfResults); + } + + /** + * Limits the number of returned elements to the given value. + * + * @param numberOfResults + * @return new instance of {@link Bottom}. + */ + public Last limit(int numberOfResults) { + return limit((Object) numberOfResults); + } + + /** + * Limits the number of returned elements to the value defined by the given {@link AggregationExpression + * expression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Last limit(AggregationExpression expression) { + return limit((Object) expression); + } + + private Last limit(Object value) { + return new Last(append("n", value)); + } + + /** + * Define the field to serve as source. + * + * @param fieldName must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Last of(String fieldName) { + return input(fieldName); + } + + /** + * Define the expression building the value to serve as source. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Last of(AggregationExpression expression) { + return input(expression); + } + + /** + * Define the field to serve as source. + * + * @param fieldName must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Last input(String fieldName) { + return new Last(append("input", Fields.field(fieldName))); + } + + /** + * Define the expression building the value to serve as source. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Bottom}. + */ + public Last input(AggregationExpression expression) { + return new Last(append("input", expression)); + } + + @Override + protected String getMongoMethod() { + return "$lastN"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index b8b376110b..62f1a606c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -218,6 +218,8 @@ public class MethodReferenceNode extends ExpressionNode { .mappingParametersTo("n", "output", "sortBy")); map.put("firstN", mapArgRef().forOperator("$firstN") // .mappingParametersTo("n", "input")); + map.put("lastN", mapArgRef().forOperator("$lastN") // + .mappingParametersTo("n", "input")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java index cc460ecedb..044b0adab3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -110,6 +110,27 @@ void firstNMapsFieldNamesCorrectly() { """)); } + @Test // GH-4139 + void lastNMapsFieldNamesCorrectly() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext( + Player.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))); + + Document document = SelectionOperators.Last.last(3).of("score").toDocument(aggregationContext); + + assertThat(document).isEqualTo(Document.parse(""" + { + $lastN: + { + n: 3, + input: "$s_cor_e" + } + } + """)); + } + static class Player { @Field("player_id") String playerId; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 1c1e91d3a9..b343d68ea1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1189,6 +1189,11 @@ void shouldRenderFirstN() { assertThat(transform("firstN(3, \"$score\")")).isEqualTo("{ $firstN : { n : 3, input : \"$score\" }}"); } + @Test // GH-4139 + void shouldRenderLastN() { + assertThat(transform("lastN(3, \"$score\")")).isEqualTo("{ $lastN : { n : 3, input : \"$score\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From 332d4b991a7c66ad391498be086ed56a60282526 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 13:01:23 +0200 Subject: [PATCH 06/21] Add support for $top & $topN aggregation operators --- .../core/aggregation/SelectionOperators.java | 103 ++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 4 + .../SelectionOperatorUnitTests.java | 40 +++++++ .../SpelExpressionTransformerUnitTests.java | 10 ++ 4 files changed, 157 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index ac1a041592..d0c63093cf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -123,6 +123,109 @@ public Bottom output(AggregationExpression... out) { } } + /** + * {@link AbstractAggregationExpression} to return the top element according to the specified {@link #sortBy(Sort) + * order}. + */ + public static class Top extends AbstractAggregationExpression { + + private Top(Object value) { + super(value); + } + + /** + * In case a limit value ({@literal n}) is present {@literal $topN} is used instead of {@literal $top}. + * + * @return + */ + @Override + protected String getMongoMethod() { + return get("n") == null ? "$top" : "$topN"; + } + + /** + * @return new instance of {@link Top}. + */ + public static Top top() { + return new Top(Collections.emptyMap()); + } + + /** + * @param numberOfResults Limits the number of returned elements to the given value. + * @return new instance of {@link Top}. + */ + public static Top top(int numberOfResults) { + return top().limit(numberOfResults); + } + + /** + * Limits the number of returned elements to the given value. + * + * @param numberOfResults + * @return new instance of {@link Top}. + */ + public Top limit(int numberOfResults) { + return limit((Object) numberOfResults); + } + + /** + * Limits the number of returned elements to the value defined by the given {@link AggregationExpression + * expression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Top}. + */ + public Top limit(AggregationExpression expression) { + return limit((Object) expression); + } + + private Top limit(Object value) { + return new Top(append("n", value)); + } + + /** + * Define result ordering. + * + * @param sort must not be {@literal null}. + * @return new instance of {@link Top}. + */ + public Top sortBy(Sort sort) { + return new Top(append("sortBy", sort)); + } + + /** + * Define result ordering. + * + * @param out must not be {@literal null}. + * @return new instance of {@link Top}. + */ + public Top output(Fields out) { + return new Top(append("output", out)); + } + + /** + * Define fields included in the output for each element. + * + * @param fieldNames must not be {@literal null}. + * @return new instance of {@link Top}. + * @see #output(Fields) + */ + public Top output(String... fieldNames) { + return output(Fields.fields(fieldNames)); + } + + /** + * Define expressions building the value included in the output for each element. + * + * @param out must not be {@literal null}. + * @return new instance of {@link Top}. + * @see #output(Fields) + */ + public Top output(AggregationExpression... out) { + return new Top(append("output", Arrays.asList(out))); + } + } + /** * {@link AbstractAggregationExpression} to return the {@literal $firstN} elements. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 62f1a606c6..2f99aafb59 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -220,6 +220,10 @@ public class MethodReferenceNode extends ExpressionNode { .mappingParametersTo("n", "input")); map.put("lastN", mapArgRef().forOperator("$lastN") // .mappingParametersTo("n", "input")); + map.put("top", mapArgRef().forOperator("$top") // + .mappingParametersTo("output", "sortBy")); + map.put("topN", mapArgRef().forOperator("$topN") // + .mappingParametersTo("n", "output", "sortBy")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java index 044b0adab3..287cebc455 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -89,6 +89,46 @@ void bottomNRenderedCorrectly() { """)); } + @Test // GH-4139 + void topMapsFieldNamesCorrectly() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext( + Player.class, mappingContext, + new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext))); + + Document document = SelectionOperators.Top.top().output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).toDocument(aggregationContext); + + assertThat(document).isEqualTo(Document.parse(""" + { + $top: + { + output: [ "$player_id", "$s_cor_e" ], + sortBy: { "s_cor_e": -1 } + } + } + """)); + } + + @Test // GH-4139 + void topNRenderedCorrectly() { + + Document document = SelectionOperators.Top.top().output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).limit(3).toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(document).isEqualTo(Document.parse(""" + { + $topN: + { + n : 3, + output: [ "$playerId", "$score" ], + sortBy: { "score": -1 } + } + } + """)); + } + @Test // GH-4139 void firstNMapsFieldNamesCorrectly() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index b343d68ea1..1c2c4b5725 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1184,6 +1184,16 @@ void shouldRenderBottomN() { assertThat(transform("bottomN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottomN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); } + @Test // GH-4139 + void shouldRenderTop() { + assertThat(transform("top(new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $top : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); + } + + @Test // GH-4139 + void shouldRenderTopN() { + assertThat(transform("topN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $topN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"); + } + @Test // GH-4139 void shouldRenderFirstN() { assertThat(transform("firstN(3, \"$score\")")).isEqualTo("{ $firstN : { n : 3, input : \"$score\" }}"); From 5e677eb4d958991fbe6150167a2a882337eb953e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 22 Sep 2022 13:03:42 +0200 Subject: [PATCH 07/21] Polishing. sync method signatures. --- .../core/aggregation/SelectionOperators.java | 16 ++++++++++++---- .../aggregation/SelectionOperatorUnitTests.java | 8 ++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index d0c63093cf..c170011841 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -55,6 +55,14 @@ public static Bottom bottom() { return new Bottom(Collections.emptyMap()); } + /** + * @param numberOfResults Limits the number of returned elements to the given value. + * @return new instance of {@link Bottom}. + */ + public static Bottom bottom(int numberOfResults) { + return bottom().limit(numberOfResults); + } + /** * Limits the number of returned elements to the given value. * @@ -239,14 +247,14 @@ protected First(Object value) { * @return new instance of {@link First}. */ public static First first() { - return new First(Collections.emptyMap()).limit(1); + return new First(Collections.emptyMap()); } /** * @return new instance of {@link First}. */ public static First first(int numberOfResults) { - return new First(Collections.emptyMap()).limit(numberOfResults); + return first().limit(numberOfResults); } /** @@ -333,14 +341,14 @@ protected Last(Object value) { * @return new instance of {@link Last}. */ public static Last last() { - return new Last(Collections.emptyMap()).limit(1); + return new Last(Collections.emptyMap()); } /** * @return new instance of {@link Last}. */ public static Last last(int numberOfResults) { - return new Last(Collections.emptyMap()).limit(numberOfResults); + return last().limit(numberOfResults); } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java index 287cebc455..508963cc74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java @@ -74,8 +74,8 @@ void bottomMapsFieldNamesCorrectly() { @Test // GH-4139 void bottomNRenderedCorrectly() { - Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score")) - .sortBy(Sort.by(Direction.DESC, "score")).limit(3).toDocument(Aggregation.DEFAULT_CONTEXT); + Document document = SelectionOperators.Bottom.bottom(3).output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).toDocument(Aggregation.DEFAULT_CONTEXT); assertThat(document).isEqualTo(Document.parse(""" { @@ -114,8 +114,8 @@ void topMapsFieldNamesCorrectly() { @Test // GH-4139 void topNRenderedCorrectly() { - Document document = SelectionOperators.Top.top().output(Fields.fields("playerId", "score")) - .sortBy(Sort.by(Direction.DESC, "score")).limit(3).toDocument(Aggregation.DEFAULT_CONTEXT); + Document document = SelectionOperators.Top.top(3).output(Fields.fields("playerId", "score")) + .sortBy(Sort.by(Direction.DESC, "score")).toDocument(Aggregation.DEFAULT_CONTEXT); assertThat(document).isEqualTo(Document.parse(""" { From ae182183f905b11b9eae63846810624ed94de8a3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 09:41:16 +0200 Subject: [PATCH 08/21] Add support for $maxN aggregation operator --- .../AbstractAggregationExpression.java | 42 +++++++++++++++++-- .../aggregation/AccumulatorOperators.java | 40 +++++++++++++++--- .../core/spel/MethodReferenceNode.java | 2 + .../AccumulatorOperatorsUnitTests.java | 14 +++++++ .../SpelExpressionTransformerUnitTests.java | 5 +++ 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index f5795bd164..f84e427bd4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -76,11 +77,11 @@ private Object unpack(Object value, AggregationOperationContext context) { return context.getReference(field).toString(); } - if(value instanceof Fields fields) { + if (value instanceof Fields fields) { return fields.asList().stream().map(it -> unpack(it, context)).collect(Collectors.toList()); } - if(value instanceof Sort sort) { + if (value instanceof Sort sort) { Document sortDoc = new Document(); for (Order order : sort) { @@ -154,9 +155,40 @@ protected Map append(String key, Object value) { Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map"); - Map clone = new LinkedHashMap<>((java.util.Map) this.value); + return append((Map) this.value, key, value); + } + + private Map append(Map existing, String key, Object value) { + + Map clone = new LinkedHashMap<>(existing); clone.put(key, value); return clone; + } + + protected Map appendTo(String key, Object value) { + + Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map"); + + if (this.value instanceof Map map) { + + Map target = new HashMap<>(map); + if (!target.containsKey(key)) { + target.put(key, value); + return target; + } + target.computeIfPresent(key, (k, v) -> { + + if (v instanceof List list) { + List targetList = new ArrayList<>(list); + targetList.add(value); + return targetList; + } + return Arrays.asList(v, value); + }); + return target; + } + throw new IllegalStateException( + String.format("Cannot append value to %s type", ObjectUtils.nullSafeClassName(this.value))); } @@ -247,6 +279,10 @@ protected T get(Object key) { return (T) ((Map) this.value).get(key); } + protected boolean isArgumentMap() { + return this.value instanceof Map; + } + /** * Get the argument map. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java index 51fa0459fd..042ff90326 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java @@ -112,6 +112,17 @@ public Max max() { return usesFieldRef() ? Max.maxOf(fieldReference) : Max.maxOf(expression); } + /** + * Creates new {@link AggregationExpression} that takes the associated numeric value expression and returns the + * requested number of maximum values. + * + * @return new instance of {@link Max}. + * @since 4.0 + */ + public Max max(int numberOfResults) { + return max().limit(numberOfResults); + } + /** * Creates new {@link AggregationExpression} that takes the associated numeric value expression and returns the * minimum value. @@ -441,7 +452,7 @@ private Max(Object value) { @Override protected String getMongoMethod() { - return "$max"; + return contains("n") ? "$maxN" : "$max"; } /** @@ -453,7 +464,7 @@ protected String getMongoMethod() { public static Max maxOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); - return new Max(asFields(fieldReference)); + return new Max(Collections.singletonMap("input", Fields.field(fieldReference))); } /** @@ -465,7 +476,7 @@ public static Max maxOf(String fieldReference) { public static Max maxOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return new Max(Collections.singletonList(expression)); + return new Max(Collections.singletonMap("input", expression)); } /** @@ -478,7 +489,7 @@ public static Max maxOf(AggregationExpression expression) { public Max and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); - return new Max(append(Fields.field(fieldReference))); + return new Max(appendTo("input", Fields.field(fieldReference))); } /** @@ -491,7 +502,26 @@ public Max and(String fieldReference) { public Max and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return new Max(append(expression)); + return new Max(appendTo("input", expression)); + } + + /** + * Creates new {@link Max} that returns the given number of maxmimum values ({@literal $maxN}). + * NOTE: Cannot be used with more than one {@literal input} value. + * + * @param numberOfResults + * @return new instance of {@link Max}. + */ + public Max limit(int numberOfResults) { + return new Max(append("n", numberOfResults)); + } + + @Override + public Document toDocument(AggregationOperationContext context) { + if (get("n") == null) { + return toDocument(get("input"), context); + } + return super.toDocument(context); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 2f99aafb59..1f33d18cde 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -224,6 +224,8 @@ public class MethodReferenceNode extends ExpressionNode { .mappingParametersTo("output", "sortBy")); map.put("topN", mapArgRef().forOperator("$topN") // .mappingParametersTo("n", "output", "sortBy")); + map.put("maxN", mapArgRef().forOperator("$maxN") // + .mappingParametersTo("n", "input")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java index 32a772950b..871f60db48 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java @@ -80,6 +80,20 @@ void rendersExpMovingAvgWithAlpha() { .isEqualTo(Document.parse("{ $expMovingAvg: { input: \"$price\", alpha: 0.75 } }")); } + @Test // GH-4139 + void rendersMax() { + + assertThat(valueOf("price").max().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $max: \"$price\" }")); + } + + @Test // GH-4139 + void rendersMaxN() { + + assertThat(valueOf("price").max(3).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $maxN: { n: 3, input : \"$price\" } }")); + } + static class Jedi { String name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 1c2c4b5725..5474616977 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1204,6 +1204,11 @@ void shouldRenderLastN() { assertThat(transform("lastN(3, \"$score\")")).isEqualTo("{ $lastN : { n : 3, input : \"$score\" }}"); } + @Test // GH-4139 + void shouldRenderMaxN() { + assertThat(transform("maxN(3, \"$score\")")).isEqualTo("{ $maxN : { n : 3, input : \"$score\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From 17ecc406cd4e11d062733f5018955bf2d9db5286 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 09:50:12 +0200 Subject: [PATCH 09/21] Add support for $minN aggregation operator --- .../aggregation/AccumulatorOperators.java | 41 ++++++++++++++++--- .../core/spel/MethodReferenceNode.java | 2 + .../AccumulatorOperatorsUnitTests.java | 14 +++++++ .../SpelExpressionTransformerUnitTests.java | 5 +++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java index 042ff90326..1a47b77590 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java @@ -133,6 +133,17 @@ public Min min() { return usesFieldRef() ? Min.minOf(fieldReference) : Min.minOf(expression); } + /** + * Creates new {@link AggregationExpression} that takes the associated numeric value expression and returns the + * requested number of maximum values. + * + * @return new instance of {@link Max}. + * @since 4.0 + */ + public Min min(int numberOfResults) { + return min().limit(numberOfResults); + } + /** * Creates new {@link AggregationExpression} that takes the associated numeric value expression and calculates the * population standard deviation of the input values. @@ -551,7 +562,7 @@ private Min(Object value) { @Override protected String getMongoMethod() { - return "$min"; + return contains("n") ? "$minN" : "$min"; } /** @@ -563,7 +574,7 @@ protected String getMongoMethod() { public static Min minOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); - return new Min(asFields(fieldReference)); + return new Min(Collections.singletonMap("input", Fields.field(fieldReference))); } /** @@ -575,7 +586,7 @@ public static Min minOf(String fieldReference) { public static Min minOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return new Min(Collections.singletonList(expression)); + return new Min(Collections.singletonMap("input", expression)); } /** @@ -588,7 +599,7 @@ public static Min minOf(AggregationExpression expression) { public Min and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); - return new Min(append(Fields.field(fieldReference))); + return new Min(appendTo("input", Fields.field(fieldReference))); } /** @@ -601,7 +612,27 @@ public Min and(String fieldReference) { public Min and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return new Min(append(expression)); + return new Min(appendTo("input", expression)); + } + + /** + * Creates new {@link Min} that returns the given number of minimum values ({@literal $minN}). + * NOTE: Cannot be used with more than one {@literal input} value. + * + * @param numberOfResults + * @return new instance of {@link Min}. + */ + public Min limit(int numberOfResults) { + return new Min(append("n", numberOfResults)); + } + + @Override + public Document toDocument(AggregationOperationContext context) { + + if (get("n") == null) { + return toDocument(get("input"), context); + } + return super.toDocument(context); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 1f33d18cde..2925e87e09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -226,6 +226,8 @@ public class MethodReferenceNode extends ExpressionNode { .mappingParametersTo("n", "output", "sortBy")); map.put("maxN", mapArgRef().forOperator("$maxN") // .mappingParametersTo("n", "input")); + map.put("minN", mapArgRef().forOperator("$minN") // + .mappingParametersTo("n", "input")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java index 871f60db48..ea0710fcbe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java @@ -94,6 +94,20 @@ void rendersMaxN() { .isEqualTo(Document.parse("{ $maxN: { n: 3, input : \"$price\" } }")); } + @Test // GH-4139 + void rendersMin() { + + assertThat(valueOf("price").min().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $min: \"$price\" }")); + } + + @Test // GH-4139 + void rendersMinN() { + + assertThat(valueOf("price").min(3).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $minN: { n: 3, input : \"$price\" } }")); + } + static class Jedi { String name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 5474616977..06f31b1c1a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1209,6 +1209,11 @@ void shouldRenderMaxN() { assertThat(transform("maxN(3, \"$score\")")).isEqualTo("{ $maxN : { n : 3, input : \"$score\" }}"); } + @Test // GH-4139 + void shouldRenderMinN() { + assertThat(transform("minN(3, \"$score\")")).isEqualTo("{ $minN : { n : 3, input : \"$score\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From 3d2c66d3d40a5e0eb5e970b43f3951f120726c5d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 10:14:25 +0200 Subject: [PATCH 10/21] Add support for $dateSubtract aggregation operator --- .../core/aggregation/DateOperators.java | 186 ++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 2 + .../aggregation/DateOperatorsUnitTests.java | 15 ++ .../SpelExpressionTransformerUnitTests.java | 6 + 4 files changed, 209 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index b39c9725f4..41968b441f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -449,6 +449,95 @@ public DateAdd add(Object value, TemporalUnit unit) { timezone); } + /** + * Creates new {@link AggregationExpression} that subtracts the value of the given {@link AggregationExpression + * expression} (in {@literal units}). + * + * @param expression must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtractValueOf(AggregationExpression expression, String unit) { + return applyTimezone(DateSubtract.subtractValueOf(expression, unit).fromDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that subtracts the value of the given {@link AggregationExpression + * expression} (in {@literal units}). + * + * @param expression must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtractValueOf(AggregationExpression expression, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + return applyTimezone( + DateSubtract.subtractValueOf(expression, unit.name().toLowerCase(Locale.ROOT)).fromDate(dateReference()), + timezone); + } + + /** + * Creates new {@link AggregationExpression} that subtracts the value stored at the given {@literal field} (in + * {@literal units}). + * + * @param fieldReference must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtractValueOf(String fieldReference, String unit) { + return applyTimezone(DateSubtract.subtractValueOf(fieldReference, unit).fromDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that subtracts the value stored at the given {@literal field} (in + * {@literal units}). + * + * @param fieldReference must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtractValueOf(String fieldReference, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone( + DateSubtract.subtractValueOf(fieldReference, unit.name().toLowerCase(Locale.ROOT)).fromDate(dateReference()), + timezone); + } + + /** + * Creates new {@link AggregationExpression} that subtracts the given value (in {@literal units}). + * + * @param value must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtract(Object value, String unit) { + return applyTimezone(DateSubtract.subtractValue(value, unit).fromDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that subtracts the given value (in {@literal units}). + * + * @param value must not be {@literal null}. + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + * @since 4.0 + */ + public DateSubtract subtract(Object value, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone( + DateSubtract.subtractValue(value, unit.name().toLowerCase(Locale.ROOT)).fromDate(dateReference()), timezone); + } + /** * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and * 366. @@ -2733,6 +2822,103 @@ protected String getMongoMethod() { } } + /** + * {@link AggregationExpression} for {@code $dateSubtract}.
+ * NOTE: Requires MongoDB 5.0 or later. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class DateSubtract extends TimezonedDateAggregationExpression { + + private DateSubtract(Object value) { + super(value); + } + + /** + * Subtract the number of {@literal units} of the result of the given {@link AggregationExpression expression} from + * a {@link #fromDate(Object) start date}. + * + * @param expression must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public static DateSubtract subtractValueOf(AggregationExpression expression, String unit) { + return subtractValue(expression, unit); + } + + /** + * Subtract the number of {@literal units} from a {@literal field} from a {@link #fromDate(Object) start date}. + * + * @param fieldReference must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public static DateSubtract subtractValueOf(String fieldReference, String unit) { + return subtractValue(Fields.field(fieldReference), unit); + } + + /** + * Subtract the number of {@literal units} from a {@link #fromDate(Object) start date}. + * + * @param value must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public static DateSubtract subtractValue(Object value, String unit) { + + Map args = new HashMap<>(); + args.put("unit", unit); + args.put("amount", value); + return new DateSubtract(args); + } + + /** + * Define the start date, in UTC, for the subtraction operation. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public DateSubtract fromDateOf(AggregationExpression expression) { + return fromDate(expression); + } + + /** + * Define the start date, in UTC, for the subtraction operation. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public DateSubtract fromDateOf(String fieldReference) { + return fromDate(Fields.field(fieldReference)); + } + + /** + * Define the start date, in UTC, for the subtraction operation. + * + * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. + * @return new instance of {@link DateSubtract}. + */ + public DateSubtract fromDate(Object dateExpression) { + return new DateSubtract(append("startDate", dateExpression)); + } + + /** + * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used. + * + * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. + * @return new instance of {@link DateSubtract}. + */ + public DateSubtract withTimezone(Timezone timezone) { + return new DateSubtract(appendTimezone(argumentMap(), timezone)); + } + + @Override + protected String getMongoMethod() { + return "$dateSubtract"; + } + } + /** * {@link AggregationExpression} for {@code $dateDiff}.
* NOTE: Requires MongoDB 5.0 or later. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 2925e87e09..60f9fba00e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -161,6 +161,8 @@ public class MethodReferenceNode extends ExpressionNode { // DATE OPERATORS map.put("dateAdd", mapArgRef().forOperator("$dateAdd").mappingParametersTo("startDate", "unit", "amount", "timezone")); + map.put("dateSubtract", + mapArgRef().forOperator("$dateSubtract").mappingParametersTo("startDate", "unit", "amount", "timezone")); map.put("dateDiff", mapArgRef().forOperator("$dateDiff").mappingParametersTo("startDate", "endDate", "unit", "timezone", "startOfWeek")); map.put("dayOfYear", singleArgRef().forOperator("$dayOfYear")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java index c9e73b5a9d..4f4c2f36e3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -49,6 +49,21 @@ void rendersDateAddWithTimezone() { "{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3, timezone : \"America/Chicago\" } }"); } + @Test // GH-4139 + void rendersDateSubtract() { + + assertThat(DateOperators.dateOf("purchaseDate").subtract(3, "day").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $dateSubtract: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }"); + } + + @Test // GH-4139 + void rendersDateSubtractWithTimezone() { + + assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).subtract(3, "day") + .toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo( + "{ $dateSubtract: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3, timezone : \"America/Chicago\" } }"); + } + @Test // GH-3713 void rendersDateDiff() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 06f31b1c1a..3c0456f63c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1163,6 +1163,12 @@ void shouldRenderDateAdd() { .isEqualTo("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }"); } + @Test // GH-4139 + void shouldRenderDateSubtract() { + assertThat(transform("dateSubtract(purchaseDate, 'day', 3)")) + .isEqualTo("{ $dateSubtract: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }"); + } + @Test // GH-3713 void shouldRenderDateDiff() { assertThat(transform("dateDiff(purchaseDate, delivered, 'day')")) From 0726c4557a70cbb337cd82645f2f4b47341f5801 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 11:31:46 +0200 Subject: [PATCH 11/21] Add support for $dateTrunc aggregation operator --- .../core/aggregation/DateOperators.java | 155 ++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 2 + .../aggregation/DateOperatorsUnitTests.java | 16 ++ .../SpelExpressionTransformerUnitTests.java | 5 + 4 files changed, 178 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index 41968b441f..0a0ac44ba0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -538,6 +538,32 @@ public DateSubtract subtract(Object value, TemporalUnit unit) { DateSubtract.subtractValue(value, unit.name().toLowerCase(Locale.ROOT)).fromDate(dateReference()), timezone); } + /** + * Creates new {@link AggregationExpression} that truncates a date to the given {@literal unit}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + * @since 4.0 + */ + public DateTrunc truncate(String unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + return applyTimezone(DateTrunc.truncateValue(dateReference()).to(unit), timezone); + } + + /** + * Creates new {@link AggregationExpression} that truncates a date to the given {@literal unit}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + * @since 4.0 + */ + public DateTrunc truncate(TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + return truncate(unit.name().toLowerCase(Locale.ROOT)); + } + /** * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and * 366. @@ -3027,6 +3053,135 @@ protected String getMongoMethod() { } } + /** + * {@link AggregationExpression} for {@code $dateTrunc}.
+ * NOTE: Requires MongoDB 5.0 or later. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class DateTrunc extends TimezonedDateAggregationExpression { + + private DateTrunc(Object value) { + super(value); + } + + /** + * Truncates the date value of computed by the given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public static DateTrunc truncateValueOf(AggregationExpression expression) { + return truncateValue(expression); + } + + /** + * Truncates the date value of the referenced {@literal field}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public static DateTrunc truncateValueOf(String fieldReference) { + return truncateValue(Fields.field(fieldReference)); + } + + /** + * Truncates the date value. + * + * @param value must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public static DateTrunc truncateValue(Object value) { + return new DateTrunc(Collections.singletonMap("date", value)); + } + + /** + * Define the unit of time. + * + * @param unit must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc to(String unit) { + return new DateTrunc(append("unit", unit)); + } + + /** + * Define the unit of time via an {@link AggregationExpression}. + * + * @param unit must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc to(AggregationExpression unit) { + return new DateTrunc(append("unit", unit)); + } + + /** + * Define the weeks starting day if {@link #to(String)} resolves to {@literal week}. + * + * @param day must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc startOfWeek(java.time.DayOfWeek day) { + return startOfWeek(day.name().toLowerCase(Locale.US)); + } + + /** + * Define the weeks starting day if {@link #to(String)} resolves to {@literal week}. + * + * @param day must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc startOfWeek(String day) { + return new DateTrunc(append("startOfWeek", day)); + } + + /** + * Define the numeric time value. + * + * @param binSize must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc binSize(int binSize) { + return binSize((Object) binSize); + } + + /** + * Define the numeric time value via an {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc binSize(AggregationExpression expression) { + return binSize((Object) expression); + } + + /** + * Define the numeric time value. + * + * @param binSize must not be {@literal null}. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc binSize(Object binSize) { + return new DateTrunc(append("binSize", binSize)); + } + + /** + * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used. + * + * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. + * @return new instance of {@link DateTrunc}. + */ + public DateTrunc withTimezone(Timezone timezone) { + return new DateTrunc(appendTimezone(argumentMap(), timezone)); + } + + @Override + protected String getMongoMethod() { + return "$dateTrunc"; + } + } + /** * Interface defining a temporal unit for date operators. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 60f9fba00e..23e558a1cf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -165,6 +165,8 @@ public class MethodReferenceNode extends ExpressionNode { mapArgRef().forOperator("$dateSubtract").mappingParametersTo("startDate", "unit", "amount", "timezone")); map.put("dateDiff", mapArgRef().forOperator("$dateDiff").mappingParametersTo("startDate", "endDate", "unit", "timezone", "startOfWeek")); + map.put("dateTrunc", + mapArgRef().forOperator("$dateTrunc").mappingParametersTo("date", "unit", "binSize", "startOfWeek", "timezone")); map.put("dayOfYear", singleArgRef().forOperator("$dayOfYear")); map.put("dayOfMonth", singleArgRef().forOperator("$dayOfMonth")); map.put("dayOfWeek", singleArgRef().forOperator("$dayOfWeek")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java index 4f4c2f36e3..f0830acd1c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -17,6 +17,7 @@ import static org.springframework.data.mongodb.test.util.Assertions.*; +import java.time.DayOfWeek; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.aggregation.DateOperators.TemporalUnit; import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone; /** @@ -102,4 +104,18 @@ void rendersTimezoneFromTimeZoneId() { void rendersTimezoneFromZoneId() { assertThat(DateOperators.Timezone.fromZone(ZoneId.of("America/Chicago")).getValue()).isEqualTo("America/Chicago"); } + + @Test // GH-4139 + void rendersDateTrunc() { + + assertThat(DateOperators.dateOf("purchaseDate").truncate("week").binSize(2).startOfWeek(DayOfWeek.MONDAY).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $dateTrunc: { date: \"$purchaseDate\", unit: \"week\", binSize: 2, startOfWeek : \"monday\" } }"); + } + + @Test // GH-4139 + void rendersDateTruncWithTimezone() { + + assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).truncate("week").binSize(2).startOfWeek(DayOfWeek.MONDAY).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $dateTrunc: { date: \"$purchaseDate\", unit: \"week\", binSize: 2, startOfWeek : \"monday\", timezone : \"America/Chicago\" } }"); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 3c0456f63c..b80963828c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1220,6 +1220,11 @@ void shouldRenderMinN() { assertThat(transform("minN(3, \"$score\")")).isEqualTo("{ $minN : { n : 3, input : \"$score\" }}"); } + @Test // GH-4139 + void shouldRenderDateTrunc() { + assertThat(transform("dateTrunc(purchaseDate, \"week\", 2, \"monday\")")).isEqualTo("{ $dateTrunc : { date : \"$purchaseDate\", unit : \"week\", binSize : 2, startOfWeek : \"monday\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From c3ec450efdcab620c3127292f6a4e73a7582ce30 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 13:53:44 +0200 Subject: [PATCH 12/21] Add support for $getField aggregation operator --- .../core/aggregation/ObjectOperators.java | 49 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 19 +++---- .../aggregation/ObjectOperatorsUnitTests.java | 7 +++ .../SpelExpressionTransformerUnitTests.java | 5 ++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java index 996e82a987..976d734392 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import org.bson.Document; import org.springframework.util.Assert; @@ -124,6 +125,16 @@ public MergeObjects mergeWithValuesOf(AggregationExpression... expression) { public ObjectToArray toArray() { return ObjectToArray.toArray(value); } + + /** + * Creates new {@link GetField aggregation expression} that takes the associated value and obtains the value of the + * field with matching name. + * + * @since 4.0 + */ + public GetField getField(String fieldName) { + return GetField.getField(fieldName).from(value); + } } /** @@ -283,4 +294,42 @@ protected String getMongoMethod() { return "$objectToArray"; } } + + /** + * {@link AggregationExpression} for {@code $getField}. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class GetField extends AbstractAggregationExpression { + + protected GetField(Object value) { + super(value); + } + + public static GetField getField(String fieldName) { + return new GetField(Collections.singletonMap("field", fieldName)); + } + + public static GetField getField(Field field) { + return getField(field.getTarget()); + } + + public GetField from(String fieldRef) { + return from(Fields.field(fieldRef)); + } + + public GetField from(AggregationExpression expression) { + return from((Object) expression); + } + + private GetField from(Object fieldRef) { + return new GetField(append("input", fieldRef)); + } + + @Override + protected String getMongoMethod() { + return "$getField"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 23e558a1cf..32e846966f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -165,8 +165,8 @@ public class MethodReferenceNode extends ExpressionNode { mapArgRef().forOperator("$dateSubtract").mappingParametersTo("startDate", "unit", "amount", "timezone")); map.put("dateDiff", mapArgRef().forOperator("$dateDiff").mappingParametersTo("startDate", "endDate", "unit", "timezone", "startOfWeek")); - map.put("dateTrunc", - mapArgRef().forOperator("$dateTrunc").mappingParametersTo("date", "unit", "binSize", "startOfWeek", "timezone")); + map.put("dateTrunc", mapArgRef().forOperator("$dateTrunc").mappingParametersTo("date", "unit", "binSize", + "startOfWeek", "timezone")); map.put("dayOfYear", singleArgRef().forOperator("$dayOfYear")); map.put("dayOfMonth", singleArgRef().forOperator("$dayOfMonth")); map.put("dayOfWeek", singleArgRef().forOperator("$dayOfWeek")); @@ -209,13 +209,6 @@ public class MethodReferenceNode extends ExpressionNode { map.put("stdDevSamp", arrayArgRef().forOperator("$stdDevSamp")); map.put("covariancePop", arrayArgRef().forOperator("$covariancePop")); map.put("covarianceSamp", arrayArgRef().forOperator("$covarianceSamp")); - - // TYPE OPERATORS - map.put("type", singleArgRef().forOperator("$type")); - - // OBJECT OPERATORS - map.put("objectToArray", singleArgRef().forOperator("$objectToArray")); - map.put("mergeObjects", arrayArgRef().forOperator("$mergeObjects")); map.put("bottom", mapArgRef().forOperator("$bottom") // .mappingParametersTo("output", "sortBy")); map.put("bottomN", mapArgRef().forOperator("$bottomN") // @@ -233,6 +226,14 @@ public class MethodReferenceNode extends ExpressionNode { map.put("minN", mapArgRef().forOperator("$minN") // .mappingParametersTo("n", "input")); + // TYPE OPERATORS + map.put("type", singleArgRef().forOperator("$type")); + + // OBJECT OPERATORS + map.put("objectToArray", singleArgRef().forOperator("$objectToArray")); + map.put("mergeObjects", arrayArgRef().forOperator("$mergeObjects")); + map.put("getField", mapArgRef().forOperator("$getField").mappingParametersTo("field", "input")); + // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // .mappingParametersTo("input", "to", "onError", "onNull")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java index f8f94f8889..1854a94fcc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java @@ -102,4 +102,11 @@ public void toArrayWithExpression() { .isEqualTo(Document.parse("{ $objectToArray : " + EXPRESSION_STRING + " }")); } + @Test // GH-4139 + public void getField() { + + assertThat(ObjectOperators.valueOf("batman").getField("robin").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $getField : { field : \"robin\", input : \"$batman\" }}")); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index b80963828c..5f54bc5d19 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1225,6 +1225,11 @@ void shouldRenderDateTrunc() { assertThat(transform("dateTrunc(purchaseDate, \"week\", 2, \"monday\")")).isEqualTo("{ $dateTrunc : { date : \"$purchaseDate\", unit : \"week\", binSize : 2, startOfWeek : \"monday\" }}"); } + @Test // GH-4139 + void shouldRenderGetField() { + assertThat(transform("getField(\"score\", source)")).isEqualTo("{ $getField : { field : \"score\", input : \"$source\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From f29dc44d2c10711e45a543080540188250c0c963 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 14:09:41 +0200 Subject: [PATCH 13/21] Add support for $setField aggregation operator --- .../core/aggregation/ObjectOperators.java | 72 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 1 + .../aggregation/ObjectOperatorsUnitTests.java | 14 ++++ .../SpelExpressionTransformerUnitTests.java | 5 ++ 4 files changed, 92 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java index 976d734392..b574d4a2a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -18,8 +18,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Set; import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.Aggregation.SystemVariable; import org.springframework.util.Assert; /** @@ -135,6 +137,26 @@ public ObjectToArray toArray() { public GetField getField(String fieldName) { return GetField.getField(fieldName).from(value); } + + /** + * Creates new {@link SetField aggregation expression} that takes the associated value and obtains the value of the + * field with matching name. + * + * @since 4.0 + */ + public SetField setField(String fieldName) { + return SetField.setField(fieldName).of(value); + } + + /** + * Creates new {@link SetField aggregation expression} that takes the associated value and obtains the value of the + * field with matching name. + * + * @since 4.0 + */ + public AggregationExpression removeField(String fieldName) { + return SetField.setField(fieldName).of(value).toValue(SystemVariable.REMOVE); + } } /** @@ -332,4 +354,54 @@ protected String getMongoMethod() { return "$getField"; } } + + /** + * {@link AggregationExpression} for {@code $setField}. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class SetField extends AbstractAggregationExpression { + + protected SetField(Object value) { + super(value); + } + + public static SetField setField(String fieldName) { + return new SetField(Collections.singletonMap("field", fieldName)); + } + + public static SetField setField(Field field) { + return setField(field.getTarget()); + } + + public SetField of(String fieldRef) { + return of(Fields.field(fieldRef)); + } + + public SetField of(AggregationExpression expression) { + return of((Object) expression); + } + + private SetField of(Object fieldRef) { + return new SetField(append("input", fieldRef)); + } + + public SetField toValueOf(String fieldReference) { + return toValue(Fields.field(fieldReference)); + } + + public SetField toValueOf(AggregationExpression expression) { + return toValue(expression); + } + + public SetField toValue(Object value) { + return new SetField(append("value", value)); + } + + @Override + protected String getMongoMethod() { + return "$setField"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 32e846966f..b1641f5f64 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -233,6 +233,7 @@ public class MethodReferenceNode extends ExpressionNode { map.put("objectToArray", singleArgRef().forOperator("$objectToArray")); map.put("mergeObjects", arrayArgRef().forOperator("$mergeObjects")); map.put("getField", mapArgRef().forOperator("$getField").mappingParametersTo("field", "input")); + map.put("setField", mapArgRef().forOperator("$setField").mappingParametersTo("field", "value", "input")); // CONVERT OPERATORS map.put("convert", mapArgRef().forOperator("$convert") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java index 1854a94fcc..3ee7b08a4a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java @@ -109,4 +109,18 @@ public void getField() { .isEqualTo(Document.parse("{ $getField : { field : \"robin\", input : \"$batman\" }}")); } + @Test // GH-4139 + public void setField() { + + assertThat(ObjectOperators.valueOf("batman").setField("friend").toValue("robin").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $setField : { field : \"friend\", value : \"robin\", input : \"$batman\" }}")); + } + + @Test // GH-4139 + public void removeField() { + + assertThat(ObjectOperators.valueOf("batman").removeField("joker").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $setField : { field : \"joker\", value : \"$$REMOVE\", input : \"$batman\" }}")); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 5f54bc5d19..e5dca5f363 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1230,6 +1230,11 @@ void shouldRenderGetField() { assertThat(transform("getField(\"score\", source)")).isEqualTo("{ $getField : { field : \"score\", input : \"$source\" }}"); } + @Test // GH-4139 + void shouldRenderSetField() { + assertThat(transform("setField(\"score\", 100, source)")).isEqualTo("{ $setField : { field : \"score\", value : 100, input : \"$source\" }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From a790e3b13baaf4af18980257a9c3b3d6cf7dcc60 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Sep 2022 14:33:41 +0200 Subject: [PATCH 14/21] Fix errors after rebase --- .../data/mongodb/core/aggregation/ObjectOperators.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java index b574d4a2a6..adf81ba9eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -18,10 +18,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Set; import org.bson.Document; -import org.springframework.data.mongodb.core.aggregation.Aggregation.SystemVariable; import org.springframework.util.Assert; /** From 400db28cdd9ae3596829c71e584b663748db8236 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 09:22:59 +0200 Subject: [PATCH 15/21] Add support for $sortArray aggregation operator --- .../core/aggregation/ArrayOperators.java | 78 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 1 + .../aggregation/ArrayOperatorsUnitTests.java | 8 ++ .../SpelExpressionTransformerUnitTests.java | 6 ++ 4 files changed, 93 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index 4babbf6eb7..1ea699852f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; @@ -315,6 +316,21 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression. .withInitialValue(initialValue).reduce(expressions); } + /** + * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given {@link Sort order}. + * + * @return new instance of {@link SortArray}. + * @since 4.0 + */ + public SortArray sort(Sort sort) { + + if (usesFieldRef()) { + return SortArray.sortArrayOf(fieldReference).by(sort); + } + + return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(sort); + } + /** * Creates new {@link AggregationExpression} that transposes an array of input arrays so that the first element of * the output array would be an array containing, the first element of the first input array, the first element of @@ -1915,4 +1931,66 @@ protected String getMongoMethod() { return "$last"; } } + + /** + * {@link AggregationExpression} for {@code $sortArray} that sorts elements in an array.
+ * + * @author Christoph Strobl + * @since 4.0 + */ + public static class SortArray extends AbstractAggregationExpression { + + private SortArray(Object value) { + super(value); + } + + /** + * Returns the given array. + * + * @param array must not be {@literal null}. + * @return new instance of {@link SortArray}. + */ + public static SortArray sortArray(Object array) { + return new SortArray(Collections.singletonMap("input", array)); + } + + /** + * Sorts the elements in the array pointed to by the given {@link Field field reference}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link SortArray}. + */ + public static SortArray sortArrayOf(String fieldReference) { + return sortArray(Fields.field(fieldReference)); + } + + /** + * Sorts the elements of the array computed buy the given {@link AggregationExpression expression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link SortArray}. + */ + public static SortArray sortArrayOf(AggregationExpression expression) { + return sortArray(expression); + } + + /** + * Set the order to put elements in. + * + * @param sort must not be {@literal null}. + * @return new instance of {@link SortArray}. + */ + public SortArray by(Sort sort) { + return new SortArray(append("sortBy", sort)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() + */ + @Override + protected String getMongoMethod() { + return "$sortArray"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index b1641f5f64..4347adba15 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -142,6 +142,7 @@ public class MethodReferenceNode extends ExpressionNode { map.put("last", singleArgRef().forOperator("$last")); map.put("size", singleArgRef().forOperator("$size")); map.put("slice", arrayArgRef().forOperator("$slice")); + map.put("sortArray", mapArgRef().forOperator("$sortArray").mappingParametersTo("input", "sortBy")); map.put("reverseArray", singleArgRef().forOperator("$reverseArray")); map.put("reduce", mapArgRef().forOperator("$reduce").mappingParametersTo("input", "initialValue", "in")); map.put("zip", mapArgRef().forOperator("$zip").mappingParametersTo("inputs", "useLongestLength", "defaults")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java index 63165c7641..a542de5bea 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject; /** @@ -171,4 +172,11 @@ public void lastWithFieldReference() { assertThat(ArrayOperators.arrayOf("field").last().toDocument(Aggregation.DEFAULT_CONTEXT)) .isEqualTo("{ $last : \"$field\" }"); } + + @Test // GH-4139 + void sortByWithFieldRef() { + + assertThat(ArrayOperators.arrayOf("team").sort(Sort.by("name")).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $sortArray: { input: \"$team\", sortBy: { name: 1 } } }"); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index e5dca5f363..ebf944020b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1235,6 +1235,12 @@ void shouldRenderSetField() { assertThat(transform("setField(\"score\", 100, source)")).isEqualTo("{ $setField : { field : \"score\", value : 100, input : \"$source\" }}"); } + @Test // GH-4139 + void shouldRenderSortArray() { + assertThat(transform( + "sortArray(team, new org.bson.Document(\"name\" , 1))")).isEqualTo("{ $sortArray : { input : \"$team\", sortBy : {\"name\" : 1 } }}"); + } + private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); } From 692bb76f8257b8f34c2aaba82249d398ab8974b6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 09:44:02 +0200 Subject: [PATCH 16/21] Add support for $tsIncrement aggregation operator --- .../core/aggregation/DateOperators.java | 72 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 1 + .../aggregation/DateOperatorsUnitTests.java | 16 ++++- .../SpelExpressionTransformerUnitTests.java | 5 ++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index 0a0ac44ba0..e380802fc2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -818,6 +818,21 @@ public DateFromString fromString() { return applyTimezone(DateFromString.fromString(dateReference()), timezone); } + /** + * Creates new {@link AggregationExpression} that returns the incrementing ordinal from a timestamp. + * + * @return new instance of {@link TsIncrement}. + * @since 4.0 + */ + public TsIncrement tsIncrement() { + + if(timezone != null && !Timezone.none().equals(timezone)) { + throw new IllegalArgumentException("$tsIncrement does not support timezones"); + } + + return TsIncrement.tsIncrement(dateReference()); + } + private Object dateReference() { if (usesFieldRef()) { @@ -3182,6 +3197,63 @@ protected String getMongoMethod() { } } + /** + * {@link AggregationExpression} for {@code $tsIncrement}. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class TsIncrement extends AbstractAggregationExpression { + + private TsIncrement(Object value) { + super(value); + } + + /** + * Creates new {@link TsIncrement} that returns the incrementing ordinal from a timestamp. + * + * @param value must not be {@literal null}. + * @return new instance of {@link TsIncrement}. + * @throws IllegalArgumentException if given {@literal value} is {@literal null}. + */ + public static TsIncrement tsIncrement(Object value) { + + Assert.notNull(value, "Value must not be null"); + return new TsIncrement(value); + } + + /** + * Creates new {@link TsIncrement} that returns the incrementing ordinal from a timestamp. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link TsIncrement}. + * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. + */ + public static TsIncrement tsIncrementValueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + return tsIncrement(Fields.field(fieldReference)); + } + + /** + * Creates new {@link TsIncrement}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link TsIncrement}. + * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. + */ + public static TsIncrement tsIncrementValueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null"); + return tsIncrement(expression); + } + + @Override + protected String getMongoMethod() { + return "$tsIncrement"; + } + } + /** * Interface defining a temporal unit for date operators. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 4347adba15..0a22d7f1d1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -191,6 +191,7 @@ public class MethodReferenceNode extends ExpressionNode { map.put("isoDayOfWeek", singleArgRef().forOperator("$isoDayOfWeek")); map.put("isoWeek", singleArgRef().forOperator("$isoWeek")); map.put("isoWeekYear", singleArgRef().forOperator("$isoWeekYear")); + map.put("tsIncrement", singleArgRef().forOperator("$tsIncrement")); // CONDITIONAL OPERATORS map.put("cond", mapArgRef().forOperator("$cond") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java index f0830acd1c..313086718f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -24,8 +24,6 @@ import java.util.TimeZone; import org.junit.jupiter.api.Test; - -import org.springframework.data.mongodb.core.aggregation.DateOperators.TemporalUnit; import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone; /** @@ -118,4 +116,18 @@ void rendersDateTruncWithTimezone() { assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).truncate("week").binSize(2).startOfWeek(DayOfWeek.MONDAY).toDocument(Aggregation.DEFAULT_CONTEXT)) .isEqualTo("{ $dateTrunc: { date: \"$purchaseDate\", unit: \"week\", binSize: 2, startOfWeek : \"monday\", timezone : \"America/Chicago\" } }"); } + + @Test // GH-4139 + void rendersTsIncrement() { + + assertThat(DateOperators.dateOf("saleTimestamp").tsIncrement().toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo( + "{ $tsIncrement: \"$saleTimestamp\" }"); + } + + @Test // GH-4139 + void tsIncrementErrorsOnTimezone() { + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).tsIncrement()); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index ebf944020b..06f2715e88 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1240,6 +1240,11 @@ void shouldRenderSortArray() { assertThat(transform( "sortArray(team, new org.bson.Document(\"name\" , 1))")).isEqualTo("{ $sortArray : { input : \"$team\", sortBy : {\"name\" : 1 } }}"); } + + @Test // GH-4139 + void shouldTsIncrement() { + assertThat(transform("tsIncrement(saleTimestamp)")).isEqualTo("{ $tsIncrement: \"$saleTimestamp\" }"); + } private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); From ccac82bc0dae4698510505ab60e21239f4cb9c90 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 09:48:47 +0200 Subject: [PATCH 17/21] Add support for $tsSecond aggregation operator --- .../core/aggregation/DateOperators.java | 72 +++++++++++++++++++ .../core/spel/MethodReferenceNode.java | 1 + .../aggregation/DateOperatorsUnitTests.java | 14 ++++ .../SpelExpressionTransformerUnitTests.java | 5 ++ 4 files changed, 92 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index e380802fc2..c3644432f9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -833,6 +833,21 @@ public TsIncrement tsIncrement() { return TsIncrement.tsIncrement(dateReference()); } + /** + * Creates new {@link AggregationExpression} that returns the seconds from a timestamp. + * + * @return new instance of {@link TsIncrement}. + * @since 4.0 + */ + public TsSecond tsSecond() { + + if(timezone != null && !Timezone.none().equals(timezone)) { + throw new IllegalArgumentException("$tsSecond does not support timezones"); + } + + return TsSecond.tsSecond(dateReference()); + } + private Object dateReference() { if (usesFieldRef()) { @@ -3254,6 +3269,63 @@ protected String getMongoMethod() { } } + /** + * {@link AggregationExpression} for {@code $tsSecond}. + * + * @author Christoph Strobl + * @since 4.0 + */ + public static class TsSecond extends AbstractAggregationExpression { + + private TsSecond(Object value) { + super(value); + } + + /** + * Creates new {@link TsSecond} that returns the incrementing ordinal from a timestamp. + * + * @param value must not be {@literal null}. + * @return new instance of {@link TsSecond}. + * @throws IllegalArgumentException if given {@literal value} is {@literal null}. + */ + public static TsSecond tsSecond(Object value) { + + Assert.notNull(value, "Value must not be null"); + return new TsSecond(value); + } + + /** + * Creates new {@link TsSecond} that returns the incrementing ordinal from a timestamp. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link TsSecond}. + * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. + */ + public static TsSecond tsSecondValueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + return tsSecond(Fields.field(fieldReference)); + } + + /** + * Creates new {@link TsSecond}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link TsSecond}. + * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. + */ + public static TsSecond tsSecondValueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null"); + return tsSecond(expression); + } + + @Override + protected String getMongoMethod() { + return "$tsSecond"; + } + } + /** * Interface defining a temporal unit for date operators. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 0a22d7f1d1..cc572ab5d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -192,6 +192,7 @@ public class MethodReferenceNode extends ExpressionNode { map.put("isoWeek", singleArgRef().forOperator("$isoWeek")); map.put("isoWeekYear", singleArgRef().forOperator("$isoWeekYear")); map.put("tsIncrement", singleArgRef().forOperator("$tsIncrement")); + map.put("tsSecond", singleArgRef().forOperator("$tsSecond")); // CONDITIONAL OPERATORS map.put("cond", mapArgRef().forOperator("$cond") // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java index 313086718f..da5b354b23 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -130,4 +130,18 @@ void tsIncrementErrorsOnTimezone() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).tsIncrement()); } + + @Test // GH-4139 + void rendersTsSecond() { + + assertThat(DateOperators.dateOf("saleTimestamp").tsSecond().toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo( + "{ $tsSecond: \"$saleTimestamp\" }"); + } + + @Test // GH-4139 + void tsSecondErrorsOnTimezone() { + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).tsSecond()); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 06f2715e88..94b9cf9356 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1245,6 +1245,11 @@ void shouldRenderSortArray() { void shouldTsIncrement() { assertThat(transform("tsIncrement(saleTimestamp)")).isEqualTo("{ $tsIncrement: \"$saleTimestamp\" }"); } + + @Test // GH-4139 + void shouldTsSecond() { + assertThat(transform("tsSecond(saleTimestamp)")).isEqualTo("{ $tsSecond: \"$saleTimestamp\" }"); + } private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); From 02337a9553a0984eb59a702145d5538c081d3759 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 10:02:08 +0200 Subject: [PATCH 18/21] Add support for $locf aggregation operator --- .../core/aggregation/EvaluationOperators.java | 52 ++++++++++++++++++- .../core/spel/MethodReferenceNode.java | 3 ++ .../EvaluationOperatorsUnitTests.java | 8 +++ .../SpelExpressionTransformerUnitTests.java | 5 ++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java index 2fa1cf5451..e2eb49c827 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java @@ -16,7 +16,6 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; - import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -24,6 +23,7 @@ * Gateway to {@literal evaluation operators} such as {@literal $expr}. * * @author Divya Srivastava + * @author Christoph Strobl * @since 3.3 */ public class EvaluationOperators { @@ -86,6 +86,15 @@ public Expr expr() { return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression); } + /** + * Creates new {@link AggregationExpression} that is a valid aggregation expression. + * + * @return new instance of {@link Expr}. + */ + public LastObservationCarriedForward locf() { + return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference) : LastObservationCarriedForward.locfValueOf(expression); + } + /** * Allows the use of aggregation expressions within the query language. */ @@ -152,4 +161,45 @@ private boolean usesFieldRef() { } } + /** + * Sets {@literal null} and missing values to the last non-null value. + * + * @since 4.0 + */ + public static class LastObservationCarriedForward extends AbstractAggregationExpression { + + private LastObservationCarriedForward(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$locf"; + } + + /** + * Creates new {@link EvaluationOperatorFactory.Expr}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link EvaluationOperatorFactory.Expr}. + */ + public static LastObservationCarriedForward locfValueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + return new LastObservationCarriedForward(Fields.field(fieldReference)); + } + + /** + * Creates new {@link LastObservationCarriedForward}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link LastObservationCarriedForward}. + */ + public static LastObservationCarriedForward locfValueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null"); + return new LastObservationCarriedForward(expression); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index cc572ab5d2..ce6d6b88e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -251,6 +251,9 @@ public class MethodReferenceNode extends ExpressionNode { map.put("toString", singleArgRef().forOperator("$toString")); map.put("degreesToRadians", singleArgRef().forOperator("$degreesToRadians")); + // expression operators + map.put("locf", singleArgRef().forOperator("$locf")); + FUNCTIONS = Collections.unmodifiableMap(map); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperatorsUnitTests.java index f547ccaf55..352b45ab2a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperatorsUnitTests.java @@ -23,6 +23,7 @@ * Unit tests for {@link EvaluationOperators}. * * @author Mark Paluch + * @author Christoph Strobl */ class EvaluationOperatorsUnitTests { @@ -32,4 +33,11 @@ void shouldRenderExprCorrectly() { assertThat(EvaluationOperators.valueOf("foo").expr().toDocument(Aggregation.DEFAULT_CONTEXT)) .isEqualTo("{ $expr: \"$foo\" }"); } + + @Test // GH-4139 + void shouldRenderLocfCorrectly() { + + assertThat(EvaluationOperators.valueOf("foo").locf().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $locf: \"$foo\" }"); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 94b9cf9356..7efa19cf46 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1250,6 +1250,11 @@ void shouldTsIncrement() { void shouldTsSecond() { assertThat(transform("tsSecond(saleTimestamp)")).isEqualTo("{ $tsSecond: \"$saleTimestamp\" }"); } + + @Test // GH-4139 + void shouldRenderLocf() { + assertThat(transform("locf(price)")).isEqualTo("{ $locf: \"$price\" }"); + } private Document transform(String expression, Object... params) { return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); From df51f6b8bedea3a7fa566ac4555f1842f9e6ab76 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 10:04:43 +0200 Subject: [PATCH 19/21] Move Expr operator one level up The Expr operator should be held within ExpressionOperators not its factory. --- .../core/aggregation/EvaluationOperators.java | 121 +++++++++--------- .../core/convert/QueryMapperUnitTests.java | 2 +- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java index e2eb49c827..184dcb03dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java @@ -92,72 +92,73 @@ public Expr expr() { * @return new instance of {@link Expr}. */ public LastObservationCarriedForward locf() { - return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference) : LastObservationCarriedForward.locfValueOf(expression); + return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference) + : LastObservationCarriedForward.locfValueOf(expression); + } + + private boolean usesFieldRef() { + return fieldReference != null; + } + } + + /** + * Allows the use of aggregation expressions within the query language. + */ + public static class Expr extends AbstractAggregationExpression { + + private Expr(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$expr"; } /** - * Allows the use of aggregation expressions within the query language. + * Creates new {@link Expr}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link Expr}. */ - 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); - } - - /** - * Creates {@code $expr} as {@link CriteriaDefinition}. - * - * @return the {@link CriteriaDefinition} from this expression. - */ - public CriteriaDefinition toCriteriaDefinition(AggregationOperationContext context) { - - Document criteriaObject = toDocument(context); - - return new CriteriaDefinition() { - @Override - public Document getCriteriaObject() { - return criteriaObject; - } - - @Override - public String getKey() { - return getMongoMethod(); - } - }; - } + public static Expr valueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + return new Expr(Fields.field(fieldReference)); } - private boolean usesFieldRef() { - return fieldReference != null; + /** + * 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); + } + + /** + * Creates {@code $expr} as {@link CriteriaDefinition}. + * + * @return the {@link CriteriaDefinition} from this expression. + */ + public CriteriaDefinition toCriteriaDefinition(AggregationOperationContext context) { + + Document criteriaObject = toDocument(context); + + return new CriteriaDefinition() { + @Override + public Document getCriteriaObject() { + return criteriaObject; + } + + @Override + public String getKey() { + return getMongoMethod(); + } + }; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 06f6aec93b..c604dd9e18 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -1416,7 +1416,7 @@ void allowsUsingFieldPathsForPropertiesHavingCustomConversionRegistered() { @Test // GH-3790 void shouldAcceptExprAsCriteriaDefinition() { - EvaluationOperators.EvaluationOperatorFactory.Expr expr = EvaluationOperators + EvaluationOperators.Expr expr = EvaluationOperators .valueOf(ConditionalOperators.ifNull("customizedField").then(true)).expr(); Query query = query( From c62b9652cf2ea7064a3b31399ddeb5c7596a99d5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 14:42:24 +0200 Subject: [PATCH 20/21] Add support for $densify aggregation stage --- .../core/aggregation/DensifyOperation.java | 381 ++++++++++++++++++ .../DensifyOperationUnitTests.java | 146 +++++++ 2 files changed, 527 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java new file mode 100644 index 0000000000..253db727c9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java @@ -0,0 +1,381 @@ +/* + * Copyright 2022 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 java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/*** + * Encapsulates the aggregation framework {@code $densify}-operation. + * + * @author Christoph Strobl + * @since 4.0 + */ +public class DensifyOperation implements AggregationOperation { + + private @Nullable Field field; + private @Nullable List partitionBy; + private @Nullable Range range; + + protected DensifyOperation(@Nullable Field field, @Nullable List partitionBy, @Nullable Range range) { + + this.field = field; + this.partitionBy = partitionBy; + this.range = range; + } + + /** + * Obtain a builder to create the {@link DensifyOperation}. + * + * @return new instance of {@link DensifyOperationBuilder}. + */ + public static DensifyOperationBuilder builder() { + return new DensifyOperationBuilder(); + } + + @Override + public Document toDocument(AggregationOperationContext context) { + + Document densify = new Document(); + densify.put("field", context.getReference(field).getRaw()); + if (!ObjectUtils.isEmpty(partitionBy)) { + densify.put("partitionByFields", partitionBy.stream().map(it -> { + if (it instanceof Field field) { + return context.getReference(field).getRaw(); + } + if (it instanceof AggregationExpression expression) { + return expression.toDocument(context); + } + return it; + }).collect(Collectors.toList())); + } + densify.put("range", range.toDocument(context)); + return new Document("$densify", densify); + } + + /** + * The {@link Range} specifies how the data is densified. + */ + public interface Range { + + /** + * Add documents spanning the range of values within the given lower (inclusive) and upper (exclusive) bound. + * + * @param lower must not be {@literal null}. + * @param upper must not be {@literal null}. + * @return new instance of {@link DensifyRange}. + */ + static DensifyRange bounded(Object lower, Object upper) { + return new BoundedRange(lower, upper, DensifyUnits.NONE); + } + + /** + * Add documents spanning the full value range. + * + * @return new instance of {@link DensifyRange}. + */ + static DensifyRange full() { + + return new DensifyRange(DensifyUnits.NONE) { + + @Override + Object getBounds(AggregationOperationContext ctx) { + return "full"; + } + }; + } + + /** + * Add documents spanning the full value range for each partition. + * + * @return new instance of {@link DensifyRange}. + */ + static DensifyRange partition() { + return new DensifyRange(DensifyUnits.NONE) { + + @Override + Object getBounds(AggregationOperationContext ctx) { + return "partition"; + } + }; + } + + /** + * Obtain the document representation of the window in a default {@link AggregationOperationContext context}. + * + * @return never {@literal null}. + */ + default Document toDocument() { + return toDocument(Aggregation.DEFAULT_CONTEXT); + } + + /** + * Obtain the document representation of the window in the given {@link AggregationOperationContext context}. + * + * @return never {@literal null}. + */ + Document toDocument(AggregationOperationContext ctx); + } + + /** + * Base {@link Range} implementation. + * + * @author Christoph Strobl + */ + public static abstract class DensifyRange implements Range { + + private @Nullable DensifyUnit unit; + private Number step; + + public DensifyRange(DensifyUnit unit) { + this.unit = unit; + } + + @Override + public Document toDocument(AggregationOperationContext ctx) { + + Document range = new Document("step", step); + if (unit != null && !DensifyUnits.NONE.equals(unit)) { + range.put("unit", unit.name().toLowerCase(Locale.US)); + } + range.put("bounds", getBounds(ctx)); + return range; + } + + /** + * Set the increment for the value. + * + * @param step must not be {@literal null}. + * @return this. + */ + public DensifyRange incrementBy(Number step) { + this.step = step; + return this; + } + + /** + * Set the increment for the value. + * + * @param step must not be {@literal null}. + * @return this. + */ + public DensifyRange incrementBy(Number step, DensifyUnit unit) { + this.step = step; + return unit(unit); + } + + /** + * Set the {@link DensifyUnit unit} for the step field. + * + * @param unit + * @return this. + */ + public DensifyRange unit(DensifyUnit unit) { + + this.unit = unit; + return this; + } + + abstract Object getBounds(AggregationOperationContext ctx); + } + + /** + * {@link Range} implementation holding lower and upper bound values. + * + * @author Christoph Strobl + */ + public static class BoundedRange extends DensifyRange { + + private List bounds; + + protected BoundedRange(Object lower, Object upper, DensifyUnit unit) { + + super(unit); + this.bounds = Arrays.asList(lower, upper); + } + + @Override + List getBounds(AggregationOperationContext ctx) { + return bounds.stream().map(it -> { + if (it instanceof AggregationExpression expression) { + return expression.toDocument(ctx); + } + return it; + }).collect(Collectors.toList()); + } + } + + /** + * The actual time unit to apply to a {@link Range}. + */ + public interface DensifyUnit { + + String name(); + + /** + * Converts the given time unit into a {@link DensifyUnit}. Supported units are: days, hours, minutes, seconds, and + * milliseconds. + * + * @param timeUnit the time unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static DensifyUnit from(TimeUnit timeUnit) { + + Assert.notNull(timeUnit, "TimeUnit must not be null"); + + switch (timeUnit) { + case DAYS: + return DensifyUnits.DAY; + case HOURS: + return DensifyUnits.HOUR; + case MINUTES: + return DensifyUnits.MINUTE; + case SECONDS: + return DensifyUnits.SECOND; + case MILLISECONDS: + return DensifyUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create DensifyUnit from %s", timeUnit)); + } + + /** + * Converts the given chrono unit into a {@link DensifyUnit}. Supported units are: years, weeks, months, days, + * hours, minutes, seconds, and millis. + * + * @param chronoUnit the chrono unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static DensifyUnits from(ChronoUnit chronoUnit) { + + switch (chronoUnit) { + case YEARS: + return DensifyUnits.YEAR; + case WEEKS: + return DensifyUnits.WEEK; + case MONTHS: + return DensifyUnits.MONTH; + case DAYS: + return DensifyUnits.DAY; + case HOURS: + return DensifyUnits.HOUR; + case MINUTES: + return DensifyUnits.MINUTE; + case SECONDS: + return DensifyUnits.SECOND; + case MILLIS: + return DensifyUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create DensifyUnit from %s", chronoUnit)); + } + } + + /** + * Quick access to available {@link DensifyUnit units}. + */ + public enum DensifyUnits implements DensifyUnit { + NONE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND + } + + public static class DensifyOperationBuilder { + + DensifyOperation target; + + public DensifyOperationBuilder() { + this.target = new DensifyOperation(null, Collections.emptyList(), null); + } + + /** + * Set the field to densify. + * + * @param fieldname must not be {@literal null}. + * @return this. + */ + public DensifyOperationBuilder densify(String fieldname) { + this.target.field = Fields.field(fieldname); + return this; + } + + /** + * Set the fields used for grouping documents. + * + * @param fields must not be {@literal null}. + * @return this. + */ + public DensifyOperationBuilder partitionBy(String... fields) { + target.partitionBy = Fields.fields(fields).asList(); + return this; + } + + /** + * Set the operational range. + * + * @param range must not be {@literal null}. + * @return this. + */ + public DensifyOperationBuilder range(Range range) { + + target.range = range; + return this; + } + + /** + * Operate on full range. + * + * @param consumer + * @return this. + */ + public DensifyOperationBuilder fullRange(Consumer consumer) { + + DensifyRange range = Range.full(); + consumer.accept(range); + + return range(range); + } + + /** + * Operate on full range. + * + * @param consumer + * @return this. + */ + public DensifyOperationBuilder partitionRange(Consumer consumer) { + + DensifyRange range = Range.partition(); + consumer.accept(range); + + return range(range); + } + + DensifyOperation build() { + return new DensifyOperation(target.field, target.partitionBy, target.range); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java new file mode 100644 index 0000000000..60284074c1 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2022 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 static org.springframework.data.mongodb.test.util.Assertions.*; + +import java.util.Date; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.aggregation.DensifyOperation.DensifyUnits; +import org.springframework.data.mongodb.core.aggregation.DensifyOperation.Range; +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.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class DensifyOperationUnitTests { + + @Test // GH-4139 + void rendersFieldNamesAsIsForUntypedContext() { + + DensifyOperation densify = DensifyOperation.builder().densify("ts") + .range(Range.bounded("2021-05-18T00:00:00", "2021-05-18T08:00:00").incrementBy(1).unit(DensifyUnits.HOUR)).build(); + + assertThat(densify.toDocument(contextFor(null))).isEqualTo(""" + { + $densify: { + field: "ts", + range: { + step: 1, + unit: "hour", + bounds:[ "2021-05-18T00:00:00", "2021-05-18T08:00:00" ] + } + } + } + """); + } + + @Test // GH-4139 + void rendersFieldNamesCorrectly() { + + DensifyOperation densify = DensifyOperation.builder().densify("ts") + .range(Range.bounded("2021-05-18T00:00:00", "2021-05-18T08:00:00").incrementBy(1).unit(DensifyUnits.HOUR)).build(); + + assertThat(densify.toDocument(contextFor(Weather.class))).isEqualTo(""" + { + $densify: { + field: "timestamp", + range: { + step: 1, + unit: "hour", + bounds:[ "2021-05-18T00:00:00", "2021-05-18T08:00:00" ] + } + } + } + """); + } + + @Test // GH-4139 + void rendersPartitonNamesCorrectly() { + + DensifyOperation densify = DensifyOperation.builder().densify("alt").partitionBy("var") + .fullRange(range -> range.incrementBy(200)).build(); + + assertThat(densify.toDocument(contextFor(Coffee.class))).isEqualTo(""" + { + $densify: { + field: "altitude", + partitionByFields : [ "variety" ], + range: { + step: 200, + bounds: "full" + } + } + } + """); + } + + @Test // GH-4139 + void rendersPartitonRangeCorrectly() { + + DensifyOperation densify = DensifyOperation.builder().densify("alt").partitionBy("var") + .partitionRange(range -> range.incrementBy(200)).build(); + + assertThat(densify.toDocument(contextFor(Coffee.class))).isEqualTo(""" + { + $densify: { + field: "altitude", + partitionByFields : [ "variety" ], + range: { + step: 200, + bounds: "partition" + } + } + } + """); + } + + private static AggregationOperationContext contextFor(@Nullable Class type) { + + if (type == null) { + return Aggregation.DEFAULT_CONTEXT; + } + + MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, + new MongoMappingContext()); + mongoConverter.afterPropertiesSet(); + + return new TypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(), + new QueryMapper(mongoConverter)); + } + + class Weather { + + @Field("timestamp") Date ts; + + @Field("temp") Long temperature; + } + + class Coffee { + + @Field("altitude") Long alt; + + @Field("variety") String var; + + Float score; + } +} From 173055ac445dffc68f12b703205ee9043a5ab983 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 26 Sep 2022 14:47:54 +0200 Subject: [PATCH 21/21] Update aggregation reference documentation --- src/main/asciidoc/reference/aggregation-framework.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/asciidoc/reference/aggregation-framework.adoc b/src/main/asciidoc/reference/aggregation-framework.adoc index 67545d39cf..3e64e161c6 100644 --- a/src/main/asciidoc/reference/aggregation-framework.adoc +++ b/src/main/asciidoc/reference/aggregation-framework.adoc @@ -82,7 +82,7 @@ At the time of this writing, we provide support for the following Aggregation Op | `setEquals`, `setIntersection`, `setUnion`, `setDifference`, `setIsSubset`, `anyElementTrue`, `allElementsTrue` | Group/Accumulator Aggregation Operators -| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `last`, `max`, `min`, `avg`, `push`, `sum`, `count` (+++*+++), `stdDevPop`, `stdDevSamp` +| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `firstN`, `last`, `lastN` `max`, `maxN`, `min`, `minN`, `avg`, `push`, `sum`, `top`, `topN`, `count` (+++*+++), `stdDevPop`, `stdDevSamp` | Arithmetic Aggregation Operators | `abs`, `acos`, `acosh`, `add` (+++*+++ via `plus`), `asin`, `asin`, `atan`, `atan2`, `atanh`, `ceil`, `cos`, `cosh`, `derivative`, `divide`, `exp`, `floor`, `integral`, `ln`, `log`, `log10`, `mod`, `multiply`, `pow`, `round`, `sqrt`, `subtract` (+++*+++ via `minus`), `sin`, `sinh`, `tan`, `tanh`, `trunc` @@ -94,13 +94,13 @@ At the time of this writing, we provide support for the following Aggregation Op | `eq` (+++*+++ via `is`), `gt`, `gte`, `lt`, `lte`, `ne` | Array Aggregation Operators -| `arrayElementAt`, `arrayToObject`, `concatArrays`, `filter`, `first`, `in`, `indexOfArray`, `isArray`, `last`, range`, `reverseArray`, `reduce`, `size`, `slice`, `zip` +| `arrayElementAt`, `arrayToObject`, `concatArrays`, `filter`, `first`, `in`, `indexOfArray`, `isArray`, `last`, range`, `reverseArray`, `reduce`, `size`, `sortArray`, `slice`, `zip` | Literal Operators | `literal` | Date Aggregation Operators -| `dayOfYear`, `dayOfMonth`, `dayOfWeek`, `year`, `month`, `week`, `hour`, `minute`, `second`, `millisecond`, `dateAdd`, `dateDiff`, `dateToString`, `dateFromString`, `dateFromParts`, `dateToParts`, `isoDayOfWeek`, `isoWeek`, `isoWeekYear` +| `dateSubstract`, `dateTrunc`, `dayOfYear`, `dayOfMonth`, `dayOfWeek`, `year`, `month`, `week`, `hour`, `minute`, `second`, `millisecond`, `dateAdd`, `dateDiff`, `dateToString`, `dateFromString`, `dateFromParts`, `dateToParts`, `isoDayOfWeek`, `isoWeek`, `isoWeekYear`, `tsIncrement`, `tsSecond` | Variable Operators | `map` @@ -115,7 +115,7 @@ At the time of this writing, we provide support for the following Aggregation Op | `convert`, `degreesToRadians`, `toBool`, `toDate`, `toDecimal`, `toDouble`, `toInt`, `toLong`, `toObjectId`, `toString` | Object Aggregation Operators -| `objectToArray`, `mergeObjects` +| `objectToArray`, `mergeObjects`, `getField`, `setField` | Script Aggregation Operators | `function`, `accumulator`