Skip to content

Commit 83cfd1f

Browse files
author
Justin Lee
committed
implement the $graphLookup pipeline stage
resolves JAVA-2187
1 parent a6b073e commit 83cfd1f

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,46 @@ public static Bson lookup(final String from, final String localField, final Stri
113113
.append("as", new BsonString(as)));
114114
}
115115

116+
/**
117+
* Creates a graphLookup pipeline stage for the specified filter
118+
*
119+
* @param <TExpression> the expression type
120+
* @param from the collection to query
121+
* @param startWith the expression to start the graph lookup with
122+
* @param connectFromField the from field
123+
* @param connectToField the to field
124+
* @param as name of field in output document
125+
* @return the graphLookup pipeline stage
126+
* @mongodb.driver.manual reference/operator/aggregation/graphLookup/ graphLookup
127+
* @mongodb.server.release 3.4
128+
* @since 3.4
129+
*/
130+
public static <TExpression> Bson graphLookup(final String from, final TExpression startWith, final String connectFromField,
131+
final String connectToField, final String as) {
132+
return graphLookup(from, startWith, connectFromField, connectToField, as, new GraphLookupOptions());
133+
}
134+
135+
/**
136+
* Creates a graphLookup pipeline stage for the specified filter
137+
*
138+
* @param <TExpression> the expression type
139+
* @param from the collection to query
140+
* @param startWith the expression to start the graph lookup with
141+
* @param connectFromField the from field
142+
* @param connectToField the to field
143+
* @param as name of field in output document
144+
* @param options optional values for the graphLookup
145+
* @return the graphLookup pipeline stage
146+
* @mongodb.driver.manual reference/operator/aggregation/graphLookup/ graphLookup
147+
* @mongodb.server.release 3.4
148+
* @since 3.4
149+
*/
150+
public static <TExpression> Bson graphLookup(final String from, final TExpression startWith, final String connectFromField,
151+
final String connectToField, final String as, final GraphLookupOptions options) {
152+
notNull("options", options);
153+
return new GraphLookupStage<TExpression>(from, startWith, connectFromField, connectToField, as, options);
154+
}
155+
116156
/**
117157
* Creates a $group pipeline stage for the specified filter
118158
*
@@ -222,6 +262,64 @@ public String toString() {
222262
}
223263
}
224264

265+
private static final class GraphLookupStage<TExpression> implements Bson {
266+
private final String from;
267+
private final TExpression startWith;
268+
private final String connectFromField;
269+
private final String connectToField;
270+
private final String as;
271+
private final GraphLookupOptions options;
272+
273+
private GraphLookupStage(final String from, final TExpression startWith, final String connectFromField, final String connectToField,
274+
final String as, final GraphLookupOptions options) {
275+
this.from = from;
276+
this.startWith = startWith;
277+
this.connectFromField = connectFromField;
278+
this.connectToField = connectToField;
279+
this.as = as;
280+
this.options = options;
281+
}
282+
283+
@Override
284+
public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
285+
BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
286+
287+
writer.writeStartDocument();
288+
289+
writer.writeStartDocument("$graphLookup");
290+
291+
writer.writeString("from", from);
292+
writer.writeName("startWith");
293+
BuildersHelper.encodeValue(writer, startWith, codecRegistry);
294+
295+
writer.writeString("connectFromField", connectFromField);
296+
writer.writeString("connectToField", connectToField);
297+
writer.writeString("as", as);
298+
if (options.getMaxDepth() != null) {
299+
writer.writeInt32("maxDepth", options.getMaxDepth());
300+
}
301+
if (options.getDepthField() != null) {
302+
writer.writeString("depthField", options.getDepthField());
303+
}
304+
305+
writer.writeEndDocument();
306+
307+
return writer.getDocument();
308+
}
309+
310+
@Override
311+
public String toString() {
312+
return "GraphLookupStage{"
313+
+ "as='" + as + '\''
314+
+ ", connectFromField='" + connectFromField + '\''
315+
+ ", connectToField='" + connectToField + '\''
316+
+ ", from='" + from + '\''
317+
+ ", options=" + options
318+
+ ", startWith=" + startWith
319+
+ '}';
320+
}
321+
}
322+
225323
private static class GroupStage<TExpression> implements Bson {
226324
private final TExpression id;
227325
private final List<BsonField> fieldAccumulators;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2008-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+
/**
20+
* The options for a graphLookup aggregation pipeline stage
21+
*
22+
* @mongodb.driver.manual reference/operator/aggregation/graphLookup/ graphLookup
23+
* @mongodb.server.release 3.4
24+
* @since 3.4
25+
*/
26+
public final class GraphLookupOptions {
27+
private Integer maxDepth;
28+
private String depthField;
29+
30+
/**
31+
* The name of the field in which to store the depth value
32+
*
33+
* @param field the field name
34+
* @return this
35+
*/
36+
public GraphLookupOptions depthField(final String field) {
37+
depthField = field;
38+
return this;
39+
}
40+
41+
/**
42+
* @return the field name
43+
*/
44+
public String getDepthField() {
45+
return depthField;
46+
}
47+
48+
/**
49+
* Specifies a maximum recursive depth for the $graphLookup. This number must be non-negative.
50+
*
51+
* @param max the maximum depth
52+
* @return this
53+
*/
54+
public GraphLookupOptions maxDepth(final Integer max) {
55+
maxDepth = max;
56+
return this;
57+
}
58+
59+
/**
60+
* @return the maximum depth
61+
*/
62+
public Integer getMaxDepth() {
63+
return maxDepth;
64+
}
65+
66+
@Override
67+
public String toString() {
68+
StringBuilder stringBuilder = new StringBuilder()
69+
.append("GraphLookupOptions{");
70+
if (depthField != null) {
71+
stringBuilder.append("depthField='")
72+
.append(depthField)
73+
.append('\'');
74+
if (maxDepth != null) {
75+
stringBuilder.append(", ");
76+
}
77+
}
78+
if (maxDepth != null) {
79+
stringBuilder
80+
.append("maxDepth=")
81+
.append(maxDepth);
82+
}
83+
return stringBuilder
84+
.append('}')
85+
.toString();
86+
}
87+
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.mongodb.client.model
1616

1717
import com.mongodb.MongoNamespace
1818
import com.mongodb.OperationFunctionalSpecification
19+
import org.bson.BsonString
1920
import org.bson.Document
2021
import org.bson.conversions.Bson
2122
import spock.lang.IgnoreIf
@@ -32,6 +33,7 @@ import static com.mongodb.client.model.Accumulators.push
3233
import static com.mongodb.client.model.Accumulators.stdDevPop
3334
import static com.mongodb.client.model.Accumulators.stdDevSamp
3435
import static com.mongodb.client.model.Accumulators.sum
36+
import static com.mongodb.client.model.Aggregates.graphLookup
3537
import static com.mongodb.client.model.Aggregates.group
3638
import static com.mongodb.client.model.Aggregates.limit
3739
import static com.mongodb.client.model.Aggregates.lookup
@@ -231,4 +233,82 @@ class AggregatesFunctionalSpecification extends OperationFunctionalSpecification
231233
cleanup:
232234
fromHelper?.drop()
233235
}
236+
237+
@IgnoreIf({ !serverVersionAtLeast(asList(3, 3, 9)) })
238+
def '$graphLookup'() {
239+
given:
240+
def fromCollectionName = 'contacts'
241+
def fromHelper = getCollectionHelper(new MongoNamespace(getDatabaseName(), fromCollectionName))
242+
243+
fromHelper.drop()
244+
245+
fromHelper.insertDocuments(Document.parse('{ _id: 0, name: "Bob Smith", friends: ["Anna Jones", "Chris Green"] }'))
246+
fromHelper.insertDocuments(Document.parse('{ _id: 1, name: "Anna Jones", friends: ["Bob Smith", "Chris Green", "Joe Lee"] }'))
247+
fromHelper.insertDocuments(Document.parse('{ _id: 2, name: "Chris Green", friends: ["Anna Jones", "Bob Smith"] }'))
248+
fromHelper.insertDocuments(Document.parse('{ _id: 3, name: "Joe Lee", friends: ["Anna Jones", "Fred Brown"] }'))
249+
fromHelper.insertDocuments(Document.parse('{ _id: 4, name: "Fred Brown", friends: ["Joe Lee"] }'))
250+
251+
def lookupDoc = graphLookup('contacts', new BsonString('$friends'), 'friends', 'name', 'socialNetwork')
252+
253+
when:
254+
def results = fromHelper.aggregate([lookupDoc])
255+
256+
then:
257+
results[0] ==
258+
Document.parse('''{
259+
_id: 0,
260+
name: "Bob Smith",
261+
friends: ["Anna Jones", "Chris Green"],
262+
socialNetwork: [
263+
{ _id: 3, name: "Joe Lee", friends: ["Anna Jones", "Fred Brown" ] },
264+
{ _id: 4, name: "Fred Brown", friends: ["Joe Lee"] },
265+
{ _id: 0, name: "Bob Smith", friends: ["Anna Jones", "Chris Green"] },
266+
{ _id: 2, name: "Chris Green", friends: ["Anna Jones", "Bob Smith"] },
267+
{ _id: 1, name: "Anna Jones", friends: ["Bob Smith", "Chris Green", "Joe Lee"] }
268+
]
269+
}''')
270+
271+
cleanup:
272+
fromHelper?.drop()
273+
}
274+
275+
@IgnoreIf({ !serverVersionAtLeast(asList(3, 3, 9)) })
276+
def '$graphLookup with options'() {
277+
given:
278+
def fromCollectionName = 'contacts'
279+
def fromHelper = getCollectionHelper(new MongoNamespace(getDatabaseName(), fromCollectionName))
280+
281+
fromHelper.drop()
282+
283+
fromHelper.insertDocuments(Document.parse('{ _id: 0, name: "Bob Smith", friends: ["Anna Jones", "Chris Green"] }'))
284+
fromHelper.insertDocuments(Document.parse('{ _id: 1, name: "Anna Jones", friends: ["Bob Smith", "Chris Green", "Joe Lee"] }'))
285+
fromHelper.insertDocuments(Document.parse('{ _id: 2, name: "Chris Green", friends: ["Anna Jones", "Bob Smith"] }'))
286+
fromHelper.insertDocuments(Document.parse('{ _id: 3, name: "Joe Lee", friends: ["Anna Jones", "Fred Brown"] }'))
287+
fromHelper.insertDocuments(Document.parse('{ _id: 4, name: "Fred Brown", friends: ["Joe Lee"] }'))
288+
289+
def lookupDoc = graphLookup('contacts', new BsonString('$friends'), 'friends', 'name', 'socialNetwork',
290+
new GraphLookupOptions()
291+
.maxDepth(1)
292+
.depthField('depth'))
293+
294+
when:
295+
def results = fromHelper.aggregate([lookupDoc])
296+
297+
then:
298+
results[0] ==
299+
Document.parse('''{
300+
_id: 0,
301+
name: "Bob Smith",
302+
friends: ["Anna Jones", "Chris Green"],
303+
socialNetwork: [
304+
{ _id: 3, name: "Joe Lee", friends: ["Anna Jones", "Fred Brown" ], depth:1 },
305+
{ _id: 0, name: "Bob Smith", friends: ["Anna Jones", "Chris Green"], depth:1 },
306+
{ _id: 2, name: "Chris Green", friends: ["Anna Jones", "Bob Smith"], depth:0 },
307+
{ _id: 1, name: "Anna Jones", friends: ["Bob Smith", "Chris Green", "Joe Lee"], depth:0 }
308+
]
309+
}''')
310+
311+
cleanup:
312+
fromHelper?.drop()
313+
}
234314
}

