Skip to content

Commit 74a4edd

Browse files
author
Justin Lee
committed
$addFields and $replaceRoot support
1 parent 9486358 commit 74a4edd

File tree

6 files changed

+370
-1
lines changed

6 files changed

+370
-1
lines changed

driver-core/src/main/com/mongodb/client/model/Aggregates.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@
3636
*/
3737
public final class Aggregates {
3838

39+
/**
40+
* Creates an $addFields pipeline stage
41+
*
42+
* @param fields the fields to add
43+
* @return the $addFields pipeline stage
44+
* @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
45+
* @mongodb.server.release 3.4
46+
* @since 3.4
47+
*/
48+
public static Bson addFields(final Field<?>... fields) {
49+
return addFields(asList(fields));
50+
}
51+
52+
/**
53+
* Creates an $addFields pipeline stage
54+
*
55+
* @param fields the fields to add
56+
* @return the $addFields pipeline stage
57+
* @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
58+
* @mongodb.server.release 3.4
59+
* @since 3.4
60+
*/
61+
public static Bson addFields(final List<Field<?>> fields) {
62+
return new AddFieldsStage(fields);
63+
}
64+
3965
/**
4066
* Creates a $bucket pipeline stage
4167
*
@@ -356,6 +382,20 @@ public static Bson out(final String collectionName) {
356382
return new BsonDocument("$out", new BsonString(collectionName));
357383
}
358384

385+
/**
386+
* Creates a $replaceRoot pipeline stage
387+
*
388+
* @param <TExpression> the new root type
389+
* @param value the new root value
390+
* @return the $replaceRoot pipeline stage
391+
* @mongodb.driver.manual reference/operator/aggregation/replaceRoot/ $replaceRoot
392+
* @mongodb.server.release 3.4
393+
* @since 3.4
394+
*/
395+
public static <TExpression> Bson replaceRoot(final TExpression value) {
396+
return new ReplaceRootStage(value);
397+
}
398+
359399
/**
360400
* Creates a $sample pipeline stage with the specified sample size
361401
*
@@ -667,6 +707,68 @@ public String toString() {
667707

668708
}
669709

710+
private static class AddFieldsStage implements Bson {
711+
private final List<Field<?>> fields;
712+
713+
AddFieldsStage(final List<Field<?>> fields) {
714+
this.fields = fields;
715+
}
716+
717+
@Override
718+
public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
719+
BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
720+
writer.writeStartDocument();
721+
writer.writeName("$addFields");
722+
writer.writeStartDocument();
723+
for (Field<?> field : fields) {
724+
writer.writeName(field.getName());
725+
BuildersHelper.encodeValue(writer, field.getValue(), codecRegistry);
726+
}
727+
writer.writeEndDocument();
728+
writer.writeEndDocument();
729+
730+
return writer.getDocument();
731+
}
732+
733+
@Override
734+
public String toString() {
735+
return "Stage{"
736+
+ "name='$addFields', "
737+
+ "fields=" + fields
738+
+ '}';
739+
}
740+
}
741+
742+
private static class ReplaceRootStage<TExpression> implements Bson {
743+
private final TExpression value;
744+
745+
ReplaceRootStage(final TExpression value) {
746+
this.value = value;
747+
}
748+
749+
@Override
750+
public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
751+
BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
752+
writer.writeStartDocument();
753+
writer.writeName("$replaceRoot");
754+
writer.writeStartDocument();
755+
writer.writeName("newRoot");
756+
BuildersHelper.encodeValue(writer, value, codecRegistry);
757+
writer.writeEndDocument();
758+
writer.writeEndDocument();
759+
760+
return writer.getDocument();
761+
}
762+
763+
@Override
764+
public String toString() {
765+
return "Stage{"
766+
+ "name='$replaceRoot', "
767+
+ "value=" + value
768+
+ '}';
769+
}
770+
}
771+
670772
private Aggregates() {
671773
}
672774
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2016 MongoDB, Inc.
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+
* http://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+
17+
package com.mongodb.client.model;
18+
19+
import static com.mongodb.assertions.Assertions.notNull;
20+
21+
/**
22+
* Helps define new fields for the $addFields pipeline stage
23+
*
24+
* @param <TExpression> the type of the value for the new field
25+
* @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
26+
* @mongodb.server.release 3.4
27+
* @since 3.4
28+
*/
29+
public class Field<TExpression> {
30+
private final String name;
31+
private TExpression value;
32+
33+
/**
34+
* Creates a new field definition for use in $addFields pipeline stages
35+
*
36+
* @param name the name of the new field
37+
* @param value the value of the new field
38+
* @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
39+
*/
40+
public Field(final String name, final TExpression value) {
41+
this.name = notNull("name", name);
42+
this.value = value;
43+
}
44+
45+
/**
46+
* @return the new of the new field
47+
*/
48+
public String getName() {
49+
return name;
50+
}
51+
52+
/**
53+
* @return the value of the new field
54+
*/
55+
public TExpression getValue() {
56+
return value;
57+
}
58+
59+
@Override
60+
public boolean equals(final Object o) {
61+
if (this == o) {
62+
return true;
63+
}
64+
if (!(o instanceof Field)) {
65+
return false;
66+
}
67+
68+
Field<?> field = (Field<?>) o;
69+
70+
if (!name.equals(field.name)) {
71+
return false;
72+
}
73+
return value != null ? value.equals(field.value) : field.value == null;
74+
75+
}
76+
77+
@Override
78+
public int hashCode() {
79+
int result = name.hashCode();
80+
result = 31 * result + (value != null ? value.hashCode() : 0);
81+
return result;
82+
}
83+
}

driver-core/src/test/functional/com/mongodb/client/model/AggregatesFunctionalSpecification.groovy

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import static com.mongodb.client.model.Accumulators.push
3434
import static com.mongodb.client.model.Accumulators.stdDevPop
3535
import static com.mongodb.client.model.Accumulators.stdDevSamp
3636
import static com.mongodb.client.model.Accumulators.sum
37+
import static com.mongodb.client.model.Aggregates.addFields
3738
import static com.mongodb.client.model.Aggregates.bucket
3839
import static com.mongodb.client.model.Aggregates.bucketAuto
3940
import static com.mongodb.client.model.Aggregates.count
@@ -45,6 +46,7 @@ import static com.mongodb.client.model.Aggregates.lookup
4546
import static com.mongodb.client.model.Aggregates.match
4647
import static com.mongodb.client.model.Aggregates.out
4748
import static com.mongodb.client.model.Aggregates.project
49+
import static com.mongodb.client.model.Aggregates.replaceRoot
4850
import static com.mongodb.client.model.Aggregates.sample
4951
import static com.mongodb.client.model.Aggregates.skip
5052
import static com.mongodb.client.model.Aggregates.sort
@@ -540,4 +542,109 @@ class AggregatesFunctionalSpecification extends OperationFunctionalSpecification
540542
cleanup:
541543
helper?.drop()
542544
}
545+
546+
@IgnoreIf({ !serverVersionAtLeast(asList(3, 3, 11)) })
547+
def '$addFields'() {
548+
given:
549+
def helper = getCollectionHelper()
550+
551+
when:
552+
helper.drop()
553+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
554+
def results = helper.aggregate([addFields(new Field('newField', null))])
555+
556+
then:
557+
results == [Document.parse('{_id: 0, a: 1, newField: null}')]
558+
559+
when:
560+
helper.drop()
561+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
562+
results = helper.aggregate([addFields(new Field('newField', 'hello'))])
563+
564+
then:
565+
results == [Document.parse('{_id: 0, a: 1, newField: "hello"}')]
566+
567+
when:
568+
helper.drop()
569+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
570+
results = helper.aggregate([addFields(new Field('b', '$a'))])
571+
572+
then:
573+
results == [Document.parse('{_id: 0, a: 1, b: 1}')]
574+
575+
when:
576+
helper.drop()
577+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
578+
results = helper.aggregate([addFields(new Field('this', '$$CURRENT'))])
579+
580+
then:
581+
results == [Document.parse('{_id: 0, a: 1, this: {_id: 0, a: 1}}')]
582+
583+
when:
584+
helper.drop()
585+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
586+
results = helper.aggregate([addFields(new Field('myNewField',
587+
new Document('c', 3).append('d', 4)))])
588+
589+
then:
590+
results == [Document.parse('{_id: 0, a: 1, myNewField: {c: 3, d: 4}}')]
591+
592+
when:
593+
helper.drop()
594+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
595+
results = helper.aggregate([addFields(new Field('alt3', new Document('$lt', asList('$a', 3))))])
596+
597+
then:
598+
results == [Document.parse('{_id: 0, a: 1, alt3: true}')]
599+
600+
when:
601+
helper.drop()
602+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
603+
results = helper.aggregate([addFields(new Field('b', 3), new Field('c', 5))])
604+
605+
then:
606+
results == [Document.parse('{_id: 0, a: 1, b: 3, c: 5}')]
607+
608+
when:
609+
helper.drop()
610+
helper.insertDocuments(Document.parse('{_id: 0, a: 1}'))
611+
results = helper.aggregate([addFields(new Field('a', [1, 2, 3]))])
612+
613+
then:
614+
results == [Document.parse('{_id: 0, a: [1, 2, 3]}')]
615+
616+
cleanup:
617+
helper?.drop()
618+
}
619+
620+
@IgnoreIf({ !serverVersionAtLeast(asList(3, 3, 11)) })
621+
def '$replaceRoot'() {
622+
given:
623+
def helper = getCollectionHelper()
624+
def results = []
625+
626+
when:
627+
helper.drop()
628+
helper.insertDocuments(Document.parse('{_id: 0, a1: {b: 1}, a2: 2}'))
629+
results = helper.aggregate([replaceRoot('$a1')])
630+
631+
then:
632+
results == [Document.parse('{b: 1}')]
633+
634+
when:
635+
helper.drop()
636+
helper.insertDocuments(Document.parse('{_id: 0, a1: {b: {c1: 4, c2: 5}}, a2: 2}'))
637+
results = helper.aggregate([replaceRoot('$a1.b')])
638+
639+
then:
640+
results == [Document.parse('{c1: 4, c2: 5}')]
641+
642+
when:
643+
helper.drop()
644+
helper.insertDocuments(Document.parse('{_id: 0, a1: {b: 1, _id: 7}, a2: 2}'))
645+
results = helper.aggregate([replaceRoot('$a1')])
646+
647+
then:
648+
results == [Document.parse('{b: 1, _id: 7}')]
649+
}
543650
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2016 MongoDB, Inc.
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+
* http://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+
17+
package com.mongodb.client.model
18+
19+
import spock.lang.Specification
20+
21+
class FieldSpecification extends Specification {
22+
def 'should validate name'() {
23+
when:
24+
new Field(null, [1, 2, 3])
25+
26+
then:
27+
thrown(IllegalArgumentException)
28+
29+
}
30+
31+
def 'should accept null values'() {
32+
when:
33+
def field = new Field('name', null)
34+
35+
then:
36+
field.getName() == 'name'
37+
field.getValue() == null
38+
}
39+
40+
def 'should accept properties'() {
41+
when:
42+
def field = new Field('name', [1, 2, 3])
43+
44+
then:
45+
field.getName() == 'name'
46+
field.getValue() == [1, 2, 3]
47+
}
48+
}

driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.bson.codecs.Decoder;
5252
import org.bson.codecs.DocumentCodec;
5353
import org.bson.codecs.DocumentCodecProvider;
54+
import org.bson.codecs.IterableCodecProvider;
5455
import org.bson.codecs.ValueCodecProvider;
5556
import org.bson.codecs.configuration.CodecRegistries;
5657
import org.bson.codecs.configuration.CodecRegistry;
@@ -68,6 +69,7 @@ public final class CollectionHelper<T> {
6869

6970
private Codec<T> codec;
7071
private CodecRegistry registry = CodecRegistries.fromProviders(new BsonValueCodecProvider(),
72+
new IterableCodecProvider(),
7173
new ValueCodecProvider(),
7274
new DocumentCodecProvider(),
7375
new GeoJsonCodecProvider());

0 commit comments

Comments
 (0)