Skip to content

Commit 72d82d3

Browse files
christophstroblmp911de
authored andcommitted
Add support for $top & $topN aggregation operators.
Closes #4139 Original pull request: #4182.
1 parent cdfe2a0 commit 72d82d3

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java

+103
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,109 @@ public Bottom output(AggregationExpression... out) {
123123
}
124124
}
125125

126+
/**
127+
* {@link AbstractAggregationExpression} to return the top element according to the specified {@link #sortBy(Sort)
128+
* order}.
129+
*/
130+
public static class Top extends AbstractAggregationExpression {
131+
132+
private Top(Object value) {
133+
super(value);
134+
}
135+
136+
/**
137+
* In case a limit value ({@literal n}) is present {@literal $topN} is used instead of {@literal $top}.
138+
*
139+
* @return
140+
*/
141+
@Override
142+
protected String getMongoMethod() {
143+
return get("n") == null ? "$top" : "$topN";
144+
}
145+
146+
/**
147+
* @return new instance of {@link Top}.
148+
*/
149+
public static Top top() {
150+
return new Top(Collections.emptyMap());
151+
}
152+
153+
/**
154+
* @param numberOfResults Limits the number of returned elements to the given value.
155+
* @return new instance of {@link Top}.
156+
*/
157+
public static Top top(int numberOfResults) {
158+
return top().limit(numberOfResults);
159+
}
160+
161+
/**
162+
* Limits the number of returned elements to the given value.
163+
*
164+
* @param numberOfResults
165+
* @return new instance of {@link Top}.
166+
*/
167+
public Top limit(int numberOfResults) {
168+
return limit((Object) numberOfResults);
169+
}
170+
171+
/**
172+
* Limits the number of returned elements to the value defined by the given {@link AggregationExpression
173+
* expression}.
174+
*
175+
* @param expression must not be {@literal null}.
176+
* @return new instance of {@link Top}.
177+
*/
178+
public Top limit(AggregationExpression expression) {
179+
return limit((Object) expression);
180+
}
181+
182+
private Top limit(Object value) {
183+
return new Top(append("n", value));
184+
}
185+
186+
/**
187+
* Define result ordering.
188+
*
189+
* @param sort must not be {@literal null}.
190+
* @return new instance of {@link Top}.
191+
*/
192+
public Top sortBy(Sort sort) {
193+
return new Top(append("sortBy", sort));
194+
}
195+
196+
/**
197+
* Define result ordering.
198+
*
199+
* @param out must not be {@literal null}.
200+
* @return new instance of {@link Top}.
201+
*/
202+
public Top output(Fields out) {
203+
return new Top(append("output", out));
204+
}
205+
206+
/**
207+
* Define fields included in the output for each element.
208+
*
209+
* @param fieldNames must not be {@literal null}.
210+
* @return new instance of {@link Top}.
211+
* @see #output(Fields)
212+
*/
213+
public Top output(String... fieldNames) {
214+
return output(Fields.fields(fieldNames));
215+
}
216+
217+
/**
218+
* Define expressions building the value included in the output for each element.
219+
*
220+
* @param out must not be {@literal null}.
221+
* @return new instance of {@link Top}.
222+
* @see #output(Fields)
223+
*/
224+
public Top output(AggregationExpression... out) {
225+
return new Top(append("output", Arrays.asList(out)));
226+
}
227+
}
228+
126229
/**
127230
* {@link AbstractAggregationExpression} to return the {@literal $firstN} elements.
128231
*/

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

+4
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ public class MethodReferenceNode extends ExpressionNode {
220220
.mappingParametersTo("n", "input"));
221221
map.put("lastN", mapArgRef().forOperator("$lastN") //
222222
.mappingParametersTo("n", "input"));
223+
map.put("top", mapArgRef().forOperator("$top") //
224+
.mappingParametersTo("output", "sortBy"));
225+
map.put("topN", mapArgRef().forOperator("$topN") //
226+
.mappingParametersTo("n", "output", "sortBy"));
223227

224228
// CONVERT OPERATORS
225229
map.put("convert", mapArgRef().forOperator("$convert") //

Diff for: spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SelectionOperatorUnitTests.java

+40
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,46 @@ void bottomNRenderedCorrectly() {
8989
"""));
9090
}
9191

92+
@Test // GH-4139
93+
void topMapsFieldNamesCorrectly() {
94+
95+
MongoMappingContext mappingContext = new MongoMappingContext();
96+
RelaxedTypeBasedAggregationOperationContext aggregationContext = new RelaxedTypeBasedAggregationOperationContext(
97+
Player.class, mappingContext,
98+
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
99+
100+
Document document = SelectionOperators.Top.top().output(Fields.fields("playerId", "score"))
101+
.sortBy(Sort.by(Direction.DESC, "score")).toDocument(aggregationContext);
102+
103+
assertThat(document).isEqualTo(Document.parse("""
104+
{
105+
$top:
106+
{
107+
output: [ "$player_id", "$s_cor_e" ],
108+
sortBy: { "s_cor_e": -1 }
109+
}
110+
}
111+
"""));
112+
}
113+
114+
@Test // GH-4139
115+
void topNRenderedCorrectly() {
116+
117+
Document document = SelectionOperators.Top.top().output(Fields.fields("playerId", "score"))
118+
.sortBy(Sort.by(Direction.DESC, "score")).limit(3).toDocument(Aggregation.DEFAULT_CONTEXT);
119+
120+
assertThat(document).isEqualTo(Document.parse("""
121+
{
122+
$topN:
123+
{
124+
n : 3,
125+
output: [ "$playerId", "$score" ],
126+
sortBy: { "score": -1 }
127+
}
128+
}
129+
"""));
130+
}
131+
92132
@Test // GH-4139
93133
void firstNMapsFieldNamesCorrectly() {
94134

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

+10
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,16 @@ void shouldRenderBottomN() {
11841184
assertThat(transform("bottomN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $bottomN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}");
11851185
}
11861186

1187+
@Test // GH-4139
1188+
void shouldRenderTop() {
1189+
assertThat(transform("top(new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $top : { output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}");
1190+
}
1191+
1192+
@Test // GH-4139
1193+
void shouldRenderTopN() {
1194+
assertThat(transform("topN(3, new String[]{\"$playerId\", \"$score\" }, { \"score\" : -1 })")).isEqualTo("{ $topN : { n : 3, output: [ \"$playerId\", \"$score\" ], sortBy: { \"score\": -1 }}}");
1195+
}
1196+
11871197
@Test // GH-4139
11881198
void shouldRenderFirstN() {
11891199
assertThat(transform("firstN(3, \"$score\")")).isEqualTo("{ $firstN : { n : 3, input : \"$score\" }}");

0 commit comments

Comments
 (0)