/*
* Copyright 2013-2017 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
*
* http://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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.bson.Document;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.test.util.BasicDbListBuilder;
/**
* Unit tests for {@link Aggregation}.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
*/
public class AggregationUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAggregationOperation() {
newAggregation((AggregationOperation[]) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullTypedAggregationOperation() {
newAggregation(String.class, (AggregationOperation[]) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNoAggregationOperation() {
newAggregation(new AggregationOperation[0]);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNoTypedAggregationOperation() {
newAggregation(String.class, new AggregationOperation[0]);
}
@Test // DATAMONGO-753
public void checkForCorrectFieldScopeTransfer() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Invalid reference");
exception.expectMessage("'b'");
newAggregation( //
project("a", "b"), //
group("a").count().as("cnt"), // a was introduced to the context by the project operation
project("cnt", "b") // b was removed from the context by the group operation
).toDocument("foo", Aggregation.DEFAULT_CONTEXT); // -> triggers IllegalArgumentException
}
@Test // DATAMONGO-753
public void unwindOperationShouldNotChangeAvailableFields() {
newAggregation( //
project("a", "b"), //
unwind("a"), //
project("a", "b") // b should still be available
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
}
@Test // DATAMONGO-1391
public void unwindOperationWithIndexShouldPreserveFields() {
newAggregation( //
project("a", "b"), //
unwind("a", "x"), //
project("a", "b") // b should still be available
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
}
@Test // DATAMONGO-1391
public void unwindOperationWithIndexShouldAddIndexField() {
newAggregation( //
project("a", "b"), //
unwind("a", "x"), //
project("a", "x") // b should still be available
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
}
@Test // DATAMONGO-1391
public void fullUnwindOperationShouldBuildCorrectClause() {
Document agg = newAggregation( //
unwind("a", "x", true)).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document unwind = ((List<Document>) agg.get("pipeline")).get(0);
assertThat((Document) unwind.get("$unwind"),
isBsonObject(). //
containing("includeArrayIndex", "x").//
containing("preserveNullAndEmptyArrays", true));
}
@Test // DATAMONGO-1391
public void unwindOperationWithPreserveNullShouldBuildCorrectClause() {
Document agg = newAggregation( //
unwind("a", true)).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document unwind = ((List<Document>) agg.get("pipeline")).get(0);
assertThat(unwind,
isBsonObject().notContaining("includeArrayIndex").containing("preserveNullAndEmptyArrays", true));
}
@Test // DATAMONGO-1550
public void replaceRootOperationShouldBuildCorrectClause() {
Document agg = newAggregation( //
replaceRoot().withDocument().andValue("value").as("field")) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document unwind = ((List<Document>) agg.get("pipeline")).get(0);
assertThat(unwind, isBsonObject().containing("$replaceRoot.newRoot", new Document("field", "value")));
}
@Test // DATAMONGO-753
public void matchOperationShouldNotChangeAvailableFields() {
newAggregation( //
project("a", "b"), //
match(where("a").gte(1)), //
project("a", "b") // b should still be available
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
}
@Test // DATAMONGO-788
public void referencesToGroupIdsShouldBeRenderedAsReferences() {
Document agg = newAggregation( //
project("a"), //
group("a").count().as("aCnt"), //
project("aCnt", "a") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(2);
Document fields = getAsDocument(secondProjection, "$project");
assertThat(fields.get("aCnt"), is((Object) 1));
assertThat(fields.get("a"), is((Object) "$_id.a"));
}
@Test // DATAMONGO-791
public void allowAggregationOperationsToBePassedAsIterable() {
List<AggregationOperation> ops = new ArrayList<AggregationOperation>();
ops.add(project("a"));
ops.add(group("a").count().as("aCnt"));
ops.add(project("aCnt", "a"));
Document agg = newAggregation(ops).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(2);
Document fields = getAsDocument(secondProjection, "$project");
assertThat(fields.get("aCnt"), is((Object) 1));
assertThat(fields.get("a"), is((Object) "$_id.a"));
}
@Test // DATAMONGO-791
public void allowTypedAggregationOperationsToBePassedAsIterable() {
List<AggregationOperation> ops = new ArrayList<AggregationOperation>();
ops.add(project("a"));
ops.add(group("a").count().as("aCnt"));
ops.add(project("aCnt", "a"));
Document agg = newAggregation(Document.class, ops).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(2);
Document fields = getAsDocument(secondProjection, "$project");
assertThat(fields.get("aCnt"), is((Object) 1));
assertThat(fields.get("a"), is((Object) "$_id.a"));
}
@Test // DATAMONGO-838
public void expressionBasedFieldsShouldBeReferencableInFollowingOperations() {
Document agg = newAggregation( //
project("a").andExpression("b+c").as("foo"), //
group("a").sum("foo").as("foosum") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(1);
Document fields = getAsDocument(secondProjection, "$group");
assertThat(fields.get("foosum"), is((Object) new Document("$sum", "$foo")));
}
@Test // DATAMONGO-908
public void shouldSupportReferingToNestedPropertiesInGroupOperation() {
Document agg = newAggregation( //
project("cmsParameterId", "rules"), //
unwind("rules"), //
group("cmsParameterId", "rules.ruleType").count().as("totol") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg, is(notNullValue()));
Document group = ((List<Document>) agg.get("pipeline")).get(2);
Document fields = getAsDocument(group, "$group");
Document id = getAsDocument(fields, "_id");
assertThat(id.get("ruleType"), is((Object) "$rules.ruleType"));
}
@Test // DATAMONGO-1585
public void shouldSupportSortingBySyntheticAndExposedGroupFields() {
Document agg = newAggregation( //
group("cmsParameterId").addToSet("title").as("titles"), //
sort(Direction.ASC, "cmsParameterId", "titles") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg, is(notNullValue()));
Document sort = ((List<Document>) agg.get("pipeline")).get(1);
assertThat(getAsDocument(sort, "$sort"), is(Document.parse("{ \"_id.cmsParameterId\" : 1 , \"titles\" : 1}")));
}
@Test // DATAMONGO-1585
public void shouldSupportSortingByProjectedFields() {
Document agg = newAggregation( //
project("cmsParameterId") //
.and(SystemVariable.CURRENT + ".titles").as("titles") //
.and("field").as("alias"), //
sort(Direction.ASC, "cmsParameterId", "titles", "alias") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg, is(notNullValue()));
Document sort = ((List<Document>) agg.get("pipeline")).get(1);
assertThat(getAsDocument(sort, "$sort"),
isBsonObject().containing("cmsParameterId", 1) //
.containing("titles", 1) //
.containing("alias", 1));
}
@Test // DATAMONGO-924
public void referencingProjectionAliasesFromPreviousStepShouldReferToTheSameFieldTarget() {
Document agg = newAggregation( //
project().and("foo.bar").as("ba") //
, project().and("ba").as("b") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document projection0 = extractPipelineElement(agg, 0, "$project");
assertThat(projection0, is((Document) new Document("ba", "$foo.bar")));
Document projection1 = extractPipelineElement(agg, 1, "$project");
assertThat(projection1, is((Document) new Document("b", "$ba")));
}
@Test // DATAMONGO-960
public void shouldRenderAggregationWithDefaultOptionsCorrectly() {
Document agg = newAggregation( //
project().and("a").as("aa") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg,
is(Document.parse("{ \"aggregate\" : \"foo\" , \"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}]}")));
}
@Test // DATAMONGO-960
public void shouldRenderAggregationWithCustomOptionsCorrectly() {
AggregationOptions aggregationOptions = newAggregationOptions().explain(true).cursor(new Document("foo", 1))
.allowDiskUse(true).build();
Document agg = newAggregation( //
project().and("a").as("aa") //
) //
.withOptions(aggregationOptions) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
assertThat(agg,
is(Document.parse("{ \"aggregate\" : \"foo\" , " //
+ "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " //
+ "\"allowDiskUse\" : true , " //
+ "\"explain\" : true , " //
+ "\"cursor\" : { \"foo\" : 1}}") //
));
}
@Test // DATAMONGO-954, DATAMONGO-1585
public void shouldSupportReferencingSystemVariables() {
Document agg = newAggregation( //
project("someKey") //
.and("a").as("a1") //
.and(Aggregation.CURRENT + ".a").as("a2") //
, sort(Direction.DESC, "a1") //
, group("someKey").first(Aggregation.ROOT).as("doc") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document projection0 = extractPipelineElement(agg, 0, "$project");
assertThat(projection0, is((Document) new Document("someKey", 1).append("a1", "$a").append("a2", "$$CURRENT.a")));
Document sort = extractPipelineElement(agg, 1, "$sort");
assertThat(sort, is((Document) new Document("a1", -1)));
Document group = extractPipelineElement(agg, 2, "$group");
assertThat(group, is((Document) new Document("_id", "$someKey").append("doc", new Document("$first", "$$ROOT"))));
}
@Test // DATAMONGO-1254
public void shouldExposeAliasedFieldnameForProjectionsIncludingOperationsDownThePipeline() {
Document agg = Aggregation.newAggregation(//
project("date") //
.and("tags").minus(10).as("tags_count")//
, group("date")//
.sum("tags_count").as("count")//
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document group = extractPipelineElement(agg, 1, "$group");
assertThat(getAsDocument(group, "count"), is(new Document().append("$sum", "$tags_count")));
}
@Test // DATAMONGO-1254
public void shouldUseAliasedFieldnameForProjectionsIncludingOperationsDownThePipelineWhenUsingSpEL() {
Document agg = Aggregation.newAggregation(//
project("date") //
.andExpression("tags-10")//
, group("date")//
.sum("tags_count").as("count")//
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document group = extractPipelineElement(agg, 1, "$group");
assertThat(getAsDocument(group, "count"), is(new Document().append("$sum", "$tags_count")));
}
@Test // DATAMONGO-861
public void conditionExpressionBasedFieldsShouldBeReferencableInFollowingOperations() {
Document agg = newAggregation( //
project("a", "answer"), //
group("a")
.first(Cond.newBuilder().when(Criteria.where("a").gte(42)).thenValueOf("answer").otherwise("no-answer"))
.as("foosum") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
System.out.println("agg: " + agg);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(1);
Document fields = getAsDocument(secondProjection, "$group");
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first"));
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "$answer"));
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer"));
}
@Test // DATAMONGO-861
public void shouldRenderProjectionConditionalExpressionCorrectly() {
Document agg = Aggregation.newAggregation(//
project().and(ConditionalOperators.Cond.newBuilder() //
.when("isYellow") //
.then("bright") //
.otherwise("dark")).as("color"))
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 0, "$project");
Document expectedCondition = new Document() //
.append("if", "$isYellow") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDocument(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
@Test // DATAMONGO-861
public void shouldRenderProjectionConditionalCorrectly() {
Document agg = Aggregation.newAggregation(//
project().and("color")
.applyCondition(ConditionalOperators.Cond.newBuilder() //
.when("isYellow") //
.then("bright") //
.otherwise("dark")))
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 0, "$project");
Document expectedCondition = new Document() //
.append("if", "$isYellow") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDocument(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
@Test // DATAMONGO-861
public void shouldRenderProjectionConditionalWithCriteriaCorrectly() {
Document agg = Aggregation
.newAggregation(project()//
.and("color")//
.applyCondition(ConditionalOperators.Cond.newBuilder().when(Criteria.where("key").gt(5)) //
.then("bright").otherwise("dark"))) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 0, "$project");
Document expectedCondition = new Document() //
.append("if", new Document("$gt", Arrays.<Object> asList("$key", 5))) //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDocument(project, "color"), isBsonObject().containing("$cond", expectedCondition));
}
@Test // DATAMONGO-861
public void referencingProjectionAliasesShouldRenderProjectionConditionalWithFieldReferenceCorrectly() {
Document agg = Aggregation
.newAggregation(//
project().and("color").as("chroma"),
project().and("luminosity") //
.applyCondition(ConditionalOperators //
.when("chroma") //
.thenValueOf("bright") //
.otherwise("dark"))) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 1, "$project");
Document expectedCondition = new Document() //
.append("if", "$chroma") //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDocument(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition));
}
@Test // DATAMONGO-861
public void referencingProjectionAliasesShouldRenderProjectionConditionalWithCriteriaReferenceCorrectly() {
Document agg = Aggregation
.newAggregation(//
project().and("color").as("chroma"),
project().and("luminosity") //
.applyCondition(ConditionalOperators.Cond.newBuilder()
.when(Criteria.where("chroma") //
.is(100)) //
.then("bright").otherwise("dark"))) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 1, "$project");
Document expectedCondition = new Document() //
.append("if", new Document("$eq", Arrays.<Object> asList("$chroma", 100))) //
.append("then", "bright") //
.append("else", "dark");
assertThat(getAsDocument(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition));
}
@Test // DATAMONGO-861
public void shouldRenderProjectionIfNullWithFieldReferenceCorrectly() {
Document agg = Aggregation
.newAggregation(//
project().and("color"), //
project().and("luminosity") //
.applyCondition(ConditionalOperators //
.ifNull("chroma") //
.then("unknown"))) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 1, "$project");
assertThat(getAsDocument(project, "luminosity"),
isBsonObject().containing("$ifNull", Arrays.<Object> asList("$chroma", "unknown")));
}
@Test // DATAMONGO-861
public void shouldRenderProjectionIfNullWithFallbackFieldReferenceCorrectly() {
Document agg = Aggregation
.newAggregation(//
project("fallback").and("color").as("chroma"),
project().and("luminosity") //
.applyCondition(ConditionalOperators.ifNull("chroma") //
.thenValueOf("fallback"))) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 1, "$project");
assertThat(getAsDocument(project, "luminosity"),
isBsonObject().containing("$ifNull", Arrays.asList("$chroma", "$fallback")));
}
@Test // DATAMONGO-1552
public void shouldHonorDefaultCountField() {
Document agg = Aggregation
.newAggregation(//
bucket("year"), //
project("count")) //
.toDocument("foo", Aggregation.DEFAULT_CONTEXT);
Document project = extractPipelineElement(agg, 1, "$project");
assertThat(project, isBsonObject().containing("count", 1));
}
@Test // DATAMONGO-1533
public void groupOperationShouldAllowUsageOfDerivedSpELAggregationExpression() {
Document agg = newAggregation( //
project("a"), //
group("a").first(AggregationSpELExpression.expressionOf("cond(a >= 42, 'answer', 'no-answer')")).as("foosum") //
).toDocument("foo", Aggregation.DEFAULT_CONTEXT);
@SuppressWarnings("unchecked")
Document secondProjection = ((List<Document>) agg.get("pipeline")).get(1);
Document fields = getAsDocument(secondProjection, "$group");
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first"));
assertThat(getAsDocument(fields, "foosum"),
isBsonObject().containing("$first.$cond.if", new Document("$gte", new Document("$a", 42))));
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "answer"));
assertThat(getAsDocument(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer"));
}
private Document extractPipelineElement(Document agg, int index, String operation) {
List<Document> pipeline = (List<Document>) agg.get("pipeline");
return (Document) pipeline.get(index).get(operation);
}
}