Skip to content

Commit 052cfdf

Browse files
christophstroblmp911de
authored andcommitted
Add support for $bottom aggregation operator.
Closes #4139 Original pull request: #4182.
1 parent b31c21b commit 052cfdf

File tree

8 files changed

+234
-2
lines changed

8 files changed

+234
-2
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222
import java.util.LinkedHashMap;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.stream.Collectors;
2526

2627
import org.bson.Document;
28+
import org.springframework.data.domain.Sort;
29+
import org.springframework.data.domain.Sort.Order;
30+
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
2731
import org.springframework.util.Assert;
2832
import org.springframework.util.ObjectUtils;
2933

@@ -68,8 +72,24 @@ private Object unpack(Object value, AggregationOperationContext context) {
6872
return ((AggregationExpression) value).toDocument(context);
6973
}
7074

71-
if (value instanceof Field) {
72-
return context.getReference((Field) value).toString();
75+
if (value instanceof Field field) {
76+
return context.getReference(field).toString();
77+
}
78+
79+
if(value instanceof Fields fields) {
80+
return fields.asList().stream().map(it -> unpack(it, context)).collect(Collectors.toList());
81+
}
82+
83+
if(value instanceof Sort sort) {
84+
85+
Document sortDoc = new Document();
86+
for (Order order : sort) {
87+
88+
// Check reference
89+
FieldReference reference = context.getReference(order.getProperty());
90+
sortDoc.put(reference.getRaw(), order.isAscending() ? 1 : -1);
91+
}
92+
return sortDoc;
7393
}
7494

7595
if (value instanceof List) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,17 @@ public GroupOperationBuilder accumulate(Accumulator accumulator) {
387387
return new GroupOperationBuilder(this, new Operation(accumulator));
388388
}
389389

390+
/**
391+
* Adds a computed field to the {@link GroupOperation}.
392+
*
393+
* @param expression must not be {@literal null}.
394+
* @return never {@literal null}.
395+
* @since 4.0
396+
*/
397+
public GroupOperation and(String fieldName, AggregationExpression expression) {
398+
return new GroupOperationBuilder(this, new Operation(expression)).as(fieldName);
399+
}
400+
390401
private GroupOperationBuilder newBuilder(Keyword keyword, @Nullable String reference, @Nullable Object value) {
391402
return new GroupOperationBuilder(this, new Operation(keyword, null, reference, value));
392403
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
21+
import org.springframework.data.domain.Sort;
22+
23+
/**
24+
* Gateway to {@literal selection operators} such as {@literal $bottom}.
25+
*
26+
* @author Christoph Strobl
27+
* @since 4.0
28+
*/
29+
public class SelectionOperators {
30+
31+
/**
32+
* {@link AbstractAggregationExpression} to return the bottom element according to the specified {@link #sortBy(Sort)
33+
* order}.
34+
*/
35+
public static class Bottom extends AbstractAggregationExpression {
36+
37+
private Bottom(Object value) {
38+
super(value);
39+
}
40+
41+
@Override
42+
protected String getMongoMethod() {
43+
return "$bottom";
44+
}
45+
46+
/**
47+
* @return new instance of {@link Bottom}.
48+
*/
49+
public static Bottom bottom() {
50+
return new Bottom(Collections.emptyMap());
51+
}
52+
53+
/**
54+
* Define result ordering.
55+
*
56+
* @param sort must not be {@literal null}.
57+
* @return new instance of {@link Bottom}.
58+
*/
59+
public Bottom sortBy(Sort sort) {
60+
return new Bottom(append("sortBy", sort));
61+
}
62+
63+
/**
64+
* Define result ordering.
65+
*
66+
* @param out must not be {@literal null}.
67+
* @return new instance of {@link Bottom}.
68+
*/
69+
public Bottom output(Fields out) {
70+
return new Bottom(append("output", out));
71+
}
72+
73+
/**
74+
* Define fields included in the output for each element.
75+
*
76+
* @param fieldNames must not be {@literal null}.
77+
* @return new instance of {@link Bottom}.
78+
* @see #output(Fields)
79+
*/
80+
public Bottom output(String... fieldNames) {
81+
return output(Fields.fields(fieldNames));
82+
}
83+
84+
/**
85+
* Define expressions building the value included in the output for each element.
86+
*
87+
* @param out must not be {@literal null}.
88+
* @return new instance of {@link Bottom}.
89+
* @see #output(Fields)
90+
*/
91+
public Bottom output(AggregationExpression... out) {
92+
return new Bottom(append("output", Arrays.asList(out)));
93+
}
94+
}
95+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ public class MethodReferenceNode extends ExpressionNode {
226226
map.put("toString", singleArgRef().forOperator("$toString"));
227227
map.put("degreesToRadians", singleArgRef().forOperator("$degreesToRadians"));
228228

229+
// SELECT OPERATORS
230+
map.put("bottom", mapArgRef().forOperator("$bottom") //
231+
.mappingParametersTo("output", "sortBy"));
232+
229233
FUNCTIONS = Collections.unmodifiableMap(map);
230234
}
231235

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222

2323
import org.bson.Document;
2424
import org.junit.jupiter.api.Test;
25+
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.domain.Sort.Direction;
2527
import org.springframework.data.mongodb.core.DocumentTestUtils;
28+
import org.springframework.data.mongodb.core.aggregation.SelectionOperators.Bottom;
2629
import org.springframework.data.mongodb.core.query.Criteria;
2730

2831
/**
@@ -252,6 +255,17 @@ void accumulatorShouldBeAllowedOnGroupOperation() {
252255
assertThat(accumulatedValue).containsKey("$accumulator");
253256
}
254257

258+
@Test // GH-4139
259+
void groupOperationAllowsToAddFieldsComputedViaExpression() {
260+
261+
GroupOperation groupOperation = Aggregation.group("id").and("playerId",
262+
Bottom.bottom().output("playerId", "score").sortBy(Sort.by(Direction.DESC, "score")));
263+
Document groupClause = extractDocumentFromGroupOperation(groupOperation);
264+
265+
assertThat(groupClause).containsEntry("playerId",
266+
Document.parse("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}"));
267+
}
268+
255269
private Document extractDocumentFromGroupOperation(GroupOperation groupOperation) {
256270
Document document = groupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
257271
Document groupClause = DocumentTestUtils.getAsDocument(document, "$group");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.bson.Document;
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.data.domain.Sort;
23+
import org.springframework.data.domain.Sort.Direction;
24+
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
25+
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
26+
import org.springframework.data.mongodb.core.convert.QueryMapper;
27+
import org.springframework.data.mongodb.core.mapping.Field;
28+
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
29+
30+
/**
31+
* @author Christoph Strobl
32+
*/
33+
class SelectionOperatorUnitTests {
34+
35+
@Test // GH-4139
36+
void bottomRenderedCorrectly() {
37+
38+
Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score"))
39+
.sortBy(Sort.by(Direction.DESC, "score")).toDocument(Aggregation.DEFAULT_CONTEXT);
40+
41+
assertThat(document).isEqualTo(Document.parse("""
42+
{
43+
$bottom:
44+
{
45+
output: [ "$playerId", "$score" ],
46+
sortBy: { "score": -1 }
47+
}
48+
}
49+
"""));
50+
}
51+
52+
@Test // GH-4139
53+
void bottomMapsFieldNamesCorrectly() {
54+
55+
MongoMappingContext mappingContext = new MongoMappingContext();
56+
RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext(
57+
Player.class, mappingContext,
58+
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
59+
60+
Document document = SelectionOperators.Bottom.bottom().output(Fields.fields("playerId", "score"))
61+
.sortBy(Sort.by(Direction.DESC, "score")).toDocument(aggregationContext);
62+
63+
assertThat(document).isEqualTo(Document.parse("""
64+
{
65+
$bottom:
66+
{
67+
output: [ "$player_id", "$s_cor_e" ],
68+
sortBy: { "s_cor_e": -1 }
69+
}
70+
}
71+
"""));
72+
}
73+
74+
static class Player {
75+
76+
@Field("player_id") String playerId;
77+
78+
@Field("s_cor_e") Integer score;
79+
}
80+
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,11 @@ void shouldRenderRand() {
11741174
assertThat(transform("rand()")).isEqualTo("{ $rand : {} }");
11751175
}
11761176

1177+
@Test // GH-4139
1178+
void shouldRenderBottom() {
1179+
assertThat(transform("bottom(new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottom : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}");
1180+
}
1181+
11771182
private Document transform(String expression, Object... params) {
11781183
return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params);
11791184
}

src/main/asciidoc/reference/aggregation-framework.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ At the time of this writing, we provide support for the following Aggregation Op
120120
| Script Aggregation Operators
121121
| `function`, `accumulator`
122122

123+
| Selection Aggregation Operators
124+
| `bottom`
125+
123126
|===
124127

125128
+++*+++ The operation is mapped or added by Spring Data MongoDB.

0 commit comments

Comments
 (0)