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