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