1 /**
2 MongoCollection class
3
4 Copyright: © 2012-2016 Sönke Ludwig
5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 Authors: Sönke Ludwig
7 */
8 module vibe.db.mongo.collection;
9
10 public import vibe.db.mongo.cursor;
11 public import vibe.db.mongo.connection;
12 public import vibe.db.mongo.flags;
13
14 public import vibe.db.mongo.impl.index;
15 public import vibe.db.mongo.impl.crud;
16
17 import vibe.core.log;
18 import vibe.db.mongo.client;
19
20 import core.time;
21 import std.algorithm : among, countUntil, find, findSplit;
22 import std.array;
23 import std.conv;
24 import std.exception;
25 import std.meta : AliasSeq;
26 import std.string;
27 import std.typecons : Nullable, tuple, Tuple;
28
29
30 /**
31 Represents a single collection inside a MongoDB.
32
33 All methods take arbitrary types for Bson arguments. serializeToBson() is implicitly called on
34 them before they are send to the database. The following example shows some possible ways
35 to specify objects.
36 */
37 struct MongoCollection {
38 private {
39 MongoClient m_client;
40 MongoDatabase m_db;
41 string m_name;
42 string m_fullPath;
43 }
44
45 this(MongoClient client, string fullPath)
46 @safe {
47 assert(client !is null);
48 m_client = client;
49
50 auto dotidx = fullPath.indexOf('.');
51 assert(dotidx > 0, "The collection name passed to MongoCollection must be of the form \"dbname.collectionname\".");
52
53 m_fullPath = fullPath;
54 m_db = m_client.getDatabase(fullPath[0 .. dotidx]);
55 m_name = fullPath[dotidx+1 .. $];
56 }
57
58 this(ref MongoDatabase db, string name)
59 @safe {
60 assert(db.client !is null);
61 m_client = db.client;
62 m_fullPath = db.name ~ "." ~ name;
63 m_db = db;
64 m_name = name;
65 }
66
67 /**
68 Returns: Root database to which this collection belongs.
69 */
70 @property MongoDatabase database() @safe { return m_db; }
71
72 /**
73 Returns: Name of this collection (excluding the database name).
74 */
75 @property string name() const @safe { return m_name; }
76
77 /**
78 Performs an update operation on documents matching 'selector', updating them with 'update'.
79
80 Throws: Exception if a DB communication error occurred.
81 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Updating)
82 */
83 deprecated("Use `replaceOne`, `updateOne` or `updateMany` taking `UpdateOptions` instead, this method breaks in MongoDB 5.1 and onwards.")
84 void update(T, U)(T selector, U update, UpdateFlags flags = UpdateFlags.None)
85 {
86 assert(m_client !is null, "Updating uninitialized MongoCollection.");
87 auto conn = m_client.lockConnection();
88 ubyte[256] selector_buf = void, update_buf = void;
89 conn.update(m_fullPath, flags, serializeToBson(selector, selector_buf), serializeToBson(update, update_buf));
90 }
91
92 /**
93 Inserts new documents into the collection.
94
95 Note that if the `_id` field of the document(s) is not set, typically
96 using `BsonObjectID.generate()`, the server will generate IDs
97 automatically. If you need to know the IDs of the inserted documents,
98 you need to generate them locally.
99
100 Throws: Exception if a DB communication error occurred.
101 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Inserting)
102 */
103 deprecated("Use `insertOne` or `insertMany`, this method breaks in MongoDB 5.1 and onwards.")
104 void insert(T)(T document_or_documents, InsertFlags flags = InsertFlags.None)
105 {
106 assert(m_client !is null, "Inserting into uninitialized MongoCollection.");
107 auto conn = m_client.lockConnection();
108 Bson[] docs;
109 Bson bdocs = () @trusted { return serializeToBson(document_or_documents); } ();
110 if( bdocs.type == Bson.Type.Array ) docs = cast(Bson[])bdocs;
111 else docs = () @trusted { return (&bdocs)[0 .. 1]; } ();
112 conn.insert(m_fullPath, flags, docs);
113 }
114
115 /**
116 Inserts the provided document(s). If a document is missing an identifier,
117 one is generated automatically by vibe.d.
118
119 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/#mongodb-method-db.collection.insertOne)
120
121 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/insert/)
122 */
123 InsertOneResult insertOne(T)(T document, InsertOneOptions options = InsertOneOptions.init)
124 {
125 assert(m_client !is null, "Querying uninitialized MongoCollection.");
126
127 Bson cmd = Bson.emptyObject; // empty object because order is important
128 cmd["insert"] = Bson(m_name);
129 auto doc = serializeToBson(document);
130 enforce(doc.type == Bson.Type.object, "Can only insert objects into collections");
131 InsertOneResult res;
132 if ("_id" !in doc.get!(Bson[string]))
133 {
134 doc["_id"] = Bson(res.insertedId = BsonObjectID.generate);
135 }
136 cmd["documents"] = Bson([doc]);
137 MongoConnection conn = m_client.lockConnection();
138 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
139 foreach (string k, v; serializeToBson(options).byKeyValue)
140 cmd[k] = v;
141
142 database.runCommandChecked(cmd);
143 return res;
144 }
145
146 /// ditto
147 InsertOneResult insertMany(T)(T[] documents, InsertManyOptions options = InsertManyOptions.init)
148 {
149 assert(m_client !is null, "Querying uninitialized MongoCollection.");
150
151 Bson cmd = Bson.emptyObject; // empty object because order is important
152 cmd["insert"] = Bson(m_name);
153 Bson[] arr = new Bson[documents.length];
154 BsonObjectID[size_t] insertedIds;
155 foreach (i, document; documents)
156 {
157 auto doc = serializeToBson(document);
158 arr[i] = doc;
159 enforce(doc.type == Bson.Type.object, "Can only insert objects into collections");
160 if ("_id" !in doc.get!(Bson[string]))
161 {
162 doc["_id"] = Bson(insertedIds[i] = BsonObjectID.generate);
163 }
164 }
165 cmd["documents"] = Bson(arr);
166 MongoConnection conn = m_client.lockConnection();
167 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
168 foreach (string k, v; serializeToBson(options).byKeyValue)
169 cmd[k] = v;
170
171 database.runCommandChecked(cmd);
172 return InsertManyResult(insertedIds);
173 }
174
175 /**
176 Deletes at most one document matching the query `filter`. The returned
177 result identifies how many documents have been deleted.
178
179 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteOne/#mongodb-method-db.collection.deleteOne)
180
181 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
182 */
183 DeleteResult deleteOne(T)(T filter, DeleteOptions options = DeleteOptions.init)
184 @trusted {
185 int limit = 1;
186 return deleteImpl([filter], options, (&limit)[0 .. 1]);
187 }
188
189 /**
190 Deletes all documents matching the query `filter`. The returned result
191 identifies how many documents have been deleted.
192
193 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteMany/#mongodb-method-db.collection.deleteMany)
194
195 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
196 */
197 DeleteResult deleteMany(T)(T filter, DeleteOptions options = DeleteOptions.init)
198 @safe
199 if (!is(T == DeleteOptions))
200 {
201 return deleteImpl([filter], options);
202 }
203
204 /**
205 Deletes all documents in the collection. The returned result identifies
206 how many documents have been deleted.
207
208 Same as calling `deleteMany` with `Bson.emptyObject` as filter.
209
210 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
211 */
212 DeleteResult deleteAll(DeleteOptions options = DeleteOptions.init)
213 @safe {
214 return deleteImpl([Bson.emptyObject], options);
215 }
216
217 /// Implementation helper. It's possible to set custom delete limits with
218 /// this method, otherwise it's identical to `deleteOne` and `deleteMany`.
219 DeleteResult deleteImpl(T)(T[] queries, DeleteOptions options = DeleteOptions.init, scope int[] limits = null)
220 @safe {
221 assert(m_client !is null, "Querying uninitialized MongoCollection.");
222
223 alias FieldsMovedIntoChildren = AliasSeq!("limit", "collation", "hint");
224
225 Bson cmd = Bson.emptyObject; // empty object because order is important
226 cmd["delete"] = Bson(m_name);
227
228 MongoConnection conn = m_client.lockConnection();
229 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
230 auto optionsBson = serializeToBson(options);
231 foreach (string k, v; optionsBson.byKeyValue)
232 if (!k.among!FieldsMovedIntoChildren)
233 cmd[k] = v;
234
235 Bson[] deletesBson = new Bson[queries.length];
236 foreach (i, q; queries)
237 {
238 auto deleteBson = Bson.emptyObject;
239 deleteBson["q"] = serializeToBson(q);
240 foreach (string k, v; optionsBson.byKeyValue)
241 if (k.among!FieldsMovedIntoChildren)
242 deleteBson[k] = v;
243 if (i < limits.length)
244 deleteBson["limit"] = Bson(limits[i]);
245 else
246 deleteBson["limit"] = Bson(0);
247 deletesBson[i] = deleteBson;
248 }
249 cmd["deletes"] = Bson(deletesBson);
250
251 auto n = database.runCommandChecked(cmd)["n"].to!long;
252 return DeleteResult(n);
253 }
254
255 /**
256 Replaces at most single document within the collection based on the filter.
257
258 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.replaceOne/#mongodb-method-db.collection.replaceOne)
259
260 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
261 */
262 UpdateResult replaceOne(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
263 @safe {
264 Bson opts = Bson.emptyObject;
265 opts["multi"] = Bson(false);
266 return updateImpl([filter], [replacement], [opts], options, true, false);
267 }
268
269 /**
270 Updates at most single document within the collection based on the filter.
271
272 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/#mongodb-method-db.collection.updateOne)
273
274 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
275 */
276 UpdateResult updateOne(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
277 @safe {
278 Bson opts = Bson.emptyObject;
279 opts["multi"] = Bson(false);
280 return updateImpl([filter], [replacement], [opts], options, false, true);
281 }
282
283 /**
284 Updates all matching document within the collection based on the filter.
285
286 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.updateMany/#mongodb-method-db.collection.updateMany)
287
288 Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
289 */
290 UpdateResult updateMany(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
291 @safe {
292 Bson opts = Bson.emptyObject;
293 opts["multi"] = Bson(true);
294 return updateImpl([filter], [replacement], [opts], options, false, true);
295 }
296
297 /// Implementation helper. It's possible to set custom per-update object
298 /// options with this method, otherwise it's identical to `replaceOne`,
299 /// `updateOne` and `updateMany`.
300 UpdateResult updateImpl(T, U, O)(T[] queries, U[] documents, O[] perUpdateOptions, UpdateOptions options = UpdateOptions.init,
301 bool mustBeDocument = false, bool mustBeModification = false)
302 @safe
303 in(queries.length == documents.length && documents.length == perUpdateOptions.length,
304 "queries, documents and perUpdateOptions must have same length")
305 {
306 assert(m_client !is null, "Querying uninitialized MongoCollection.");
307
308 alias FieldsMovedIntoChildren = AliasSeq!("arrayFilters",
309 "collation",
310 "hint",
311 "upsert");
312
313 Bson cmd = Bson.emptyObject; // empty object because order is important
314 cmd["update"] = Bson(m_name);
315
316 MongoConnection conn = m_client.lockConnection();
317 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
318 auto optionsBson = serializeToBson(options);
319 foreach (string k, v; optionsBson.byKeyValue)
320 if (!k.among!FieldsMovedIntoChildren)
321 cmd[k] = v;
322
323 Bson[] updatesBson = new Bson[queries.length];
324 foreach (i, q; queries)
325 {
326 auto updateBson = Bson.emptyObject;
327 auto qbson = serializeToBson(q);
328 updateBson["q"] = qbson;
329 if (mustBeDocument)
330 {
331 if (qbson.type != Bson.Type.object)
332 assert(false, "Passed in non-document into a place where only replacements are expected. "
333 ~ "Maybe you want to call updateOne or updateMany instead?");
334
335 foreach (string k, v; qbson.byKeyValue)
336 {
337 if (k.startsWith("$"))
338 assert(false, "Passed in atomic modifiers (" ~ k
339 ~ ") into a place where only replacements are expected. "
340 ~ "Maybe you want to call updateOne or updateMany instead?");
341 debug break; // server checks that the rest is consistent (only $ or only non-$ allowed)
342 // however in debug mode we check the full document, as we can give better error messages to the dev
343 }
344 }
345 if (mustBeModification)
346 {
347 if (qbson.type == Bson.Type.object)
348 {
349 bool anyDollar = false;
350 foreach (string k, v; qbson.byKeyValue)
351 {
352 if (k.startsWith("$"))
353 anyDollar = true;
354 debug break; // server checks that the rest is consistent (only $ or only non-$ allowed)
355 // however in debug mode we check the full document, as we can give better error messages to the dev
356 // also nice side effect: if this is an empty document, this also matches the assert(false) branch.
357 }
358
359 if (!anyDollar)
360 assert(false, "Passed in a regular document into a place where only updates are expected. "
361 ~ "Maybe you want to call replaceOne instead? "
362 ~ "(this update call would otherwise replace the entire matched object with the passed in update object)");
363 }
364 }
365 updateBson["u"] = serializeToBson(documents[i]);
366 foreach (string k, v; optionsBson.byKeyValue)
367 if (k.among!FieldsMovedIntoChildren)
368 updateBson[k] = v;
369 foreach (string k, v; perUpdateOptions[i].byKeyValue)
370 updateBson[k] = v;
371 updatesBson[i] = updateBson;
372 }
373 cmd["updates"] = Bson(updatesBson);
374
375 auto res = database.runCommandChecked(cmd);
376 auto ret = UpdateResult(
377 res["n"].to!long,
378 res["nModified"].to!long,
379 );
380 auto upserted = res["upserted"].opt!(Bson[]);
381 if (upserted.length)
382 {
383 ret.upsertedIds.length = upserted.length;
384 foreach (i, id; upserted)
385 ret.upsertedIds[i] = id.get!BsonObjectID;
386 }
387 return ret;
388 }
389
390 deprecated("Use the overload taking `FindOptions` instead, this method breaks in MongoDB 5.1 and onwards. Note: using a `$query` / `query` member to override the query arguments is no longer supported in the new overload.")
391 MongoCursor!R find(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags, int num_skip = 0, int num_docs_per_chunk = 0)
392 {
393 assert(m_client !is null, "Querying uninitialized MongoCollection.");
394 return MongoCursor!R(m_client, m_fullPath, flags, num_skip, num_docs_per_chunk, query, returnFieldSelector);
395 }
396
397 ///
398 @safe deprecated unittest {
399 import vibe.db.mongo.mongo;
400
401 void test()
402 {
403 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
404 // find documents with status == "A"
405 auto x = coll.find(["status": "A"], ["status": true], QueryFlags.none);
406 foreach (item; x)
407 {
408 // only for legacy overload
409 }
410 }
411 }
412
413 /**
414 Queries the collection for existing documents, limiting what fields are
415 returned by the database. (called projection)
416
417 See_Also:
418 - Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying)
419 - Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections)
420 - $(LREF findOne)
421 */
422 MongoCursor!R find(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init)
423 if (!is(U == FindOptions))
424 {
425 options.projection = serializeToBson(projection);
426 return find(query, options);
427 }
428
429 ///
430 @safe unittest {
431 import vibe.db.mongo.mongo;
432
433 void test()
434 {
435 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
436 // find documents with status == "A", return list of {"item":..., "status":...}
437 coll.find(["status": "A"], ["item": 1, "status": 1]);
438 }
439 }
440
441 /**
442 Queries the collection for existing documents.
443
444 If no arguments are passed to find(), all documents of the collection will be returned.
445
446 See_Also:
447 - $(LINK http://www.mongodb.org/display/DOCS/Querying)
448 - $(LREF findOne)
449 */
450 MongoCursor!R find(R = Bson, Q)(Q query, FindOptions options = FindOptions.init)
451 {
452 return MongoCursor!R(m_client, m_db.name, m_name, query, options);
453 }
454
455 ///
456 @safe unittest {
457 import vibe.db.mongo.mongo;
458
459 void test()
460 {
461 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
462 // find documents with status == "A"
463 coll.find(["status": "A"]);
464 }
465 }
466
467 /**
468 Queries all documents of the collection.
469
470 See_Also:
471 - $(LINK http://www.mongodb.org/display/DOCS/Querying)
472 - $(LREF findOne)
473 */
474 MongoCursor!R find(R = Bson)() { return find!R(Bson.emptyObject, FindOptions.init); }
475
476 ///
477 @safe unittest {
478 import vibe.db.mongo.mongo;
479
480 void test()
481 {
482 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
483 // find all documents in the "test" collection.
484 coll.find();
485 }
486 }
487
488 deprecated("Use the overload taking `FindOptions` instead, this method breaks in MongoDB 5.1 and onwards. Note: using a `$query` / `query` member to override the query arguments is no longer supported in the new overload.")
489 auto findOne(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags)
490 {
491 import std.traits;
492 import std.typecons;
493
494 auto c = find!R(query, returnFieldSelector, flags, 0, 1);
495 static if (is(R == Bson)) {
496 foreach (doc; c) return doc;
497 return Bson(null);
498 } else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
499 foreach (doc; c) return doc;
500 return null;
501 } else {
502 foreach (doc; c) {
503 Nullable!R ret;
504 ret = doc;
505 return ret;
506 }
507 return Nullable!R.init;
508 }
509 }
510
511 /** Queries the collection for existing documents.
512
513 Returns:
514 By default, a Bson value of the matching document is returned, or $(D Bson(null))
515 when no document matched. For types R that are not Bson, the returned value is either
516 of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
517
518 The projection parameter limits what fields are returned by the database,
519 see projection documentation linked below.
520
521 Throws: Exception if a DB communication error or a query error occurred.
522
523 See_Also:
524 - Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying)
525 - Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections)
526 - $(LREF find)
527 */
528 auto findOne(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init)
529 if (!is(U == FindOptions))
530 {
531 options.projection = serializeToBson(projection);
532 return findOne!(R, T)(query, options);
533 }
534
535 ///
536 @safe unittest {
537 import vibe.db.mongo.mongo;
538
539 void test()
540 {
541 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
542 // find documents with status == "A"
543 auto x = coll.findOne(["status": "A"], ["status": true, "otherField": true]);
544 // x now only contains _id (implicit, unless you make it `false`), status and otherField
545 }
546 }
547
548 /** Queries the collection for existing documents.
549
550 Returns:
551 By default, a Bson value of the matching document is returned, or $(D Bson(null))
552 when no document matched. For types R that are not Bson, the returned value is either
553 of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
554
555 Throws: Exception if a DB communication error or a query error occurred.
556 See_Also:
557 - $(LINK http://www.mongodb.org/display/DOCS/Querying)
558 - $(LREF find)
559 */
560 auto findOne(R = Bson, T)(T query, FindOptions options = FindOptions.init)
561 {
562 import std.traits;
563 import std.typecons;
564
565 options.limit = 1;
566 auto c = find!R(query, options);
567 static if (is(R == Bson)) {
568 foreach (doc; c) return doc;
569 return Bson(null);
570 } else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
571 foreach (doc; c) return doc;
572 return null;
573 } else {
574 foreach (doc; c) {
575 Nullable!R ret;
576 ret = doc;
577 return ret;
578 }
579 return Nullable!R.init;
580 }
581 }
582
583 /**
584 Removes documents from the collection.
585
586 Throws: Exception if a DB communication error occurred.
587 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Removing)
588 */
589 deprecated("Use `deleteOne` or `deleteMany` taking DeleteOptions instead, this method breaks in MongoDB 5.1 and onwards.")
590 void remove(T)(T selector, DeleteFlags flags = DeleteFlags.None)
591 {
592 assert(m_client !is null, "Removing from uninitialized MongoCollection.");
593 auto conn = m_client.lockConnection();
594 ubyte[256] selector_buf = void;
595 conn.delete_(m_fullPath, flags, serializeToBson(selector, selector_buf));
596 }
597
598 /// ditto
599 deprecated("Use `deleteMany` taking `DeleteOptions` instead, this method breaks in MongoDB 5.1 and onwards.")
600 void remove()() { remove(Bson.emptyObject); }
601
602 /**
603 Combines a modify and find operation to a single atomic operation.
604
605 Params:
606 query = MongoDB query expression to identify the matched document
607 update = Update expression for the matched document
608 returnFieldSelector = Optional map of fields to return in the response
609
610 Throws:
611 An `Exception` will be thrown if an error occurs in the
612 communication with the database server.
613
614 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
615 */
616 Bson findAndModify(T, U, V)(T query, U update, V returnFieldSelector)
617 {
618 static struct CMD {
619 string findAndModify;
620 T query;
621 U update;
622 V fields;
623 }
624 CMD cmd;
625 cmd.findAndModify = m_name;
626 cmd.query = query;
627 cmd.update = update;
628 cmd.fields = returnFieldSelector;
629 auto ret = database.runCommandChecked(cmd);
630 return ret["value"];
631 }
632
633 /// ditto
634 Bson findAndModify(T, U)(T query, U update)
635 {
636 return findAndModify(query, update, null);
637 }
638
639 /**
640 Combines a modify and find operation to a single atomic operation with generic options support.
641
642 Params:
643 query = MongoDB query expression to identify the matched document
644 update = Update expression for the matched document
645 options = Generic BSON object that contains additional options
646 fields, such as `"new": true`
647
648 Throws:
649 An `Exception` will be thrown if an error occurs in the
650 communication with the database server.
651
652 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
653 */
654 Bson findAndModifyExt(T, U, V)(T query, U update, V options)
655 {
656 auto bopt = serializeToBson(options);
657 assert(bopt.type == Bson.Type.object,
658 "The options parameter to findAndModifyExt must be a BSON object.");
659
660 Bson cmd = Bson.emptyObject;
661 cmd["findAndModify"] = m_name;
662 cmd["query"] = serializeToBson(query);
663 cmd["update"] = serializeToBson(update);
664 bopt.opApply(delegate int(string key, Bson value) @safe {
665 cmd[key] = value;
666 return 0;
667 });
668 auto ret = database.runCommandChecked(cmd);
669 return ret["value"];
670 }
671
672 ///
673 @safe unittest {
674 import vibe.db.mongo.mongo;
675
676 void test()
677 {
678 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
679 coll.findAndModifyExt(["name": "foo"], ["$set": ["value": "bar"]], ["new": true]);
680 }
681 }
682
683 deprecated("deprecated since MongoDB v4.0, use countDocuments or estimatedDocumentCount instead")
684 ulong count(T)(T query)
685 {
686 return countImpl!T(query);
687 }
688
689 private ulong countImpl(T)(T query)
690 {
691 Bson cmd = Bson.emptyObject;
692 cmd["count"] = m_name;
693 cmd["query"] = serializeToBson(query);
694 auto reply = database.runCommandChecked(cmd);
695 switch (reply["n"].type) with (Bson.Type) {
696 default: assert(false, "Unsupported data type in BSON reply for COUNT");
697 case double_: return cast(ulong)reply["n"].get!double; // v2.x
698 case int_: return reply["n"].get!int; // v3.x
699 case long_: return reply["n"].get!long; // just in case
700 }
701 }
702
703 /**
704 Returns the count of documents that match the query for a collection or
705 view.
706
707 The method wraps the `$group` aggregation stage with a `$sum` expression
708 to perform the count.
709
710 Throws Exception if a DB communication error occurred.
711
712 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/)
713 */
714 ulong countDocuments(T)(T filter, CountOptions options = CountOptions.init)
715 {
716 // https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
717 Bson[] pipeline = [Bson(["$match": serializeToBson(filter)])];
718 if (!options.skip.isNull)
719 pipeline ~= Bson(["$skip": Bson(options.skip.get)]);
720 if (!options.limit.isNull)
721 pipeline ~= Bson(["$limit": Bson(options.limit.get)]);
722 pipeline ~= Bson(["$group": Bson([
723 "_id": Bson(1),
724 "n": Bson(["$sum": Bson(1)])
725 ])]);
726 AggregateOptions aggOptions;
727 foreach (i, field; options.tupleof)
728 {
729 enum name = CountOptions.tupleof[i].stringof;
730 if (name != "filter" && name != "skip" && name != "limit")
731 __traits(getMember, aggOptions, name) = field;
732 }
733 auto reply = aggregate(pipeline, aggOptions).front;
734 return reply["n"].to!long;
735 }
736
737 /**
738 Returns the count of all documents in a collection or view.
739
740 Throws Exception if a DB communication error occurred.
741
742 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.estimatedDocumentCount/)
743 */
744 ulong estimatedDocumentCount(EstimatedDocumentCountOptions options = EstimatedDocumentCountOptions.init)
745 {
746 // https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
747 MongoConnection conn = m_client.lockConnection();
748 if (conn.description.satisfiesVersion(WireVersion.v49)) {
749 Bson[] pipeline = [
750 Bson(["$collStats": Bson(["count": Bson.emptyObject])]),
751 Bson(["$group": Bson([
752 "_id": Bson(1),
753 "n": Bson(["$sum": Bson("$count")])
754 ])])
755 ];
756 AggregateOptions aggOptions;
757 aggOptions.maxTimeMS = options.maxTimeMS;
758 auto reply = aggregate(pipeline, aggOptions).front;
759 return reply["n"].to!long;
760 } else {
761 return countImpl(null);
762 }
763 }
764
765 /**
766 Calculates aggregate values for the data in a collection.
767
768 Params:
769 pipeline = A sequence of data aggregation processes. These can
770 either be given as separate parameters, or as a single array
771 parameter.
772
773 Returns:
774 Returns the list of documents aggregated by the pipeline. The return
775 value is either a single `Bson` array value or a `MongoCursor`
776 (input range) of the requested document type.
777
778 Throws: Exception if a DB communication error occurred.
779
780 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/db.collection.aggregate)
781 */
782 Bson aggregate(ARGS...)(ARGS pipeline) @safe
783 {
784 import std.traits : isArray;
785
786 static if (ARGS.length == 1 && isArray!(ARGS[0]))
787 auto convPipeline = pipeline;
788 else {
789 static struct Pipeline { @asArray ARGS pipeline; }
790
791 Bson[] convPipeline = serializeToBson(Pipeline(pipeline))["pipeline"].get!(Bson[]);
792 }
793
794 return aggregate(convPipeline, AggregateOptions.init).array.serializeToBson;
795 }
796
797 /// ditto
798 MongoCursor!R aggregate(R = Bson, S = Bson)(S[] pipeline, AggregateOptions options) @safe
799 {
800 assert(m_client !is null, "Querying uninitialized MongoCollection.");
801
802 Bson cmd = Bson.emptyObject; // empty object because order is important
803 cmd["aggregate"] = Bson(m_name);
804 cmd["$db"] = Bson(m_db.name);
805 cmd["pipeline"] = serializeToBson(pipeline);
806 MongoConnection conn = m_client.lockConnection();
807 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
808 foreach (string k, v; serializeToBson(options).byKeyValue)
809 {
810 // spec recommends to omit cursor field when explain is true
811 if (!options.explain.isNull && options.explain.get && k == "cursor")
812 continue;
813 cmd[k] = v;
814 }
815 return MongoCursor!R(m_client, cmd,
816 !options.batchSize.isNull ? options.batchSize.get : 0,
817 !options.maxAwaitTimeMS.isNull ? options.maxAwaitTimeMS.get.msecs
818 : !options.maxTimeMS.isNull ? options.maxTimeMS.get.msecs
819 : Duration.max);
820 }
821
822 /// Example taken from the MongoDB documentation
823 @safe unittest {
824 import vibe.db.mongo.mongo;
825
826 void test() {
827 auto db = connectMongoDB("127.0.0.1").getDatabase("test");
828 auto results = db["coll"].aggregate(
829 ["$match": ["status": "A"]],
830 ["$group": ["_id": Bson("$cust_id"),
831 "total": Bson(["$sum": Bson("$amount")])]],
832 ["$sort": ["total": -1]]);
833 }
834 }
835
836 /// The same example, but using an array of arguments with custom options
837 @safe unittest {
838 import vibe.db.mongo.mongo;
839
840 void test() {
841 auto db = connectMongoDB("127.0.0.1").getDatabase("test");
842
843 Bson[] args;
844 args ~= serializeToBson(["$match": ["status": "A"]]);
845 args ~= serializeToBson(["$group": ["_id": Bson("$cust_id"),
846 "total": Bson(["$sum": Bson("$amount")])]]);
847 args ~= serializeToBson(["$sort": ["total": -1]]);
848
849 AggregateOptions options;
850 options.cursor.batchSize = 10; // pre-fetch the first 10 results
851 auto results = db["coll"].aggregate(args, options);
852 }
853 }
854
855 /**
856 Returns an input range of all unique values for a certain field for
857 records matching the given query.
858
859 Params:
860 fieldName = Name of the field for which to collect unique values
861 query = The query used to select records
862 options = Options to apply
863
864 Returns:
865 An input range with items of type `R` (`Bson` by default) is
866 returned.
867 */
868 auto distinct(R = Bson, Q)(string fieldName, Q query, DistinctOptions options = DistinctOptions.init)
869 {
870 assert(m_client !is null, "Querying uninitialized MongoCollection.");
871
872 Bson cmd = Bson.emptyObject; // empty object because order is important
873 cmd["distinct"] = Bson(m_name);
874 cmd["key"] = Bson(fieldName);
875 cmd["query"] = serializeToBson(query);
876 MongoConnection conn = m_client.lockConnection();
877 enforceWireVersionConstraints(options, conn.description.maxWireVersion);
878 foreach (string k, v; serializeToBson(options).byKeyValue)
879 cmd[k] = v;
880
881 import std.algorithm : map;
882
883 auto res = m_db.runCommandChecked(cmd);
884 static if (is(R == Bson)) return res["values"].byValue;
885 else return res["values"].byValue.map!(b => deserializeBson!R(b));
886 }
887
888 ///
889 @safe unittest {
890 import std.algorithm : equal;
891 import vibe.db.mongo.mongo;
892
893 void test()
894 {
895 auto db = connectMongoDB("127.0.0.1").getDatabase("test");
896 auto coll = db["collection"];
897
898 coll.drop();
899 coll.insertOne(["a": "first", "b": "foo"]);
900 coll.insertOne(["a": "first", "b": "bar"]);
901 coll.insertOne(["a": "first", "b": "bar"]);
902 coll.insertOne(["a": "second", "b": "baz"]);
903 coll.insertOne(["a": "second", "b": "bam"]);
904
905 auto result = coll.distinct!string("b", ["a": "first"]);
906
907 assert(result.equal(["foo", "bar"]));
908 }
909 }
910
911 /*
912 following MongoDB standard API for the Index Management specification:
913
914 Standards: https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/index-management.rst#standard-api
915 */
916
917 deprecated("This is a legacy API, call createIndexes instead")
918 void ensureIndex(scope const(Tuple!(string, int))[] field_orders, IndexFlags flags = IndexFlags.none, Duration expire_time = 0.seconds)
919 @safe {
920 IndexModel[1] models;
921 IndexOptions options;
922 if (flags & IndexFlags.unique) options.unique = true;
923 if (flags & IndexFlags.dropDuplicates) options.dropDups = true;
924 if (flags & IndexFlags.background) options.background = true;
925 if (flags & IndexFlags.sparse) options.sparse = true;
926 if (flags & IndexFlags.expireAfterSeconds) options.expireAfter = expire_time;
927
928 models[0].options = options;
929 foreach (field; field_orders) {
930 models[0].add(field[0], field[1]);
931 }
932 createIndexes(models);
933 }
934
935 deprecated("This is a legacy API, call createIndexes instead. This API is not recommended to be used because of unstable dictionary ordering.")
936 void ensureIndex(int[string] field_orders, IndexFlags flags = IndexFlags.none, ulong expireAfterSeconds = 0)
937 @safe {
938 Tuple!(string, int)[] orders;
939 foreach (k, v; field_orders)
940 orders ~= tuple(k, v);
941 ensureIndex(orders, flags, expireAfterSeconds.seconds);
942 }
943
944 /**
945 Drops a single index from the collection by the index name.
946
947 Throws: `Exception` if it is attempted to pass in `*`.
948 Use dropIndexes() to remove all indexes instead.
949 */
950 void dropIndex(string name, DropIndexOptions options = DropIndexOptions.init)
951 @safe {
952 if (name == "*")
953 throw new Exception("Attempted to remove single index with '*'");
954
955 static struct CMD {
956 string dropIndexes;
957 string index;
958 }
959
960 CMD cmd;
961 cmd.dropIndexes = m_name;
962 cmd.index = name;
963 database.runCommandChecked(cmd);
964 }
965
966 /// ditto
967 void dropIndex(T)(T keys,
968 IndexOptions indexOptions = IndexOptions.init,
969 DropIndexOptions options = DropIndexOptions.init)
970 @safe if (!is(Unqual!T == IndexModel))
971 {
972 IndexModel model;
973 model.keys = serializeToBson(keys);
974 model.options = indexOptions;
975 dropIndex(model.name, options);
976 }
977
978 /// ditto
979 void dropIndex(const IndexModel keys,
980 DropIndexOptions options = DropIndexOptions.init)
981 @safe {
982 dropIndex(keys.name, options);
983 }
984
985 ///
986 @safe unittest
987 {
988 import vibe.db.mongo.mongo;
989
990 void test()
991 {
992 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
993 auto primarykey = IndexModel()
994 .add("name", 1)
995 .add("primarykey", -1);
996 coll.dropIndex(primarykey);
997 }
998 }
999
1000 /// Drops all indexes in the collection.
1001 void dropIndexes(DropIndexOptions options = DropIndexOptions.init)
1002 @safe {
1003 static struct CMD {
1004 string dropIndexes;
1005 string index;
1006 }
1007
1008 CMD cmd;
1009 cmd.dropIndexes = m_name;
1010 cmd.index = "*";
1011 database.runCommandChecked(cmd);
1012 }
1013
1014 /// Unofficial API extension, more efficient multi-index removal on
1015 /// MongoDB 4.2+
1016 void dropIndexes(string[] names, DropIndexOptions options = DropIndexOptions.init)
1017 @safe {
1018 MongoConnection conn = m_client.lockConnection();
1019 if (conn.description.satisfiesVersion(WireVersion.v42)) {
1020 static struct CMD {
1021 string dropIndexes;
1022 string[] index;
1023 }
1024
1025 CMD cmd;
1026 cmd.dropIndexes = m_name;
1027 cmd.index = names;
1028 database.runCommandChecked(cmd);
1029 } else {
1030 foreach (name; names)
1031 dropIndex(name);
1032 }
1033 }
1034
1035 ///
1036 @safe unittest
1037 {
1038 import vibe.db.mongo.mongo;
1039
1040 void test()
1041 {
1042 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1043 coll.dropIndexes(["name_1_primarykey_-1"]);
1044 }
1045 }
1046
1047 /**
1048 Convenience method for creating a single index. Calls `createIndexes`
1049
1050 Supports any kind of document for template parameter T or a IndexModel.
1051
1052 Params:
1053 keys = a IndexModel or type with integer or string fields indicating
1054 index direction or index type.
1055 */
1056 string createIndex(T)(T keys,
1057 IndexOptions indexOptions = IndexOptions.init,
1058 CreateIndexOptions options = CreateIndexOptions.init)
1059 @safe if (!is(Unqual!T == IndexModel))
1060 {
1061 IndexModel[1] model;
1062 model[0].keys = serializeToBson(keys);
1063 model[0].options = indexOptions;
1064 return createIndexes(model[], options)[0];
1065 }
1066
1067 /// ditto
1068 string createIndex(const IndexModel keys,
1069 CreateIndexOptions options = CreateIndexOptions.init)
1070 @safe {
1071 IndexModel[1] model;
1072 model[0] = keys;
1073 return createIndexes(model[], options)[0];
1074 }
1075
1076 ///
1077 @safe unittest
1078 {
1079 import vibe.db.mongo.mongo;
1080
1081 void test()
1082 {
1083 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1084
1085 // simple ascending name, descending primarykey compound-index
1086 coll.createIndex(["name": 1, "primarykey": -1]);
1087
1088 IndexOptions textOptions = {
1089 // pick language from another field called "idioma"
1090 languageOverride: "idioma"
1091 };
1092 auto textIndex = IndexModel()
1093 .withOptions(textOptions)
1094 .add("comments", IndexType.text);
1095 // more complex text index in DB with independent language
1096 coll.createIndex(textIndex);
1097 }
1098 }
1099
1100 /**
1101 Builds one or more indexes in the collection.
1102
1103 See_Also: $(LINK https://docs.mongodb.com/manual/reference/command/createIndexes/)
1104 */
1105 string[] createIndexes(scope const(IndexModel)[] models,
1106 CreateIndexesOptions options = CreateIndexesOptions.init,
1107 string file = __FILE__, size_t line = __LINE__)
1108 @safe {
1109 string[] keys = new string[models.length];
1110
1111 MongoConnection conn = m_client.lockConnection();
1112 if (conn.description.satisfiesVersion(WireVersion.v26)) {
1113 Bson cmd = Bson.emptyObject;
1114 cmd["createIndexes"] = m_name;
1115 Bson[] indexes;
1116 foreach (model; models) {
1117 // trusted to support old compilers which think opt_dup has
1118 // longer lifetime than model.options
1119 IndexOptions opt_dup = (() @trusted => model.options)();
1120 enforceWireVersionConstraints(opt_dup, conn.description.maxWireVersion, file, line);
1121 Bson index = serializeToBson(opt_dup);
1122 index["key"] = model.keys;
1123 index["name"] = model.name;
1124 indexes ~= index;
1125 }
1126 cmd["indexes"] = Bson(indexes);
1127 database.runCommandChecked(cmd);
1128 } else {
1129 foreach (model; models) {
1130 // trusted to support old compilers which think opt_dup has
1131 // longer lifetime than model.options
1132 IndexOptions opt_dup = (() @trusted => model.options)();
1133 enforceWireVersionConstraints(opt_dup, WireVersion.old, file, line);
1134 Bson doc = serializeToBson(opt_dup);
1135 doc["v"] = 1;
1136 doc["key"] = model.keys;
1137 doc["ns"] = m_fullPath;
1138 doc["name"] = model.name;
1139 database["system.indexes"].insertOne(doc);
1140 }
1141 }
1142
1143 return keys;
1144 }
1145
1146 /**
1147 Returns an array that holds a list of documents that identify and describe the existing indexes on the collection.
1148 */
1149 MongoCursor!R listIndexes(R = Bson)()
1150 @safe {
1151 MongoConnection conn = m_client.lockConnection();
1152 if (conn.description.satisfiesVersion(WireVersion.v30)) {
1153 Bson command = Bson.emptyObject;
1154 command["listIndexes"] = Bson(m_name);
1155 command["$db"] = Bson(m_db.name);
1156 return MongoCursor!R(m_client, command);
1157 } else {
1158 throw new MongoDriverException("listIndexes not supported on MongoDB <3.0");
1159 }
1160 }
1161
1162 ///
1163 @safe unittest
1164 {
1165 import vibe.db.mongo.mongo;
1166
1167 void test()
1168 {
1169 auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1170
1171 foreach (index; coll.listIndexes())
1172 logInfo("index %s: %s", index["name"].get!string, index);
1173 }
1174 }
1175
1176 deprecated("Please use the standard API name 'listIndexes'") alias getIndexes = listIndexes;
1177
1178 /**
1179 Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection.
1180 */
1181 void drop()
1182 @safe {
1183 static struct CMD {
1184 string drop;
1185 }
1186
1187 CMD cmd;
1188 cmd.drop = m_name;
1189 database.runCommandChecked(cmd);
1190 }
1191 }
1192
1193 ///
1194 @safe unittest {
1195 import vibe.data.bson;
1196 import vibe.data.json;
1197 import vibe.db.mongo.mongo;
1198
1199 void test()
1200 {
1201 MongoClient client = connectMongoDB("127.0.0.1");
1202 MongoCollection users = client.getCollection("myapp.users");
1203
1204 // canonical version using a Bson object
1205 users.insertOne(Bson(["name": Bson("admin"), "password": Bson("secret")]));
1206
1207 // short version using a string[string] AA that is automatically
1208 // serialized to Bson
1209 users.insertOne(["name": "admin", "password": "secret"]);
1210
1211 // BSON specific types are also serialized automatically
1212 auto uid = BsonObjectID.fromString("507f1f77bcf86cd799439011");
1213 Bson usr = users.findOne(["_id": uid]);
1214
1215 // JSON is another possibility
1216 Json jusr = parseJsonString(`{"name": "admin", "password": "secret"}`);
1217 users.insertOne(jusr);
1218 }
1219 }
1220
1221 /// Using the type system to define a document "schema"
1222 @safe unittest {
1223 import vibe.db.mongo.mongo;
1224 import vibe.data.serialization : name;
1225 import std.typecons : Nullable;
1226
1227 // Nested object within a "User" document
1228 struct Address {
1229 string name;
1230 string street;
1231 int zipCode;
1232 }
1233
1234 // The document structure of the "myapp.users" collection
1235 struct User {
1236 @name("_id") BsonObjectID id; // represented as "_id" in the database
1237 string loginName;
1238 string password;
1239 Address address;
1240 }
1241
1242 void test()
1243 {
1244 MongoClient client = connectMongoDB("127.0.0.1");
1245 MongoCollection users = client.getCollection("myapp.users");
1246
1247 // D values are automatically serialized to the internal BSON format
1248 // upon insertion - see also vibe.data.serialization
1249 User usr;
1250 usr.id = BsonObjectID.generate();
1251 usr.loginName = "admin";
1252 usr.password = "secret";
1253 users.insertOne(usr);
1254
1255 // find supports direct de-serialization of the returned documents
1256 foreach (usr2; users.find!User()) {
1257 logInfo("User: %s", usr2.loginName);
1258 }
1259
1260 // the same goes for findOne
1261 Nullable!User qusr = users.findOne!User(["_id": usr.id]);
1262 if (!qusr.isNull)
1263 logInfo("User: %s", qusr.get.loginName);
1264 }
1265 }
1266
1267 /**
1268 Specifies a level of isolation for read operations. For example, you can use read concern to only read data that has propagated to a majority of nodes in a replica set.
1269
1270 See_Also: $(LINK https://docs.mongodb.com/manual/reference/read-concern/)
1271 */
1272 struct ReadConcern {
1273 ///
1274 enum Level : string {
1275 /// This is the default read concern level.
1276 local = "local",
1277 /// This is the default for reads against secondaries when afterClusterTime and "level" are unspecified. The query returns the the instance’s most recent data.
1278 available = "available",
1279 /// Available for replica sets that use WiredTiger storage engine.
1280 majority = "majority",
1281 /// Available for read operations on the primary only.
1282 linearizable = "linearizable"
1283 }
1284
1285 /// The level of the read concern.
1286 string level;
1287 }
1288
1289 /**
1290 See_Also: $(LINK https://docs.mongodb.com/manual/reference/write-concern/)
1291 */
1292 struct WriteConcern {
1293 /**
1294 If true, wait for the the write operation to get committed to the
1295
1296 See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#journaled)
1297 */
1298 @embedNullable @name("j")
1299 Nullable!bool journal;
1300
1301 /**
1302 When an integer, specifies the number of nodes that should acknowledge
1303 the write and MUST be greater than or equal to 0.
1304
1305 When a string, indicates tags. "majority" is defined, but users could
1306 specify other custom error modes.
1307 */
1308 @embedNullable
1309 Nullable!Bson w;
1310
1311 /**
1312 If provided, and the write concern is not satisfied within the specified
1313 timeout (in milliseconds), the server will return an error for the
1314 operation.
1315
1316 See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#timeouts)
1317 */
1318 @embedNullable @name("wtimeout")
1319 Nullable!long wtimeoutMS;
1320 }
1321
1322 /**
1323 Collation allows users to specify language-specific rules for string comparison, such as rules for letter-case and accent marks.
1324
1325 See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation/)
1326 */
1327 struct Collation {
1328 ///
1329 enum Alternate : string {
1330 /// Whitespace and punctuation are considered base characters
1331 nonIgnorable = "non-ignorable",
1332 /// Whitespace and punctuation are not considered base characters and are only distinguished at strength levels greater than 3
1333 shifted = "shifted",
1334 }
1335
1336 ///
1337 enum MaxVariable : string {
1338 /// Both whitespaces and punctuation are “ignorable”, i.e. not considered base characters.
1339 punct = "punct",
1340 /// Whitespace are “ignorable”, i.e. not considered base characters.
1341 space = "space"
1342 }
1343
1344 /**
1345 The ICU locale
1346
1347 See_Also: See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation-locales-defaults/#collation-languages-locales) for a list of supported locales.
1348
1349 To specify simple binary comparison, specify locale value of "simple".
1350 */
1351 string locale;
1352 /// The level of comparison to perform. Corresponds to ICU Comparison Levels.
1353 @embedNullable Nullable!int strength;
1354 /// Flag that determines whether to include case comparison at strength level 1 or 2.
1355 @embedNullable Nullable!bool caseLevel;
1356 /// A flag that determines sort order of case differences during tertiary level comparisons.
1357 @embedNullable Nullable!string caseFirst;
1358 /// Flag that determines whether to compare numeric strings as numbers or as strings.
1359 @embedNullable Nullable!bool numericOrdering;
1360 /// Field that determines whether collation should consider whitespace and punctuation as base characters for purposes of comparison.
1361 @embedNullable Nullable!Alternate alternate;
1362 /// Field that determines up to which characters are considered ignorable when `alternate: "shifted"`. Has no effect if `alternate: "non-ignorable"`
1363 @embedNullable Nullable!MaxVariable maxVariable;
1364 /**
1365 Flag that determines whether strings with diacritics sort from back of the string, such as with some French dictionary ordering.
1366
1367 If `true` compare from back to front, otherwise front to back.
1368 */
1369 @embedNullable Nullable!bool backwards;
1370 /// Flag that determines whether to check if text require normalization and to perform normalization. Generally, majority of text does not require this normalization processing.
1371 @embedNullable Nullable!bool normalization;
1372 }
1373
1374 ///
1375 struct CursorInitArguments {
1376 /// Specifies the initial batch size for the cursor. Or null for server
1377 /// default value.
1378 @embedNullable Nullable!int batchSize;
1379 }
1380
1381 /// UDA to unset a nullable field if the server wire version doesn't at least
1382 /// match the given version. (inclusive)
1383 ///
1384 /// Use with $(LREF enforceWireVersionConstraints)
1385 struct MinWireVersion
1386 {
1387 ///
1388 WireVersion v;
1389 }
1390
1391 /// ditto
1392 MinWireVersion since(WireVersion v) @safe { return MinWireVersion(v); }
1393
1394 /// UDA to warn when a nullable field is set and the server wire version matches
1395 /// the given version. (inclusive)
1396 ///
1397 /// Use with $(LREF enforceWireVersionConstraints)
1398 struct DeprecatedSinceWireVersion
1399 {
1400 ///
1401 WireVersion v;
1402 }
1403
1404 /// ditto
1405 DeprecatedSinceWireVersion deprecatedSince(WireVersion v) @safe { return DeprecatedSinceWireVersion(v); }
1406
1407 /// UDA to throw a MongoException when a nullable field is set and the server
1408 /// wire version doesn't match the version. (inclusive)
1409 ///
1410 /// Use with $(LREF enforceWireVersionConstraints)
1411 struct ErrorBeforeWireVersion
1412 {
1413 ///
1414 WireVersion v;
1415 }
1416
1417 /// ditto
1418 ErrorBeforeWireVersion errorBefore(WireVersion v) @safe { return ErrorBeforeWireVersion(v); }
1419
1420 /// UDA to unset a nullable field if the server wire version is newer than the
1421 /// given version. (inclusive)
1422 ///
1423 /// Use with $(LREF enforceWireVersionConstraints)
1424 struct MaxWireVersion
1425 {
1426 ///
1427 WireVersion v;
1428 }
1429 /// ditto
1430 MaxWireVersion until(WireVersion v) @safe { return MaxWireVersion(v); }
1431
1432 /// Unsets nullable fields not matching the server version as defined per UDAs.
1433 void enforceWireVersionConstraints(T)(ref T field, WireVersion serverVersion,
1434 string file = __FILE__, size_t line = __LINE__)
1435 @safe {
1436 import std.traits : getUDAs;
1437
1438 string exception;
1439
1440 foreach (i, ref v; field.tupleof) {
1441 enum minV = getUDAs!(field.tupleof[i], MinWireVersion);
1442 enum maxV = getUDAs!(field.tupleof[i], MaxWireVersion);
1443 enum deprecateV = getUDAs!(field.tupleof[i], DeprecatedSinceWireVersion);
1444 enum errorV = getUDAs!(field.tupleof[i], ErrorBeforeWireVersion);
1445
1446 static foreach (depr; deprecateV)
1447 if (serverVersion >= depr.v && !v.isNull)
1448 logInfo("User-set field '%s' is deprecated since MongoDB %s (from %s:%s)",
1449 T.tupleof[i].stringof, depr.v, file, line);
1450
1451 static foreach (err; errorV)
1452 if (serverVersion < err.v && !v.isNull)
1453 exception ~= format("User-set field '%s' is not supported before MongoDB %s\n",
1454 T.tupleof[i].stringof, err.v);
1455
1456 static foreach (min; minV)
1457 if (serverVersion < min.v)
1458 v.nullify();
1459
1460 static foreach (max; maxV)
1461 if (serverVersion > max.v)
1462 v.nullify();
1463 }
1464
1465 if (exception.length)
1466 throw new MongoException(exception ~ "from " ~ file ~ ":" ~ line.to!string);
1467 }
1468
1469 ///
1470 @safe unittest
1471 {
1472 struct SomeMongoCommand
1473 {
1474 @embedNullable @since(WireVersion.v34)
1475 Nullable!int a;
1476
1477 @embedNullable @until(WireVersion.v30)
1478 Nullable!int b;
1479 }
1480
1481 SomeMongoCommand cmd;
1482 cmd.a = 1;
1483 cmd.b = 2;
1484 assert(!cmd.a.isNull);
1485 assert(!cmd.b.isNull);
1486
1487 SomeMongoCommand test = cmd;
1488 enforceWireVersionConstraints(test, WireVersion.v30);
1489 assert(test.a.isNull);
1490 assert(!test.b.isNull);
1491
1492 test = cmd;
1493 enforceWireVersionConstraints(test, WireVersion.v32);
1494 assert(test.a.isNull);
1495 assert(test.b.isNull);
1496
1497 test = cmd;
1498 enforceWireVersionConstraints(test, WireVersion.v34);
1499 assert(!test.a.isNull);
1500 assert(test.b.isNull);
1501 }