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