2626 * it in the license file.
2727 */
2828
29+ #include " mongo/platform/basic.h"
30+
2931#include " mongo/db/pipeline/document_source.h"
3032
3133namespace mongo {
3234
3335using boost::intrusive_ptr;
3436using std::pair;
37+ using std::string;
3538using std::vector;
3639
3740REGISTER_DOCUMENT_SOURCE (bucketAuto, DocumentSourceBucketAuto::createFromBson);
@@ -103,6 +106,25 @@ Value DocumentSourceBucketAuto::extractKey(const Document& doc) {
103106 _variables->setRoot (doc);
104107 Value key = _groupByExpression->evaluate (_variables.get ());
105108
109+ if (_granularityRounder) {
110+ uassert (40258 ,
111+ str::stream () << " $bucketAuto can specify a 'granularity' with numeric boundaries "
112+ " only, but found a value with type: "
113+ << typeName (key.getType ()),
114+ key.numeric ());
115+
116+ double keyValue = key.coerceToDouble ();
117+ uassert (
118+ 40259 ,
119+ " $bucketAuto can specify a 'granularity' with numeric boundaries only, but found a NaN" ,
120+ !std::isnan (keyValue));
121+
122+ uassert (40260 ,
123+ " $bucketAuto can specify a 'granularity' with non-negative numbers only, but found "
124+ " a negative number" ,
125+ keyValue >= 0.0 );
126+ }
127+
106128 // To be consistent with the $group stage, we consider "missing" to be equivalent to null when
107129 // grouping values into buckets.
108130 return key.missing () ? Value (BSONNULL) : std::move (key);
@@ -186,21 +208,48 @@ void DocumentSourceBucketAuto::populateBuckets() {
186208 ? boost::optional<pair<Value, Document>>(_sortedInput->next ())
187209 : boost::none;
188210
189- // If there are any more values that are equal to the boundary value, then absorb them
190- // into the current bucket too.
191- while (nextValue &&
192- pExpCtx->getValueComparator ().evaluate (currentBucket._max == nextValue->first )) {
193- addDocumentToBucket (*nextValue, currentBucket);
194- nextValue = _sortedInput->more ()
195- ? boost::optional<pair<Value, Document>>(_sortedInput->next ())
196- : boost::none;
211+ if (_granularityRounder) {
212+ Value boundaryValue = _granularityRounder->roundUp (currentBucket._max );
213+ // If there are any values that now fall into this bucket after we round the
214+ // boundary, absorb them into this bucket too.
215+ while (nextValue &&
216+ pExpCtx->getValueComparator ().evaluate (boundaryValue > nextValue->first )) {
217+ addDocumentToBucket (*nextValue, currentBucket);
218+ nextValue = _sortedInput->more ()
219+ ? boost::optional<pair<Value, Document>>(_sortedInput->next ())
220+ : boost::none;
221+ }
222+ if (nextValue) {
223+ currentBucket._max = boundaryValue;
224+ }
225+ } else {
226+ // If there are any more values that are equal to the boundary value, then absorb
227+ // them into the current bucket too.
228+ while (nextValue &&
229+ pExpCtx->getValueComparator ().evaluate (currentBucket._max ==
230+ nextValue->first )) {
231+ addDocumentToBucket (*nextValue, currentBucket);
232+ nextValue = _sortedInput->more ()
233+ ? boost::optional<pair<Value, Document>>(_sortedInput->next ())
234+ : boost::none;
235+ }
197236 }
198237 firstEntryInNextBucket = nextValue;
199238 }
200239
201240 // Add the current bucket to the vector of buckets.
202241 addBucket (currentBucket);
203242 }
243+
244+ if (!_buckets.empty () && _granularityRounder) {
245+ // If we we have a granularity, we round the first bucket's minimum down and the last
246+ // bucket's maximum up. This way all of the bucket boundaries are rounded to numbers in the
247+ // granularity specification.
248+ Bucket& firstBucket = _buckets.front ();
249+ Bucket& lastBucket = _buckets.back ();
250+ firstBucket._min = _granularityRounder->roundDown (firstBucket._min );
251+ lastBucket._max = _granularityRounder->roundUp (lastBucket._max );
252+ }
204253}
205254
206255DocumentSourceBucketAuto::Bucket::Bucket (Value min,
@@ -213,14 +262,33 @@ DocumentSourceBucketAuto::Bucket::Bucket(Value min,
213262 }
214263}
215264
216- void DocumentSourceBucketAuto::addBucket (const Bucket& newBucket) {
217- // If there is a bucket that comes before the new bucket being added, then the previous bucket's
218- // max boundary is updated to the new bucket's min. This is makes it so that buckets' min
219- // boundaries are inclusive and max boundaries are exclusive (except for the last bucket, which
220- // has an inclusive max).
265+ void DocumentSourceBucketAuto::addBucket (Bucket& newBucket) {
221266 if (!_buckets.empty ()) {
222267 Bucket& previous = _buckets.back ();
223- previous._max = newBucket._min ;
268+ if (_granularityRounder) {
269+ // If we have a granularity specified and if there is a bucket that comes before the new
270+ // bucket being added, then the new bucket's min boundary is updated to be the
271+ // previous bucket's max boundary. This makes it so that bucket boundaries follow the
272+ // granularity, have inclusive minimums, and have exclusive maximums.
273+
274+ double prevMax = previous._max .coerceToDouble ();
275+ if (prevMax == 0.0 ) {
276+ // Handle the special case where the largest value in the first bucket is zero. In
277+ // this case, we take the minimum boundary of the second bucket and round it down.
278+ // We then set the maximum boundary of the first bucket to be the rounded down
279+ // value. This maintains that the maximum boundary of the first bucket is exclusive
280+ // and the minimum boundary of the second bucket is inclusive.
281+ previous._max = _granularityRounder->roundDown (newBucket._min );
282+ }
283+
284+ newBucket._min = previous._max ;
285+ } else {
286+ // If there is a bucket that comes before the new bucket being added, then the previous
287+ // bucket's max boundary is updated to the new bucket's min. This makes it so that
288+ // buckets' min boundaries are inclusive and max boundaries are exclusive (except for
289+ // the last bucket, which has an inclusive max).
290+ previous._max = newBucket._min ;
291+ }
224292 }
225293 _buckets.push_back (newBucket);
226294}
@@ -254,6 +322,10 @@ Value DocumentSourceBucketAuto::serialize(bool explain) const {
254322 insides[" groupBy" ] = _groupByExpression->serialize (explain);
255323 insides[" buckets" ] = Value (_nBuckets);
256324
325+ if (_granularityRounder) {
326+ insides[" granularity" ] = Value (_granularityRounder->getName ());
327+ }
328+
257329 const size_t nOutputFields = _fieldNames.size ();
258330 MutableDocument outputSpec (nOutputFields);
259331 for (size_t i = 0 ; i < nOutputFields; i++) {
@@ -263,8 +335,6 @@ Value DocumentSourceBucketAuto::serialize(bool explain) const {
263335 }
264336 insides[" output" ] = outputSpec.freezeToValue ();
265337
266- // TODO SERVER-24152: handle granularity field
267-
268338 return Value{Document{{getSourceName (), insides.freezeToValue ()}}};
269339}
270340
@@ -304,6 +374,10 @@ void DocumentSourceBucketAuto::addAccumulator(StringData fieldName,
304374 _expressions.push_back (expression);
305375}
306376
377+ void DocumentSourceBucketAuto::setGranularity (string granularity) {
378+ _granularityRounder = GranularityRounder::getGranularityRounder (std::move (granularity));
379+ }
380+
307381intrusive_ptr<DocumentSource> DocumentSourceBucketAuto::createFromBson (
308382 BSONElement elem, const intrusive_ptr<ExpressionContext>& pExpCtx) {
309383 uassert (40240 ,
@@ -363,11 +437,16 @@ intrusive_ptr<DocumentSource> DocumentSourceBucketAuto::createFromBson(
363437
364438 bucketAuto->addAccumulator (fieldName, factory, accExpression);
365439 }
440+ } else if (" granularity" == argName) {
441+ uassert (40261 ,
442+ str::stream ()
443+ << " The $bucketAuto 'granularity' field must be a string, but found type: "
444+ << typeName (argument.type ()),
445+ argument.type () == BSONType::String);
446+ bucketAuto->setGranularity (argument.str ());
366447 } else {
367448 uasserted (40245 , str::stream () << " Unrecognized option to $bucketAuto: " << argName);
368449 }
369-
370- // TODO SERVER-24152: handle granularity field
371450 }
372451
373452 uassert (40246 ,
0 commit comments