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(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 523 /// 524 @safe unittest { 525 import vibe.db.mongo.mongo; 526 527 void test() 528 { 529 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 530 // find all documents in the "test" collection. 531 coll.find(); 532 } 533 } 534 535 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.") 536 auto findOne(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags) 537 { 538 import std.traits; 539 import std.typecons; 540 541 auto c = find!R(query, returnFieldSelector, flags, 0, 1); 542 static if (is(R == Bson)) { 543 foreach (doc; c) return doc; 544 return Bson(null); 545 } else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) { 546 foreach (doc; c) return doc; 547 return null; 548 } else { 549 foreach (doc; c) { 550 Nullable!R ret; 551 ret = doc; 552 return ret; 553 } 554 return Nullable!R.init; 555 } 556 } 557 558 /** Queries the collection for existing documents. 559 560 Returns: 561 By default, a Bson value of the matching document is returned, or $(D Bson(null)) 562 when no document matched. For types R that are not Bson, the returned value is either 563 of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type. 564 565 The projection parameter limits what fields are returned by the database, 566 see projection documentation linked below. 567 568 Throws: Exception if a DB communication error or a query error occurred. 569 570 See_Also: 571 - Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying) 572 - Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections) 573 - $(LREF find) 574 */ 575 auto findOne(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init) 576 if (!is(U == FindOptions)) 577 { 578 options.projection = serializeToBson(projection); 579 return findOne!(R, T)(query, options); 580 } 581 582 /// 583 @safe unittest { 584 import vibe.db.mongo.mongo; 585 586 void test() 587 { 588 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 589 // find documents with status == "A" 590 auto x = coll.findOne(["status": "A"], ["status": true, "otherField": true]); 591 // x now only contains _id (implicit, unless you make it `false`), status and otherField 592 } 593 } 594 595 /** Queries the collection for existing documents. 596 597 Returns: 598 By default, a Bson value of the matching document is returned, or $(D Bson(null)) 599 when no document matched. For types R that are not Bson, the returned value is either 600 of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type. 601 602 Throws: Exception if a DB communication error or a query error occurred. 603 See_Also: 604 - $(LINK http://www.mongodb.org/display/DOCS/Querying) 605 - $(LREF find) 606 */ 607 auto findOne(R = Bson, T)(T query, FindOptions options = FindOptions.init) 608 { 609 import std.traits; 610 import std.typecons; 611 612 options.limit = 1; 613 auto c = find!R(query, options); 614 static if (is(R == Bson)) { 615 foreach (doc; c) return doc; 616 return Bson(null); 617 } else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) { 618 foreach (doc; c) return doc; 619 return null; 620 } else { 621 foreach (doc; c) { 622 Nullable!R ret; 623 ret = doc; 624 return ret; 625 } 626 return Nullable!R.init; 627 } 628 } 629 630 /** 631 Removes documents from the collection. 632 633 Throws: Exception if a DB communication error occurred. 634 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Removing) 635 */ 636 deprecated("Use `deleteOne` or `deleteMany` taking DeleteOptions instead, this method breaks in MongoDB 5.1 and onwards.") 637 void remove(T)(T selector, DeleteFlags flags = DeleteFlags.None) 638 { 639 assert(m_client !is null, "Removing from uninitialized MongoCollection."); 640 auto conn = m_client.lockConnection(); 641 ubyte[256] selector_buf = void; 642 conn.delete_(m_fullPath, flags, serializeToBson(selector, selector_buf)); 643 } 644 645 /// ditto 646 deprecated("Use `deleteMany` taking `DeleteOptions` instead, this method breaks in MongoDB 5.1 and onwards.") 647 void remove()() { remove(Bson.emptyObject); } 648 649 /** 650 Combines a modify and find operation to a single atomic operation. 651 652 Params: 653 query = MongoDB query expression to identify the matched document 654 update = Update expression for the matched document 655 returnFieldSelector = Optional map of fields to return in the response 656 657 Throws: 658 An `Exception` will be thrown if an error occurs in the 659 communication with the database server. 660 661 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify) 662 */ 663 Bson findAndModify(T, U, V)(T query, U update, V returnFieldSelector) 664 { 665 static struct CMD { 666 string findAndModify; 667 T query; 668 U update; 669 V fields; 670 } 671 CMD cmd; 672 cmd.findAndModify = m_name; 673 cmd.query = query; 674 cmd.update = update; 675 cmd.fields = returnFieldSelector; 676 auto ret = database.runCommandChecked(cmd); 677 return ret["value"]; 678 } 679 680 /// ditto 681 Bson findAndModify(T, U)(T query, U update) 682 { 683 return findAndModify(query, update, null); 684 } 685 686 /** 687 Combines a modify and find operation to a single atomic operation with generic options support. 688 689 Params: 690 query = MongoDB query expression to identify the matched document 691 update = Update expression for the matched document 692 options = Generic BSON object that contains additional options 693 fields, such as `"new": true` 694 695 Throws: 696 An `Exception` will be thrown if an error occurs in the 697 communication with the database server. 698 699 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify) 700 */ 701 Bson findAndModifyExt(T, U, V)(T query, U update, V options) 702 { 703 auto bopt = serializeToBson(options); 704 assert(bopt.type == Bson.Type.object, 705 "The options parameter to findAndModifyExt must be a BSON object."); 706 707 Bson cmd = Bson.emptyObject; 708 cmd["findAndModify"] = m_name; 709 cmd["query"] = serializeToBson(query); 710 cmd["update"] = serializeToBson(update); 711 bopt.opApply(delegate int(string key, Bson value) @safe { 712 cmd[key] = value; 713 return 0; 714 }); 715 auto ret = database.runCommandChecked(cmd); 716 return ret["value"]; 717 } 718 719 /// 720 @safe unittest { 721 import vibe.db.mongo.mongo; 722 723 void test() 724 { 725 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 726 coll.findAndModifyExt(["name": "foo"], ["$set": ["value": "bar"]], ["new": true]); 727 } 728 } 729 730 deprecated("deprecated since MongoDB v4.0, use countDocuments or estimatedDocumentCount instead") 731 ulong count(T)(T query) 732 { 733 return countImpl!T(query); 734 } 735 736 private ulong countImpl(T)(T query) 737 { 738 Bson cmd = Bson.emptyObject; 739 cmd["count"] = m_name; 740 cmd["query"] = serializeToBson(query); 741 auto reply = database.runCommandChecked(cmd); 742 switch (reply["n"].type) with (Bson.Type) { 743 default: assert(false, "Unsupported data type in BSON reply for COUNT"); 744 case double_: return cast(ulong)reply["n"].get!double; // v2.x 745 case int_: return reply["n"].get!int; // v3.x 746 case long_: return reply["n"].get!long; // just in case 747 } 748 } 749 750 /** 751 Returns the count of documents that match the query for a collection or 752 view. 753 754 The method wraps the `$group` aggregation stage with a `$sum` expression 755 to perform the count. 756 757 Throws Exception if a DB communication error occurred. 758 759 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/) 760 */ 761 ulong countDocuments(T)(T filter, CountOptions options = CountOptions.init) 762 { 763 // https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details 764 Bson[] pipeline = [Bson(["$match": serializeToBson(filter)])]; 765 if (!options.skip.isNull) 766 pipeline ~= Bson(["$skip": Bson(options.skip.get)]); 767 if (!options.limit.isNull) 768 pipeline ~= Bson(["$limit": Bson(options.limit.get)]); 769 pipeline ~= Bson(["$group": Bson([ 770 "_id": Bson(1), 771 "n": Bson(["$sum": Bson(1)]) 772 ])]); 773 AggregateOptions aggOptions; 774 foreach (i, field; options.tupleof) 775 { 776 enum name = CountOptions.tupleof[i].stringof; 777 static if (name != "filter" && name != "skip" && name != "limit") 778 __traits(getMember, aggOptions, name) = field; 779 } 780 auto reply = aggregate(pipeline, aggOptions); 781 return reply.empty ? 0 : reply.front["n"].to!long; 782 } 783 784 /** 785 Returns the count of all documents in a collection or view. 786 787 Throws Exception if a DB communication error occurred. 788 789 See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.estimatedDocumentCount/) 790 */ 791 ulong estimatedDocumentCount(EstimatedDocumentCountOptions options = EstimatedDocumentCountOptions.init) 792 { 793 // https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details 794 MongoConnection conn = m_client.lockConnection(); 795 if (conn.description.satisfiesVersion(WireVersion.v49)) { 796 Bson[] pipeline = [ 797 Bson(["$collStats": Bson(["count": Bson.emptyObject])]), 798 Bson(["$group": Bson([ 799 "_id": Bson(1), 800 "n": Bson(["$sum": Bson("$count")]) 801 ])]) 802 ]; 803 AggregateOptions aggOptions; 804 aggOptions.maxTimeMS = options.maxTimeMS; 805 auto reply = aggregate(pipeline, aggOptions).front; 806 return reply["n"].to!long; 807 } else { 808 return countImpl(null); 809 } 810 } 811 812 /** 813 Calculates aggregate values for the data in a collection. 814 815 Params: 816 pipeline = A sequence of data aggregation processes. These can 817 either be given as separate parameters, or as a single array 818 parameter. 819 820 Returns: 821 Returns the list of documents aggregated by the pipeline. The return 822 value is either a single `Bson` array value or a `MongoCursor` 823 (input range) of the requested document type. 824 825 Throws: Exception if a DB communication error occurred. 826 827 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/db.collection.aggregate) 828 */ 829 Bson aggregate(ARGS...)(ARGS pipeline) @safe 830 { 831 import std.traits : isArray; 832 833 static if (ARGS.length == 1 && isArray!(ARGS[0])) 834 auto convPipeline = pipeline; 835 else { 836 static struct Pipeline { @asArray ARGS pipeline; } 837 838 Bson[] convPipeline = serializeToBson(Pipeline(pipeline))["pipeline"].get!(Bson[]); 839 } 840 841 return aggregate(convPipeline, AggregateOptions.init).array.serializeToBson; 842 } 843 844 /// ditto 845 MongoCursor!R aggregate(R = Bson, S = Bson)(S[] pipeline, AggregateOptions options) @safe 846 { 847 assert(m_client !is null, "Querying uninitialized MongoCollection."); 848 849 Bson cmd = Bson.emptyObject; // empty object because order is important 850 cmd["aggregate"] = Bson(m_name); 851 cmd["$db"] = Bson(m_db.name); 852 cmd["pipeline"] = serializeToBson(pipeline); 853 MongoConnection conn = m_client.lockConnection(); 854 enforceWireVersionConstraints(options, conn.description.maxWireVersion); 855 foreach (string k, v; serializeToBson(options).byKeyValue) 856 { 857 // spec recommends to omit cursor field when explain is true 858 if (!options.explain.isNull && options.explain.get && k == "cursor") 859 continue; 860 cmd[k] = v; 861 } 862 return MongoCursor!R(m_client, cmd, 863 !options.batchSize.isNull ? options.batchSize.get : 0, 864 !options.maxAwaitTimeMS.isNull ? options.maxAwaitTimeMS.get.msecs 865 : !options.maxTimeMS.isNull ? options.maxTimeMS.get.msecs 866 : Duration.max); 867 } 868 869 /// Example taken from the MongoDB documentation 870 @safe unittest { 871 import vibe.db.mongo.mongo; 872 873 void test() { 874 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 875 auto results = db["coll"].aggregate( 876 ["$match": ["status": "A"]], 877 ["$group": ["_id": Bson("$cust_id"), 878 "total": Bson(["$sum": Bson("$amount")])]], 879 ["$sort": ["total": -1]]); 880 } 881 } 882 883 /// The same example, but using an array of arguments with custom options 884 @safe unittest { 885 import vibe.db.mongo.mongo; 886 887 void test() { 888 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 889 890 Bson[] args; 891 args ~= serializeToBson(["$match": ["status": "A"]]); 892 args ~= serializeToBson(["$group": ["_id": Bson("$cust_id"), 893 "total": Bson(["$sum": Bson("$amount")])]]); 894 args ~= serializeToBson(["$sort": ["total": -1]]); 895 896 AggregateOptions options; 897 options.cursor.batchSize = 10; // pre-fetch the first 10 results 898 auto results = db["coll"].aggregate(args, options); 899 } 900 } 901 902 /** 903 Returns an input range of all unique values for a certain field for 904 records matching the given query. 905 906 Params: 907 fieldName = Name of the field for which to collect unique values 908 query = The query used to select records 909 options = Options to apply 910 911 Returns: 912 An input range with items of type `R` (`Bson` by default) is 913 returned. 914 */ 915 auto distinct(R = Bson, Q)(string fieldName, Q query, DistinctOptions options = DistinctOptions.init) 916 { 917 assert(m_client !is null, "Querying uninitialized MongoCollection."); 918 919 Bson cmd = Bson.emptyObject; // empty object because order is important 920 cmd["distinct"] = Bson(m_name); 921 cmd["key"] = Bson(fieldName); 922 cmd["query"] = serializeToBson(query); 923 MongoConnection conn = m_client.lockConnection(); 924 enforceWireVersionConstraints(options, conn.description.maxWireVersion); 925 foreach (string k, v; serializeToBson(options).byKeyValue) 926 cmd[k] = v; 927 928 import std.algorithm : map; 929 930 auto res = m_db.runCommandChecked(cmd); 931 static if (is(R == Bson)) return res["values"].byValue; 932 else return res["values"].byValue.map!(b => deserializeBson!R(b)); 933 } 934 935 /// 936 @safe unittest { 937 import std.algorithm : equal; 938 import vibe.db.mongo.mongo; 939 940 void test() 941 { 942 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 943 auto coll = db["collection"]; 944 945 coll.drop(); 946 coll.insertOne(["a": "first", "b": "foo"]); 947 coll.insertOne(["a": "first", "b": "bar"]); 948 coll.insertOne(["a": "first", "b": "bar"]); 949 coll.insertOne(["a": "second", "b": "baz"]); 950 coll.insertOne(["a": "second", "b": "bam"]); 951 952 auto result = coll.distinct!string("b", ["a": "first"]); 953 954 assert(result.equal(["foo", "bar"])); 955 } 956 } 957 958 /* 959 following MongoDB standard API for the Index Management specification: 960 961 Standards: https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/index-management.rst#standard-api 962 */ 963 964 deprecated("This is a legacy API, call createIndexes instead") 965 void ensureIndex(scope const(Tuple!(string, int))[] field_orders, IndexFlags flags = IndexFlags.none, Duration expire_time = 0.seconds) 966 @safe { 967 IndexModel[1] models; 968 IndexOptions options; 969 if (flags & IndexFlags.unique) options.unique = true; 970 if (flags & IndexFlags.dropDuplicates) options.dropDups = true; 971 if (flags & IndexFlags.background) options.background = true; 972 if (flags & IndexFlags.sparse) options.sparse = true; 973 if (flags & IndexFlags.expireAfterSeconds) options.expireAfter = expire_time; 974 975 models[0].options = options; 976 foreach (field; field_orders) { 977 models[0].add(field[0], field[1]); 978 } 979 createIndexes(models); 980 } 981 982 deprecated("This is a legacy API, call createIndexes instead. This API is not recommended to be used because of unstable dictionary ordering.") 983 void ensureIndex(int[string] field_orders, IndexFlags flags = IndexFlags.none, ulong expireAfterSeconds = 0) 984 @safe { 985 Tuple!(string, int)[] orders; 986 foreach (k, v; field_orders) 987 orders ~= tuple(k, v); 988 ensureIndex(orders, flags, expireAfterSeconds.seconds); 989 } 990 991 /** 992 Drops a single index from the collection by the index name. 993 994 Throws: `Exception` if it is attempted to pass in `*`. 995 Use dropIndexes() to remove all indexes instead. 996 */ 997 void dropIndex(string name, DropIndexOptions options = DropIndexOptions.init) 998 @safe { 999 if (name == "*") 1000 throw new Exception("Attempted to remove single index with '*'"); 1001 1002 static struct CMD { 1003 string dropIndexes; 1004 string index; 1005 } 1006 1007 CMD cmd; 1008 cmd.dropIndexes = m_name; 1009 cmd.index = name; 1010 database.runCommandChecked(cmd); 1011 } 1012 1013 /// ditto 1014 void dropIndex(T)(T keys, 1015 IndexOptions indexOptions = IndexOptions.init, 1016 DropIndexOptions options = DropIndexOptions.init) 1017 @safe if (!is(Unqual!T == IndexModel)) 1018 { 1019 IndexModel model; 1020 model.keys = serializeToBson(keys); 1021 model.options = indexOptions; 1022 dropIndex(model.name, options); 1023 } 1024 1025 /// ditto 1026 void dropIndex(const IndexModel keys, 1027 DropIndexOptions options = DropIndexOptions.init) 1028 @safe { 1029 dropIndex(keys.name, options); 1030 } 1031 1032 /// 1033 @safe unittest 1034 { 1035 import vibe.db.mongo.mongo; 1036 1037 void test() 1038 { 1039 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 1040 auto primarykey = IndexModel() 1041 .add("name", 1) 1042 .add("primarykey", -1); 1043 coll.dropIndex(primarykey); 1044 } 1045 } 1046 1047 /// Drops all indexes in the collection. 1048 void dropIndexes(DropIndexOptions options = DropIndexOptions.init) 1049 @safe { 1050 static struct CMD { 1051 string dropIndexes; 1052 string index; 1053 } 1054 1055 CMD cmd; 1056 cmd.dropIndexes = m_name; 1057 cmd.index = "*"; 1058 database.runCommandChecked(cmd); 1059 } 1060 1061 /// Unofficial API extension, more efficient multi-index removal on 1062 /// MongoDB 4.2+ 1063 void dropIndexes(string[] names, DropIndexOptions options = DropIndexOptions.init) 1064 @safe { 1065 MongoConnection conn = m_client.lockConnection(); 1066 if (conn.description.satisfiesVersion(WireVersion.v42)) { 1067 static struct CMD { 1068 string dropIndexes; 1069 string[] index; 1070 } 1071 1072 CMD cmd; 1073 cmd.dropIndexes = m_name; 1074 cmd.index = names; 1075 database.runCommandChecked(cmd); 1076 } else { 1077 foreach (name; names) 1078 dropIndex(name); 1079 } 1080 } 1081 1082 /// 1083 @safe unittest 1084 { 1085 import vibe.db.mongo.mongo; 1086 1087 void test() 1088 { 1089 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 1090 coll.dropIndexes(["name_1_primarykey_-1"]); 1091 } 1092 } 1093 1094 /** 1095 Convenience method for creating a single index. Calls `createIndexes` 1096 1097 Supports any kind of document for template parameter T or a IndexModel. 1098 1099 Params: 1100 keys = a IndexModel or type with integer or string fields indicating 1101 index direction or index type. 1102 */ 1103 string createIndex(T)(T keys, 1104 IndexOptions indexOptions = IndexOptions.init, 1105 CreateIndexOptions options = CreateIndexOptions.init) 1106 @safe if (!is(Unqual!T == IndexModel)) 1107 { 1108 IndexModel[1] model; 1109 model[0].keys = serializeToBson(keys); 1110 model[0].options = indexOptions; 1111 return createIndexes(model[], options)[0]; 1112 } 1113 1114 /// ditto 1115 string createIndex(const IndexModel keys, 1116 CreateIndexOptions options = CreateIndexOptions.init) 1117 @safe { 1118 IndexModel[1] model; 1119 model[0] = keys; 1120 return createIndexes(model[], options)[0]; 1121 } 1122 1123 /// 1124 @safe unittest 1125 { 1126 import vibe.db.mongo.mongo; 1127 1128 void test() 1129 { 1130 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 1131 1132 // simple ascending name, descending primarykey compound-index 1133 coll.createIndex(["name": 1, "primarykey": -1]); 1134 1135 IndexOptions textOptions = { 1136 // pick language from another field called "idioma" 1137 languageOverride: "idioma" 1138 }; 1139 auto textIndex = IndexModel() 1140 .withOptions(textOptions) 1141 .add("comments", IndexType.text); 1142 // more complex text index in DB with independent language 1143 coll.createIndex(textIndex); 1144 } 1145 } 1146 1147 /** 1148 Builds one or more indexes in the collection. 1149 1150 See_Also: $(LINK https://docs.mongodb.com/manual/reference/command/createIndexes/) 1151 */ 1152 string[] createIndexes(scope const(IndexModel)[] models, 1153 CreateIndexesOptions options = CreateIndexesOptions.init, 1154 string file = __FILE__, size_t line = __LINE__) 1155 @safe { 1156 string[] keys = new string[models.length]; 1157 1158 MongoConnection conn = m_client.lockConnection(); 1159 if (conn.description.satisfiesVersion(WireVersion.v26)) { 1160 Bson cmd = Bson.emptyObject; 1161 cmd["createIndexes"] = m_name; 1162 Bson[] indexes; 1163 foreach (model; models) { 1164 // trusted to support old compilers which think opt_dup has 1165 // longer lifetime than model.options 1166 IndexOptions opt_dup = (() @trusted => model.options)(); 1167 enforceWireVersionConstraints(opt_dup, conn.description.maxWireVersion, file, line); 1168 Bson index = serializeToBson(opt_dup); 1169 index["key"] = model.keys; 1170 index["name"] = model.name; 1171 indexes ~= index; 1172 } 1173 cmd["indexes"] = Bson(indexes); 1174 database.runCommandChecked(cmd); 1175 } else { 1176 foreach (model; models) { 1177 // trusted to support old compilers which think opt_dup has 1178 // longer lifetime than model.options 1179 IndexOptions opt_dup = (() @trusted => model.options)(); 1180 enforceWireVersionConstraints(opt_dup, WireVersion.old, file, line); 1181 Bson doc = serializeToBson(opt_dup); 1182 doc["v"] = 1; 1183 doc["key"] = model.keys; 1184 doc["ns"] = m_fullPath; 1185 doc["name"] = model.name; 1186 database["system.indexes"].insertOne(doc); 1187 } 1188 } 1189 1190 return keys; 1191 } 1192 1193 /** 1194 Returns an array that holds a list of documents that identify and describe the existing indexes on the collection. 1195 */ 1196 MongoCursor!R listIndexes(R = Bson)() 1197 @safe { 1198 MongoConnection conn = m_client.lockConnection(); 1199 if (conn.description.satisfiesVersion(WireVersion.v30)) { 1200 Bson command = Bson.emptyObject; 1201 command["listIndexes"] = Bson(m_name); 1202 command["$db"] = Bson(m_db.name); 1203 return MongoCursor!R(m_client, command); 1204 } else { 1205 throw new MongoDriverException("listIndexes not supported on MongoDB <3.0"); 1206 } 1207 } 1208 1209 /// 1210 @safe unittest 1211 { 1212 import vibe.db.mongo.mongo; 1213 1214 void test() 1215 { 1216 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 1217 1218 foreach (index; coll.listIndexes()) 1219 logInfo("index %s: %s", index["name"].get!string, index); 1220 } 1221 } 1222 1223 deprecated("Please use the standard API name 'listIndexes'") alias getIndexes = listIndexes; 1224 1225 /** 1226 Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection. 1227 */ 1228 void drop() 1229 @safe { 1230 static struct CMD { 1231 string drop; 1232 } 1233 1234 CMD cmd; 1235 cmd.drop = m_name; 1236 database.runCommandChecked(cmd); 1237 } 1238 } 1239 1240 /// 1241 @safe unittest { 1242 import vibe.data.bson; 1243 import vibe.data.json; 1244 import vibe.db.mongo.mongo; 1245 1246 void test() 1247 { 1248 MongoClient client = connectMongoDB("127.0.0.1"); 1249 MongoCollection users = client.getCollection("myapp.users"); 1250 1251 // canonical version using a Bson object 1252 users.insertOne(Bson(["name": Bson("admin"), "password": Bson("secret")])); 1253 1254 // short version using a string[string] AA that is automatically 1255 // serialized to Bson 1256 users.insertOne(["name": "admin", "password": "secret"]); 1257 1258 // BSON specific types are also serialized automatically 1259 auto uid = BsonObjectID.fromString("507f1f77bcf86cd799439011"); 1260 Bson usr = users.findOne(["_id": uid]); 1261 1262 // JSON is another possibility 1263 Json jusr = parseJsonString(`{"name": "admin", "password": "secret"}`); 1264 users.insertOne(jusr); 1265 } 1266 } 1267 1268 /// Using the type system to define a document "schema" 1269 @safe unittest { 1270 import vibe.db.mongo.mongo; 1271 import vibe.data.serialization : name; 1272 import std.typecons : Nullable; 1273 1274 // Nested object within a "User" document 1275 struct Address { 1276 string name; 1277 string street; 1278 int zipCode; 1279 } 1280 1281 // The document structure of the "myapp.users" collection 1282 struct User { 1283 @name("_id") BsonObjectID id; // represented as "_id" in the database 1284 string loginName; 1285 string password; 1286 Address address; 1287 } 1288 1289 void test() 1290 { 1291 MongoClient client = connectMongoDB("127.0.0.1"); 1292 MongoCollection users = client.getCollection("myapp.users"); 1293 1294 // D values are automatically serialized to the internal BSON format 1295 // upon insertion - see also vibe.data.serialization 1296 User usr; 1297 usr.id = BsonObjectID.generate(); 1298 usr.loginName = "admin"; 1299 usr.password = "secret"; 1300 users.insertOne(usr); 1301 1302 // find supports direct de-serialization of the returned documents 1303 foreach (usr2; users.find!User()) { 1304 logInfo("User: %s", usr2.loginName); 1305 } 1306 1307 // the same goes for findOne 1308 Nullable!User qusr = users.findOne!User(["_id": usr.id]); 1309 if (!qusr.isNull) 1310 logInfo("User: %s", qusr.get.loginName); 1311 } 1312 } 1313 1314 /** 1315 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. 1316 1317 See_Also: $(LINK https://docs.mongodb.com/manual/reference/read-concern/) 1318 */ 1319 struct ReadConcern { 1320 /// 1321 enum Level : string { 1322 /// This is the default read concern level. 1323 local = "local", 1324 /// This is the default for reads against secondaries when afterClusterTime and "level" are unspecified. The query returns the the instance’s most recent data. 1325 available = "available", 1326 /// Available for replica sets that use WiredTiger storage engine. 1327 majority = "majority", 1328 /// Available for read operations on the primary only. 1329 linearizable = "linearizable" 1330 } 1331 1332 /// The level of the read concern. 1333 string level; 1334 } 1335 1336 /** 1337 See_Also: $(LINK https://docs.mongodb.com/manual/reference/write-concern/) 1338 */ 1339 struct WriteConcern { 1340 /** 1341 If true, wait for the the write operation to get committed to the 1342 1343 See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#journaled) 1344 */ 1345 @embedNullable @name("j") 1346 Nullable!bool journal; 1347 1348 /** 1349 When an integer, specifies the number of nodes that should acknowledge 1350 the write and MUST be greater than or equal to 0. 1351 1352 When a string, indicates tags. "majority" is defined, but users could 1353 specify other custom error modes. 1354 */ 1355 @embedNullable 1356 Nullable!Bson w; 1357 1358 /** 1359 If provided, and the write concern is not satisfied within the specified 1360 timeout (in milliseconds), the server will return an error for the 1361 operation. 1362 1363 See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#timeouts) 1364 */ 1365 @embedNullable @name("wtimeout") 1366 Nullable!long wtimeoutMS; 1367 } 1368 1369 /** 1370 Collation allows users to specify language-specific rules for string comparison, such as rules for letter-case and accent marks. 1371 1372 See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation/) 1373 */ 1374 struct Collation { 1375 /// 1376 enum Alternate : string { 1377 /// Whitespace and punctuation are considered base characters 1378 nonIgnorable = "non-ignorable", 1379 /// Whitespace and punctuation are not considered base characters and are only distinguished at strength levels greater than 3 1380 shifted = "shifted", 1381 } 1382 1383 /// 1384 enum MaxVariable : string { 1385 /// Both whitespaces and punctuation are “ignorable”, i.e. not considered base characters. 1386 punct = "punct", 1387 /// Whitespace are “ignorable”, i.e. not considered base characters. 1388 space = "space" 1389 } 1390 1391 /** 1392 The ICU locale 1393 1394 See_Also: See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation-locales-defaults/#collation-languages-locales) for a list of supported locales. 1395 1396 To specify simple binary comparison, specify locale value of "simple". 1397 */ 1398 string locale; 1399 /// The level of comparison to perform. Corresponds to ICU Comparison Levels. 1400 @embedNullable Nullable!int strength; 1401 /// Flag that determines whether to include case comparison at strength level 1 or 2. 1402 @embedNullable Nullable!bool caseLevel; 1403 /// A flag that determines sort order of case differences during tertiary level comparisons. 1404 @embedNullable Nullable!string caseFirst; 1405 /// Flag that determines whether to compare numeric strings as numbers or as strings. 1406 @embedNullable Nullable!bool numericOrdering; 1407 /// Field that determines whether collation should consider whitespace and punctuation as base characters for purposes of comparison. 1408 @embedNullable Nullable!Alternate alternate; 1409 /// Field that determines up to which characters are considered ignorable when `alternate: "shifted"`. Has no effect if `alternate: "non-ignorable"` 1410 @embedNullable Nullable!MaxVariable maxVariable; 1411 /** 1412 Flag that determines whether strings with diacritics sort from back of the string, such as with some French dictionary ordering. 1413 1414 If `true` compare from back to front, otherwise front to back. 1415 */ 1416 @embedNullable Nullable!bool backwards; 1417 /// Flag that determines whether to check if text require normalization and to perform normalization. Generally, majority of text does not require this normalization processing. 1418 @embedNullable Nullable!bool normalization; 1419 } 1420 1421 /// 1422 struct CursorInitArguments { 1423 /// Specifies the initial batch size for the cursor. Or null for server 1424 /// default value. 1425 @embedNullable Nullable!int batchSize; 1426 } 1427 1428 /// UDA to unset a nullable field if the server wire version doesn't at least 1429 /// match the given version. (inclusive) 1430 /// 1431 /// Use with $(LREF enforceWireVersionConstraints) 1432 struct MinWireVersion 1433 { 1434 /// 1435 WireVersion v; 1436 } 1437 1438 /// ditto 1439 MinWireVersion since(WireVersion v) @safe { return MinWireVersion(v); } 1440 1441 /// UDA to warn when a nullable field is set and the server wire version matches 1442 /// the given version. (inclusive) 1443 /// 1444 /// Use with $(LREF enforceWireVersionConstraints) 1445 struct DeprecatedSinceWireVersion 1446 { 1447 /// 1448 WireVersion v; 1449 } 1450 1451 /// ditto 1452 DeprecatedSinceWireVersion deprecatedSince(WireVersion v) @safe { return DeprecatedSinceWireVersion(v); } 1453 1454 /// UDA to throw a MongoException when a nullable field is set and the server 1455 /// wire version doesn't match the version. (inclusive) 1456 /// 1457 /// Use with $(LREF enforceWireVersionConstraints) 1458 struct ErrorBeforeWireVersion 1459 { 1460 /// 1461 WireVersion v; 1462 } 1463 1464 /// ditto 1465 ErrorBeforeWireVersion errorBefore(WireVersion v) @safe { return ErrorBeforeWireVersion(v); } 1466 1467 /// UDA to unset a nullable field if the server wire version is newer than the 1468 /// given version. (inclusive) 1469 /// 1470 /// Use with $(LREF enforceWireVersionConstraints) 1471 struct MaxWireVersion 1472 { 1473 /// 1474 WireVersion v; 1475 } 1476 /// ditto 1477 MaxWireVersion until(WireVersion v) @safe { return MaxWireVersion(v); } 1478 1479 /// Unsets nullable fields not matching the server version as defined per UDAs. 1480 void enforceWireVersionConstraints(T)(ref T field, int serverVersion, 1481 string file = __FILE__, size_t line = __LINE__) 1482 @safe { 1483 import std.traits : getUDAs; 1484 1485 string exception; 1486 1487 foreach (i, ref v; field.tupleof) { 1488 enum minV = getUDAs!(field.tupleof[i], MinWireVersion); 1489 enum maxV = getUDAs!(field.tupleof[i], MaxWireVersion); 1490 enum deprecateV = getUDAs!(field.tupleof[i], DeprecatedSinceWireVersion); 1491 enum errorV = getUDAs!(field.tupleof[i], ErrorBeforeWireVersion); 1492 1493 static foreach (depr; deprecateV) 1494 if (serverVersion >= depr.v && !v.isNull) 1495 logInfo("User-set field '%s' is deprecated since MongoDB %s (from %s:%s)", 1496 T.tupleof[i].stringof, depr.v, file, line); 1497 1498 static foreach (err; errorV) 1499 if (serverVersion < err.v && !v.isNull) 1500 exception ~= format("User-set field '%s' is not supported before MongoDB %s\n", 1501 T.tupleof[i].stringof, err.v); 1502 1503 static foreach (min; minV) 1504 if (serverVersion < min.v) 1505 v.nullify(); 1506 1507 static foreach (max; maxV) 1508 if (serverVersion > max.v) 1509 v.nullify(); 1510 } 1511 1512 if (exception.length) 1513 throw new MongoException(exception ~ "from " ~ file ~ ":" ~ line.to!string); 1514 } 1515 1516 /// 1517 @safe unittest 1518 { 1519 struct SomeMongoCommand 1520 { 1521 @embedNullable @since(WireVersion.v34) 1522 Nullable!int a; 1523 1524 @embedNullable @until(WireVersion.v30) 1525 Nullable!int b; 1526 } 1527 1528 SomeMongoCommand cmd; 1529 cmd.a = 1; 1530 cmd.b = 2; 1531 assert(!cmd.a.isNull); 1532 assert(!cmd.b.isNull); 1533 1534 SomeMongoCommand test = cmd; 1535 enforceWireVersionConstraints(test, WireVersion.v30); 1536 assert(test.a.isNull); 1537 assert(!test.b.isNull); 1538 1539 test = cmd; 1540 enforceWireVersionConstraints(test, WireVersion.v32); 1541 assert(test.a.isNull); 1542 assert(test.b.isNull); 1543 1544 test = cmd; 1545 enforceWireVersionConstraints(test, WireVersion.v34); 1546 assert(!test.a.isNull); 1547 assert(test.b.isNull); 1548 }