1+ /**
2+ * Tests basic functionality of the $getField expression.
3+ */
4+ ( function ( ) {
5+ "use strict" ;
6+
7+ load ( "jstests/aggregation/extras/utils.js" ) ; // For assertArrayEq.
8+
9+ const coll = db . expression_get_field ;
10+ coll . drop ( ) ;
11+
12+ for ( let i = 0 ; i < 2 ; i ++ ) {
13+ assert . commandWorked ( coll . insert ( {
14+ _id : i ,
15+ x : i ,
16+ y : "c" ,
17+ "a$b" : "foo" ,
18+ "a.b" : "bar" ,
19+ "a.$b" : 5 ,
20+ ".xy" : i ,
21+ ".$xz" : i ,
22+ "..zz" : i ,
23+ c : { d : "x" } ,
24+ } ) ) ;
25+ }
26+
27+ // Test that $getField fails with the provided 'code' for invalid arguments 'getFieldArgs'.
28+ function assertGetFieldFailedWithCode ( getFieldArgs , code ) {
29+ const error =
30+ assert . throws ( ( ) => coll . aggregate ( [ { $project : { test : { $getField : getFieldArgs } } } ] ) ) ;
31+ assert . commandFailedWithCode ( error , code ) ;
32+ }
33+
34+ // Test that $getField returns the 'expected' results for the given arguments 'getFieldArgs'.
35+ function assertGetFieldResultsEq ( getFieldArgs , expected ) {
36+ assertPipelineResultsEq ( [ { $project : { _id : 1 , test : { $getField : getFieldArgs } } } ] , expected ) ;
37+ }
38+
39+ // Test the given 'pipeline' returns the 'expected' results.
40+ function assertPipelineResultsEq ( pipeline , expected ) {
41+ const actual = coll . aggregate ( pipeline ) . toArray ( ) ;
42+ assertArrayEq ( { actual, expected} ) ;
43+ }
44+
45+ const isDotsAndDollarsEnabled = db . adminCommand ( { getParameter : 1 , featureFlagDotsAndDollars : 1 } )
46+ . featureFlagDotsAndDollars . value ;
47+
48+ if ( ! isDotsAndDollarsEnabled ) {
49+ // Verify that $getField is not available if the feature flag is set to false and don't
50+ // run the rest of the test.
51+ assertGetFieldFailedWithCode ( { field : "a" , from : { a : "b" } } , 31325 ) ;
52+ return ;
53+ }
54+
55+ // Test that $getField fails with a document missing named arguments.
56+ assertGetFieldFailedWithCode ( { from : { a : "b" } } , 3041702 ) ;
57+ assertGetFieldFailedWithCode ( { field : "a" } , 3041703 ) ;
58+
59+ // Test that $getField fails with a document with one or more arguments of incorrect type.
60+ assertGetFieldFailedWithCode ( { field : true , from : { a : "b" } } , 3041704 ) ;
61+ assertGetFieldFailedWithCode ( { field : { "a" : 1 } , from : { "a" : 1 } } , 3041704 ) ;
62+ assertGetFieldFailedWithCode ( { field : "a" , from : true } , 3041705 ) ;
63+ assertGetFieldFailedWithCode ( 5 , 3041704 ) ;
64+ assertGetFieldFailedWithCode ( true , 3041704 ) ;
65+ assertGetFieldFailedWithCode ( { $add : [ 2 , 3 ] } , 3041704 ) ;
66+
67+ // Test that $getField fails with a document with invalid arguments.
68+ assertGetFieldFailedWithCode ( { field : "a" , from : { a : "b" } , unknown : true } , 3041701 ) ;
69+
70+ // Test that $getField returns the correct value from the provided object.
71+ assertGetFieldResultsEq ( { field : "a" , from : { a : "b" } } , [ { _id : 0 , test : "b" } , { _id : 1 , test : "b" } ] ) ;
72+
73+ // Test that $getField returns the correct value from the $$ROOT object.
74+ assertGetFieldResultsEq ( null , [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
75+ assertGetFieldResultsEq ( "a" , [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
76+ assertGetFieldResultsEq ( "a$b" , [ { _id : 0 , test : "foo" } , { _id : 1 , test : "foo" } ] ) ;
77+ assertGetFieldResultsEq ( "a.b" , [ { _id : 0 , test : "bar" } , { _id : 1 , test : "bar" } ] ) ;
78+ assertGetFieldResultsEq ( "x" , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
79+ assertGetFieldResultsEq ( "a.$b" , [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
80+ assertGetFieldResultsEq ( ".xy" , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
81+ assertGetFieldResultsEq ( ".$xz" , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
82+ assertGetFieldResultsEq ( "..zz" , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
83+
84+ // Test that $getField returns the correct value from the $$ROOT object when field is an expression.
85+ assertGetFieldResultsEq ( { $concat : [ "a" , "b" ] } ,
86+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
87+ assertGetFieldResultsEq ( { $concat : [ "a" , { $const : "$" } , "b" ] } ,
88+ [ { _id : 0 , test : "foo" } , { _id : 1 , test : "foo" } ] ) ;
89+ assertGetFieldResultsEq ( { $cond : [ true , null , "x" ] } , [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
90+ assertGetFieldResultsEq ( { $cond : [ false , null , "x" ] } , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
91+
92+ // Test that $getField treats dotted fields as key literals instead of field paths. Note that it is
93+ // necessary to use $const in places, otherwise object field validation would reject some of these
94+ // field names.
95+ assertGetFieldResultsEq ( { field : "a.b" , from : { $const : { "a.b" : "b" } } } ,
96+ [ { _id : 0 , test : "b" } , { _id : 1 , test : "b" } ] ) ;
97+ assertGetFieldResultsEq ( { field : ".ab" , from : { $const : { ".ab" : "b" } } } ,
98+ [ { _id : 0 , test : "b" } , { _id : 1 , test : "b" } ] ) ;
99+ assertGetFieldResultsEq ( { field : "ab." , from : { $const : { "ab." : "b" } } } ,
100+ [ { _id : 0 , test : "b" } , { _id : 1 , test : "b" } ] ) ;
101+ assertGetFieldResultsEq ( { field : "a.b.c" , from : { $const : { "a.b.c" : 5 } } } ,
102+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
103+ assertGetFieldResultsEq ( { field : "a.b.c" , from : { a : { b : { c : 5 } } } } ,
104+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
105+ assertGetFieldResultsEq ( { field : { $concat : [ "a.b" , "." , "c" ] } , from : { $const : { "a.b.c" : 5 } } } ,
106+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
107+ assertGetFieldResultsEq ( { field : { $concat : [ "a.b" , "." , "c" ] } , from : { a : { b : { c : 5 } } } } ,
108+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
109+
110+ // Test that $getField works with fields that contain '$'.
111+ assertGetFieldResultsEq ( { field : "a$b" , from : { "a$b" : "b" } } ,
112+ [ { _id : 0 , test : "b" } , { _id : 1 , test : "b" } ] ) ;
113+ assertGetFieldResultsEq ( { field : "a$b.b" , from : { $const : { "a$b.b" : 5 } } } ,
114+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
115+ assertGetFieldResultsEq ( { field : { $const : "a$b.b" } , from : { $const : { "a$b.b" : 5 } } } ,
116+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
117+ assertGetFieldResultsEq ( { field : { $const : "$b.b" } , from : { $const : { "$b.b" : 5 } } } ,
118+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
119+ assertGetFieldResultsEq ( { field : { $const : "$b" } , from : { $const : { "$b" : 5 } } } ,
120+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
121+ assertGetFieldResultsEq ( { field : { $const : "$.ab" } , from : { $const : { "$.ab" : 5 } } } ,
122+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
123+ assertGetFieldResultsEq ( { field : { $const : "$$xz" } , from : { $const : { "$$xz" : 5 } } } ,
124+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
125+
126+ // Test null and missing cases.
127+ assertGetFieldResultsEq ( { field : "a" , from : null } , [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
128+ assertGetFieldResultsEq ( { field : null , from : { a : 1 } } , [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
129+ assertGetFieldResultsEq ( { field : "a" , from : { b : 2 , c : 3 } } ,
130+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
131+ assertGetFieldResultsEq ( { field : "a" , from : { a : null , b : 2 , c : 3 } } ,
132+ [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
133+ assertGetFieldResultsEq ( { field : { $const : "$a" } , from : { $const : { "$a" : null , b : 2 , c : 3 } } } ,
134+ [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
135+ assertGetFieldResultsEq ( { field : "a" , from : { } } ,
136+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
137+
138+ // These should return null because "$a.b" evaluates to a field path expression which returns a
139+ // nullish value (so the expression should return null), as there is no $a.b field path.
140+ assertGetFieldResultsEq ( { field : "$a.b" , from : { $const : { "$a.b" : 5 } } } ,
141+ [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
142+ assertGetFieldResultsEq ( "$a.b" , [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
143+
144+ // When the field path does actually resolve to a field, the value of that field should be used.
145+
146+ // The fieldpath $y resolves to "c" in $$ROOT.
147+ assertGetFieldResultsEq ( { field : "$y" , from : { $const : { "c" : 5 } } } ,
148+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
149+ assertGetFieldResultsEq ( { field : "$y" , from : { $const : { "a" : 5 } } } ,
150+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
151+ assertGetFieldResultsEq ( "$y" , [ { _id : 0 , test : { d : "x" } } , { _id : 1 , test : { d : "x" } } ] ) ;
152+
153+ // The fieldpath $c.d resolves to "x" in $$ROOT.
154+ assertGetFieldResultsEq ( { field : "$c.d" , from : { $const : { "x" : 5 } } } ,
155+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
156+ assertGetFieldResultsEq ( { field : "$c.d" , from : { $const : { "y" : 5 } } } ,
157+ [ { _id : 0 } , { _id : 1 } ] ) ; // The test field should evaluate to missing.
158+ assertGetFieldResultsEq ( "$c.d" , [ { _id : 0 , test : 0 } , { _id : 1 , test : 1 } ] ) ;
159+
160+ // $x resolves to a number, so this should fail.
161+ assertGetFieldFailedWithCode ( { field : "$x" , from : { $const : { "c" : 5 } } } , 3041704 ) ;
162+ assertGetFieldFailedWithCode ( "$x" , 3041704 ) ;
163+
164+ // Test case where $getField stages are nested.
165+ assertGetFieldResultsEq (
166+ { field : "a" , from : { $getField : { field : "b.c" , from : { $const : { "b.c" : { a : 5 } } } } } } ,
167+ [ { _id : 0 , test : 5 } , { _id : 1 , test : 5 } ] ) ;
168+ assertGetFieldResultsEq (
169+ { field : "x" , from : { $getField : { field : "b.c" , from : { $const : { "b.c" : { a : 5 } } } } } } ,
170+ [ { _id : 0 } , { _id : 1 } ] ) ;
171+ assertGetFieldResultsEq (
172+ { field : "a" , from : { $getField : { field : "b.d" , from : { $const : { "b.c" : { a : 5 } } } } } } ,
173+ [ { _id : 0 , test : null } , { _id : 1 , test : null } ] ) ;
174+
175+ // Test case when a dotted/dollar path is within an array.
176+ assertGetFieldResultsEq ( {
177+ field : { $const : "a$b" } ,
178+ from : { $arrayElemAt : [ [ { $const : { "a$b" : 1 } } , { $const : { "a$b" : 2 } } ] , 0 ] }
179+ } ,
180+ [ { _id : 0 , test : 1 } , { _id : 1 , test : 1 } ] ) ;
181+ assertGetFieldResultsEq ( {
182+ field : { $const : "a.." } ,
183+ from : { $arrayElemAt : [ [ { $const : { "a.." : 1 } } , { $const : { "a.." : 2 } } ] , 1 ] }
184+ } ,
185+ [ { _id : 0 , test : 2 } , { _id : 1 , test : 2 } ] ) ;
186+
187+ // Test $getField expression with other pipeline stages.
188+
189+ assertPipelineResultsEq (
190+ [
191+ { $match : { $expr : { $eq : [ { $getField : "_id" } , { $getField : ".$xz" } ] } } } ,
192+ { $project : { aa : { $getField : ".$xz" } , "_id" : 1 } } ,
193+ ] ,
194+ [ { _id : 0 , aa : 0 } , { _id : 1 , aa : 1 } ] ) ;
195+
196+ assertPipelineResultsEq ( [ { $match : { $expr : { $ne : [ { $getField : "_id" } , { $getField : ".$xz" } ] } } } ] , [ ] ) ;
197+ assertPipelineResultsEq (
198+ [
199+ { $match : { $expr : { $ne : [ { $getField : "_id" } , { $getField : "a.b" } ] } } } ,
200+ { $project : { "a" : { $getField : "x" } , "b" : { $getField : { $const : "a.b" } } } }
201+ ] ,
202+ [ { _id : 0 , a : 0 , b : "bar" } , { _id : 1 , a : 1 , b : "bar" } ] ) ;
203+
204+ assertPipelineResultsEq (
205+ [
206+ { $addFields : { aa : { $getField : { $const : "a.b" } } } } ,
207+ { $project : { aa : 1 , _id : 1 } } ,
208+ ] ,
209+ [ { _id : 0 , aa : "bar" } , { _id : 1 , aa : "bar" } ] ) ;
210+
211+ assertPipelineResultsEq (
212+ [
213+ { $bucket : { groupBy : { $getField : { $const : "a.b" } } , boundaries : [ "aaa" , "bar" , "zzz" ] } }
214+ ] , // We should get one bucket here ("bar") with two documents.
215+ [ { _id : "bar" , count : 2 } ] ) ;
216+ assertPipelineResultsEq ( [ {
217+ $bucket : { groupBy : { $getField : "x" } , boundaries : [ 0 , 1 , 2 , 3 , 4 ] }
218+ } ] , // We should get two buckets here for the two possible values of x.
219+ [ { _id : 0 , count : 1 } , { _id : 1 , count : 1 } ] ) ;
220+ } ) ( ) ;
0 commit comments