driver-core/src/test/unit/com/mongodb/client/model/AggregatesSpecification.groovy

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import static com.mongodb.client.model.Accumulators.push
3232
import static com.mongodb.client.model.Accumulators.stdDevPop
3333
import static com.mongodb.client.model.Accumulators.stdDevSamp
3434
import static com.mongodb.client.model.Accumulators.sum
35+
import static com.mongodb.client.model.Aggregates.graphLookup
3536
import static com.mongodb.client.model.Aggregates.group
3637
import static com.mongodb.client.model.Aggregates.limit
3738
import static com.mongodb.client.model.Aggregates.lookup
@@ -80,6 +81,25 @@ class AggregatesSpecification extends Specification {
8081
foreignField: "foreignField", as: "as" } }''')
8182
}
8283

84+
def 'should render $graphLookup'() {
85+
expect:
86+
toBson(graphLookup('contacts', '$friends', 'friends', 'name', 'socialNetwork')) ==
87+
parse('''{ $graphLookup: { from: "contacts", startWith: "$friends", connectFromField: "friends", connectToField: "name",
88+
as: "socialNetwork" } }''')
89+
toBson(graphLookup('contacts', '$friends', 'friends', 'name', 'socialNetwork', new GraphLookupOptions().maxDepth(1))) ==
90+
parse('''{ $graphLookup: { from: "contacts", startWith: "$friends", connectFromField: "friends", connectToField: "name",
91+
as: "socialNetwork", maxDepth: 1 } }''')
92+
toBson(graphLookup('contacts', '$friends', 'friends', 'name', 'socialNetwork', new GraphLookupOptions()
93+
.maxDepth(1)
94+
.depthField('master'))) ==
95+
parse('''{ $graphLookup: { from: "contacts", startWith: "$friends", connectFromField: "friends", connectToField: "name",
96+
as: "socialNetwork", maxDepth: 1, depthField: "master" } }''')
97+
toBson(graphLookup('contacts', '$friends', 'friends', 'name', 'socialNetwork', new GraphLookupOptions()
98+
.depthField('master'))) ==
99+
parse('''{ $graphLookup: { from: "contacts", startWith: "$friends", connectFromField: "friends", connectToField: "name",
100+
as: "socialNetwork", depthField: "master" } }''')
101+
}
102+
83103
def 'should render $skip'() {
84104
expect:
85105
toBson(skip(5)) == parse('{ $skip : 5 }')

0 commit comments

Comments
 (0)