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 16 import vibe.core.log; 17 import vibe.db.mongo.client; 18 19 import core.time; 20 import std.algorithm : countUntil, find; 21 import std.array; 22 import std.conv; 23 import std.exception; 24 import std.string; 25 import std.typecons : Tuple, tuple, Nullable; 26 27 28 /** 29 Represents a single collection inside a MongoDB. 30 31 All methods take arbitrary types for Bson arguments. serializeToBson() is implicitly called on 32 them before they are send to the database. The following example shows some possible ways 33 to specify objects. 34 */ 35 struct MongoCollection { 36 private { 37 MongoClient m_client; 38 MongoDatabase m_db; 39 string m_name; 40 string m_fullPath; 41 } 42 43 this(MongoClient client, string fullPath) 44 @safe { 45 assert(client !is null); 46 m_client = client; 47 48 auto dotidx = fullPath.indexOf('.'); 49 assert(dotidx > 0, "The collection name passed to MongoCollection must be of the form \"dbname.collectionname\"."); 50 51 m_fullPath = fullPath; 52 m_db = m_client.getDatabase(fullPath[0 .. dotidx]); 53 m_name = fullPath[dotidx+1 .. $]; 54 } 55 56 this(ref MongoDatabase db, string name) 57 @safe { 58 assert(db.client !is null); 59 m_client = db.client; 60 m_fullPath = db.name ~ "." ~ name; 61 m_db = db; 62 m_name = name; 63 } 64 65 /** 66 Returns: Root database to which this collection belongs. 67 */ 68 @property MongoDatabase database() @safe { return m_db; } 69 70 /** 71 Returns: Name of this collection (excluding the database name). 72 */ 73 @property string name() const @safe { return m_name; } 74 75 /** 76 Performs an update operation on documents matching 'selector', updating them with 'update'. 77 78 Throws: Exception if a DB communication error occurred. 79 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Updating) 80 */ 81 void update(T, U)(T selector, U update, UpdateFlags flags = UpdateFlags.None) 82 { 83 assert(m_client !is null, "Updating uninitialized MongoCollection."); 84 auto conn = m_client.lockConnection(); 85 ubyte[256] selector_buf = void, update_buf = void; 86 conn.update(m_fullPath, flags, serializeToBson(selector, selector_buf), serializeToBson(update, update_buf)); 87 } 88 89 /** 90 Inserts new documents into the collection. 91 92 Note that if the `_id` field of the document(s) is not set, typically 93 using `BsonObjectID.generate()`, the server will generate IDs 94 automatically. If you need to know the IDs of the inserted documents, 95 you need to generate them locally. 96 97 Throws: Exception if a DB communication error occurred. 98 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Inserting) 99 */ 100 void insert(T)(T document_or_documents, InsertFlags flags = InsertFlags.None) 101 { 102 assert(m_client !is null, "Inserting into uninitialized MongoCollection."); 103 auto conn = m_client.lockConnection(); 104 Bson[] docs; 105 Bson bdocs = () @trusted { return serializeToBson(document_or_documents); } (); 106 if( bdocs.type == Bson.Type.Array ) docs = cast(Bson[])bdocs; 107 else docs = () @trusted { return (&bdocs)[0 .. 1]; } (); 108 conn.insert(m_fullPath, flags, docs); 109 } 110 111 /** 112 Queries the collection for existing documents. 113 114 If no arguments are passed to find(), all documents of the collection will be returned. 115 116 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Querying) 117 */ 118 MongoCursor!R find(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0) 119 { 120 assert(m_client !is null, "Querying uninitialized MongoCollection."); 121 return MongoCursor!R(m_client, m_fullPath, flags, num_skip, num_docs_per_chunk, query, returnFieldSelector); 122 } 123 124 /// ditto 125 MongoCursor!R find(R = Bson, T)(T query) { return find!R(query, null); } 126 127 /// ditto 128 MongoCursor!R find(R = Bson)() { return find!R(Bson.emptyObject, null); } 129 130 /** Queries the collection for existing documents. 131 132 Returns: 133 By default, a Bson value of the matching document is returned, or $(D Bson(null)) 134 when no document matched. For types R that are not Bson, the returned value is either 135 of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type. 136 137 Throws: Exception if a DB communication error or a query error occurred. 138 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Querying) 139 */ 140 auto findOne(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags = QueryFlags.None) 141 { 142 import std.traits; 143 import std.typecons; 144 145 auto c = find!R(query, returnFieldSelector, flags, 0, 1); 146 static if (is(R == Bson)) { 147 foreach (doc; c) return doc; 148 return Bson(null); 149 } else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) { 150 foreach (doc; c) return doc; 151 return null; 152 } else { 153 foreach (doc; c) { 154 Nullable!R ret; 155 ret = doc; 156 return ret; 157 } 158 return Nullable!R.init; 159 } 160 } 161 /// ditto 162 auto findOne(R = Bson, T)(T query) { return findOne!R(query, Bson(null)); } 163 164 /** 165 Removes documents from the collection. 166 167 Throws: Exception if a DB communication error occurred. 168 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Removing) 169 */ 170 void remove(T)(T selector, DeleteFlags flags = DeleteFlags.None) 171 { 172 assert(m_client !is null, "Removing from uninitialized MongoCollection."); 173 auto conn = m_client.lockConnection(); 174 ubyte[256] selector_buf = void; 175 conn.delete_(m_fullPath, flags, serializeToBson(selector, selector_buf)); 176 } 177 178 /// ditto 179 void remove()() { remove(Bson.emptyObject); } 180 181 /** 182 Combines a modify and find operation to a single atomic operation. 183 184 Params: 185 query = MongoDB query expression to identify the matched document 186 update = Update expression for the matched document 187 returnFieldSelector = Optional map of fields to return in the response 188 189 Throws: 190 An `Exception` will be thrown if an error occurs in the 191 communication with the database server. 192 193 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify) 194 */ 195 Bson findAndModify(T, U, V)(T query, U update, V returnFieldSelector) 196 { 197 static struct CMD { 198 string findAndModify; 199 T query; 200 U update; 201 V fields; 202 } 203 CMD cmd; 204 cmd.findAndModify = m_name; 205 cmd.query = query; 206 cmd.update = update; 207 cmd.fields = returnFieldSelector; 208 auto ret = database.runCommand(cmd); 209 if( !ret["ok"].get!double ) throw new Exception("findAndModify failed."); 210 return ret["value"]; 211 } 212 213 /// ditto 214 Bson findAndModify(T, U)(T query, U update) 215 { 216 return findAndModify(query, update, null); 217 } 218 219 /** 220 Combines a modify and find operation to a single atomic operation with generic options support. 221 222 Params: 223 query = MongoDB query expression to identify the matched document 224 update = Update expression for the matched document 225 options = Generic BSON object that contains additional options 226 fields, such as `"new": true` 227 228 Throws: 229 An `Exception` will be thrown if an error occurs in the 230 communication with the database server. 231 232 See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify) 233 */ 234 Bson findAndModifyExt(T, U, V)(T query, U update, V options) 235 { 236 auto bopt = serializeToBson(options); 237 assert(bopt.type == Bson.Type.object, 238 "The options parameter to findAndModifyExt must be a BSON object."); 239 240 Bson cmd = Bson.emptyObject; 241 cmd["findAndModify"] = m_name; 242 cmd["query"] = serializeToBson(query); 243 cmd["update"] = serializeToBson(update); 244 bopt.opApply(delegate int(string key, Bson value) @safe { 245 cmd[key] = value; 246 return 0; 247 }); 248 auto ret = database.runCommand(cmd); 249 enforce(ret["ok"].get!double != 0, "findAndModifyExt failed: "~ret["errmsg"].opt!string); 250 return ret["value"]; 251 } 252 253 /// 254 unittest { 255 import vibe.db.mongo.mongo; 256 257 void test() 258 { 259 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 260 coll.findAndModifyExt(["name": "foo"], ["$set": ["value": "bar"]], ["new": true]); 261 } 262 } 263 264 /** 265 Counts the results of the specified query expression. 266 267 Throws Exception if a DB communication error occurred. 268 See_Also: $(LINK http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-{{count%28%29}}) 269 */ 270 ulong count(T)(T query) 271 { 272 static struct Empty {} 273 static struct CMD { 274 string count; 275 T query; 276 Empty fields; 277 } 278 279 CMD cmd; 280 cmd.count = m_name; 281 cmd.query = query; 282 auto reply = database.runCommand(cmd); 283 enforce(reply["ok"].opt!double == 1 || reply["ok"].opt!int == 1, "Count command failed: "~reply["errmsg"].opt!string); 284 switch (reply["n"].type) with (Bson.Type) { 285 default: assert(false, "Unsupported data type in BSON reply for COUNT"); 286 case double_: return cast(ulong)reply["n"].get!double; // v2.x 287 case int_: return reply["n"].get!int; // v3.x 288 case long_: return reply["n"].get!long; // just in case 289 } 290 } 291 292 /** 293 Calculates aggregate values for the data in a collection. 294 295 Params: 296 pipeline = A sequence of data aggregation processes. These can 297 either be given as separate parameters, or as a single array 298 parameter. 299 300 Returns: 301 Returns the list of documents aggregated by the pipeline. The return 302 value is either a single `Bson` array value or a `MongoCursor` 303 (input range) of the requested document type. 304 305 Throws: Exception if a DB communication error occurred. 306 307 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/db.collection.aggregate) 308 */ 309 Bson aggregate(ARGS...)(ARGS pipeline) @safe 310 { 311 import std.traits : isArray; 312 313 static if (ARGS.length == 1 && isArray!(ARGS[0])) 314 auto convPipeline = pipeline; 315 else { 316 static struct Pipeline { @asArray ARGS pipeline; } 317 318 Bson[] convPipeline = serializeToBson(Pipeline(pipeline))["pipeline"].get!(Bson[]); 319 } 320 321 return aggregate(convPipeline, AggregateOptions.init).array.serializeToBson; 322 } 323 324 /// ditto 325 MongoCursor!R aggregate(R = Bson, S = Bson)(S[] pipeline, AggregateOptions options) @safe 326 { 327 assert(m_client !is null, "Querying uninitialized MongoCollection."); 328 329 Bson cmd = Bson.emptyObject; // empty object because order is important 330 cmd["aggregate"] = Bson(m_name); 331 cmd["pipeline"] = serializeToBson(pipeline); 332 foreach (string k, v; serializeToBson(options).byKeyValue) 333 { 334 // spec recommends to omit cursor field when explain is true 335 if (!options.explain.isNull && options.explain.get && k == "cursor") 336 continue; 337 cmd[k] = v; 338 } 339 auto ret = database.runCommand(cmd); 340 enforce(ret["ok"].get!double == 1, "Aggregate command failed: "~ret["errmsg"].opt!string); 341 R[] existing; 342 static if (is(R == Bson)) 343 existing = ret["cursor"]["firstBatch"].get!(Bson[]); 344 else 345 existing = ret["cursor"]["firstBatch"].deserializeBson!(R[]); 346 return MongoCursor!R(m_client, ret["cursor"]["ns"].get!string, ret["cursor"]["id"].get!long, existing); 347 } 348 349 /// Example taken from the MongoDB documentation 350 @safe unittest { 351 import vibe.db.mongo.mongo; 352 353 void test() { 354 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 355 auto results = db["coll"].aggregate( 356 ["$match": ["status": "A"]], 357 ["$group": ["_id": Bson("$cust_id"), 358 "total": Bson(["$sum": Bson("$amount")])]], 359 ["$sort": ["total": -1]]); 360 } 361 } 362 363 /// The same example, but using an array of arguments with custom options 364 unittest { 365 import vibe.db.mongo.mongo; 366 367 void test() { 368 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 369 370 Bson[] args; 371 args ~= serializeToBson(["$match": ["status": "A"]]); 372 args ~= serializeToBson(["$group": ["_id": Bson("$cust_id"), 373 "total": Bson(["$sum": Bson("$amount")])]]); 374 args ~= serializeToBson(["$sort": ["total": -1]]); 375 376 AggregateOptions options; 377 options.cursor.batchSize = 10; // pre-fetch the first 10 results 378 auto results = db["coll"].aggregate(args, options); 379 } 380 } 381 382 /** 383 Returns an input range of all unique values for a certain field for 384 records matching the given query. 385 386 Params: 387 key = Name of the field for which to collect unique values 388 query = The query used to select records 389 390 Returns: 391 An input range with items of type `R` (`Bson` by default) is 392 returned. 393 */ 394 auto distinct(R = Bson, Q)(string key, Q query) 395 { 396 import std.algorithm : map; 397 398 static struct CMD { 399 string distinct; 400 string key; 401 Q query; 402 } 403 CMD cmd; 404 cmd.distinct = m_name; 405 cmd.key = key; 406 cmd.query = query; 407 auto res = m_db.runCommand(cmd); 408 409 enforce(res["ok"].get!double != 0, "Distinct query failed: "~res["errmsg"].opt!string); 410 411 static if (is(R == Bson)) return res["values"].byValue; 412 else return res["values"].byValue.map!(b => deserializeBson!R(b)); 413 } 414 415 /// 416 unittest { 417 import std.algorithm : equal; 418 import vibe.db.mongo.mongo; 419 420 void test() 421 { 422 auto db = connectMongoDB("127.0.0.1").getDatabase("test"); 423 auto coll = db["collection"]; 424 425 coll.drop(); 426 coll.insert(["a": "first", "b": "foo"]); 427 coll.insert(["a": "first", "b": "bar"]); 428 coll.insert(["a": "first", "b": "bar"]); 429 coll.insert(["a": "second", "b": "baz"]); 430 coll.insert(["a": "second", "b": "bam"]); 431 432 auto result = coll.distinct!string("b", ["a": "first"]); 433 434 assert(result.equal(["foo", "bar"])); 435 } 436 } 437 438 /* 439 following MongoDB standard API for the Index Management specification: 440 441 Standards: https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/index-management.rst#standard-api 442 */ 443 444 deprecated("This is a legacy API, call createIndexes instead") 445 void ensureIndex(scope const(Tuple!(string, int))[] field_orders, IndexFlags flags = IndexFlags.none, Duration expire_time = 0.seconds) 446 @safe { 447 IndexModel[1] models; 448 IndexOptions options; 449 if (flags & IndexFlags.unique) options.unique = true; 450 if (flags & IndexFlags.dropDuplicates) options.dropDups = true; 451 if (flags & IndexFlags.background) options.background = true; 452 if (flags & IndexFlags.sparse) options.sparse = true; 453 if (flags & IndexFlags.expireAfterSeconds) options.expireAfter = expire_time; 454 455 models[0].options = options; 456 foreach (field; field_orders) { 457 models[0].add(field[0], field[1]); 458 } 459 createIndexes(models); 460 } 461 462 deprecated("This is a legacy API, call createIndexes instead. This API is not recommended to be used because of unstable dictionary ordering.") 463 void ensureIndex(int[string] field_orders, IndexFlags flags = IndexFlags.none, ulong expireAfterSeconds = 0) 464 @safe { 465 Tuple!(string, int)[] orders; 466 foreach (k, v; field_orders) 467 orders ~= tuple(k, v); 468 ensureIndex(orders, flags, expireAfterSeconds.seconds); 469 } 470 471 /** 472 Drops a single index from the collection by the index name. 473 474 Throws: `Exception` if it is attempted to pass in `*`. 475 Use dropIndexes() to remove all indexes instead. 476 */ 477 void dropIndex(string name, DropIndexOptions options = DropIndexOptions.init) 478 @safe { 479 if (name == "*") 480 throw new Exception("Attempted to remove single index with '*'"); 481 482 static struct CMD { 483 string dropIndexes; 484 string index; 485 } 486 487 CMD cmd; 488 cmd.dropIndexes = m_name; 489 cmd.index = name; 490 auto reply = database.runCommand(cmd); 491 enforce(reply["ok"].get!double == 1, "dropIndex command failed: "~reply["errmsg"].opt!string); 492 } 493 494 /// ditto 495 void dropIndex(T)(T keys, 496 IndexOptions indexOptions = IndexOptions.init, 497 DropIndexOptions options = DropIndexOptions.init) 498 @safe if (!is(Unqual!T == IndexModel)) 499 { 500 IndexModel model; 501 model.keys = serializeToBson(keys); 502 model.options = indexOptions; 503 dropIndex(model.name, options); 504 } 505 506 /// ditto 507 void dropIndex(const IndexModel keys, 508 DropIndexOptions options = DropIndexOptions.init) 509 @safe { 510 dropIndex(keys.name, options); 511 } 512 513 /// 514 @safe unittest 515 { 516 import vibe.db.mongo.mongo; 517 518 void test() 519 { 520 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 521 auto primarykey = IndexModel() 522 .add("name", 1) 523 .add("primarykey", -1); 524 coll.dropIndex(primarykey); 525 } 526 } 527 528 /// Drops all indexes in the collection. 529 void dropIndexes(DropIndexOptions options = DropIndexOptions.init) 530 @safe { 531 static struct CMD { 532 string dropIndexes; 533 string index; 534 } 535 536 CMD cmd; 537 cmd.dropIndexes = m_name; 538 cmd.index = "*"; 539 auto reply = database.runCommand(cmd); 540 enforce(reply["ok"].get!double == 1, "dropIndexes command failed: "~reply["errmsg"].opt!string); 541 } 542 543 /// Unofficial API extension, more efficient multi-index removal on 544 /// MongoDB 4.2+ 545 void dropIndexes(string[] names, DropIndexOptions options = DropIndexOptions.init) 546 @safe { 547 MongoConnection conn = m_client.lockConnection(); 548 if (conn.description.satisfiesVersion(WireVersion.v42)) { 549 static struct CMD { 550 string dropIndexes; 551 string[] index; 552 } 553 554 CMD cmd; 555 cmd.dropIndexes = m_name; 556 cmd.index = names; 557 auto reply = database.runCommand(cmd); 558 enforce(reply["ok"].get!double == 1, "dropIndexes command failed: "~reply["errmsg"].opt!string); 559 } else { 560 foreach (name; names) 561 dropIndex(name); 562 } 563 } 564 565 /// 566 @safe unittest 567 { 568 import vibe.db.mongo.mongo; 569 570 void test() 571 { 572 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 573 coll.dropIndexes(["name_1_primarykey_-1"]); 574 } 575 } 576 577 /** 578 Convenience method for creating a single index. Calls `createIndexes` 579 580 Supports any kind of document for template parameter T or a IndexModel. 581 582 Params: 583 keys = a IndexModel or type with integer or string fields indicating 584 index direction or index type. 585 */ 586 string createIndex(T)(T keys, 587 IndexOptions indexOptions = IndexOptions.init, 588 CreateIndexOptions options = CreateIndexOptions.init) 589 @safe if (!is(Unqual!T == IndexModel)) 590 { 591 IndexModel[1] model; 592 model[0].keys = serializeToBson(keys); 593 model[0].options = indexOptions; 594 return createIndexes(model[], options)[0]; 595 } 596 597 /// ditto 598 string createIndex(const IndexModel keys, 599 CreateIndexOptions options = CreateIndexOptions.init) 600 @safe { 601 IndexModel[1] model; 602 model[0] = keys; 603 return createIndexes(model[], options)[0]; 604 } 605 606 /// 607 @safe unittest 608 { 609 import vibe.db.mongo.mongo; 610 611 void test() 612 { 613 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 614 615 // simple ascending name, descending primarykey compound-index 616 coll.createIndex(["name": 1, "primarykey": -1]); 617 618 IndexOptions textOptions = { 619 // pick language from another field called "idioma" 620 languageOverride: "idioma" 621 }; 622 auto textIndex = IndexModel() 623 .withOptions(textOptions) 624 .add("comments", IndexType.text); 625 // more complex text index in DB with independent language 626 coll.createIndex(textIndex); 627 } 628 } 629 630 /** 631 Builds one or more indexes in the collection. 632 633 See_Also: $(LINK https://docs.mongodb.com/manual/reference/command/createIndexes/) 634 */ 635 string[] createIndexes(scope const(IndexModel)[] models, 636 CreateIndexesOptions options = CreateIndexesOptions.init) 637 @safe { 638 string[] keys = new string[models.length]; 639 640 MongoConnection conn = m_client.lockConnection(); 641 if (conn.description.satisfiesVersion(WireVersion.v26)) { 642 Bson cmd = Bson.emptyObject; 643 cmd["createIndexes"] = m_name; 644 Bson[] indexes; 645 foreach (model; models) { 646 // trusted to support old compilers which think opt_dup has 647 // longer lifetime than model.options 648 IndexOptions opt_dup = (() @trusted => model.options)(); 649 enforceWireVersionConstraints(opt_dup, conn.description.maxWireVersion); 650 Bson index = serializeToBson(opt_dup); 651 index["key"] = model.keys; 652 index["name"] = model.name; 653 indexes ~= index; 654 } 655 cmd["indexes"] = Bson(indexes); 656 auto reply = database.runCommand(cmd); 657 enforce(reply["ok"].get!double == 1, "createIndex command failed: " 658 ~ reply["errmsg"].opt!string); 659 } else { 660 foreach (model; models) { 661 // trusted to support old compilers which think opt_dup has 662 // longer lifetime than model.options 663 IndexOptions opt_dup = (() @trusted => model.options)(); 664 enforceWireVersionConstraints(opt_dup, WireVersion.old); 665 Bson doc = serializeToBson(opt_dup); 666 doc["v"] = 1; 667 doc["key"] = model.keys; 668 doc["ns"] = m_fullPath; 669 doc["name"] = model.name; 670 database["system.indexes"].insert(doc); 671 } 672 } 673 674 return keys; 675 } 676 677 /** 678 Returns an array that holds a list of documents that identify and describe the existing indexes on the collection. 679 */ 680 MongoCursor!R listIndexes(R = Bson)() 681 @safe { 682 MongoConnection conn = m_client.lockConnection(); 683 if (conn.description.satisfiesVersion(WireVersion.v30)) { 684 static struct CMD { 685 string listIndexes; 686 } 687 688 CMD cmd; 689 cmd.listIndexes = m_name; 690 691 auto reply = database.runCommand(cmd); 692 enforce(reply["ok"].get!double == 1, "getIndexes command failed: "~reply["errmsg"].opt!string); 693 return MongoCursor!R(m_client, reply["cursor"]["ns"].get!string, reply["cursor"]["id"].get!long, reply["cursor"]["firstBatch"].get!(Bson[])); 694 } else { 695 return database["system.indexes"].find!R(); 696 } 697 } 698 699 /// 700 @safe unittest 701 { 702 import vibe.db.mongo.mongo; 703 704 void test() 705 { 706 auto coll = connectMongoDB("127.0.0.1").getCollection("test"); 707 708 foreach (index; coll.listIndexes()) 709 logInfo("index %s: %s", index["name"].get!string, index); 710 } 711 } 712 713 deprecated("Please use the standard API name 'listIndexes'") alias getIndexes = listIndexes; 714 715 /** 716 Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection. 717 */ 718 void drop() 719 @safe { 720 static struct CMD { 721 string drop; 722 } 723 724 CMD cmd; 725 cmd.drop = m_name; 726 auto reply = database.runCommand(cmd); 727 enforce(reply["ok"].get!double == 1, "drop command failed: "~reply["errmsg"].opt!string); 728 } 729 } 730 731 /// 732 unittest { 733 import vibe.data.bson; 734 import vibe.data.json; 735 import vibe.db.mongo.mongo; 736 737 void test() 738 { 739 MongoClient client = connectMongoDB("127.0.0.1"); 740 MongoCollection users = client.getCollection("myapp.users"); 741 742 // canonical version using a Bson object 743 users.insert(Bson(["name": Bson("admin"), "password": Bson("secret")])); 744 745 // short version using a string[string] AA that is automatically 746 // serialized to Bson 747 users.insert(["name": "admin", "password": "secret"]); 748 749 // BSON specific types are also serialized automatically 750 auto uid = BsonObjectID.fromString("507f1f77bcf86cd799439011"); 751 Bson usr = users.findOne(["_id": uid]); 752 753 // JSON is another possibility 754 Json jusr = parseJsonString(`{"name": "admin", "password": "secret"}`); 755 users.insert(jusr); 756 } 757 } 758 759 /// Using the type system to define a document "schema" 760 unittest { 761 import vibe.db.mongo.mongo; 762 import vibe.data.serialization : name; 763 import std.typecons : Nullable; 764 765 // Nested object within a "User" document 766 struct Address { 767 string name; 768 string street; 769 int zipCode; 770 } 771 772 // The document structure of the "myapp.users" collection 773 struct User { 774 @name("_id") BsonObjectID id; // represented as "_id" in the database 775 string loginName; 776 string password; 777 Address address; 778 } 779 780 void test() 781 { 782 MongoClient client = connectMongoDB("127.0.0.1"); 783 MongoCollection users = client.getCollection("myapp.users"); 784 785 // D values are automatically serialized to the internal BSON format 786 // upon insertion - see also vibe.data.serialization 787 User usr; 788 usr.id = BsonObjectID.generate(); 789 usr.loginName = "admin"; 790 usr.password = "secret"; 791 users.insert(usr); 792 793 // find supports direct de-serialization of the returned documents 794 foreach (usr2; users.find!User()) { 795 logInfo("User: %s", usr2.loginName); 796 } 797 798 // the same goes for findOne 799 Nullable!User qusr = users.findOne!User(["_id": usr.id]); 800 if (!qusr.isNull) 801 logInfo("User: %s", qusr.get.loginName); 802 } 803 } 804 805 /** 806 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. 807 808 See_Also: $(LINK https://docs.mongodb.com/manual/reference/read-concern/) 809 */ 810 struct ReadConcern { 811 /// 812 enum Level : string { 813 /// This is the default read concern level. 814 local = "local", 815 /// This is the default for reads against secondaries when afterClusterTime and "level" are unspecified. The query returns the the instance’s most recent data. 816 available = "available", 817 /// Available for replica sets that use WiredTiger storage engine. 818 majority = "majority", 819 /// Available for read operations on the primary only. 820 linearizable = "linearizable" 821 } 822 823 /// The level of the read concern. 824 string level; 825 } 826 827 /** 828 Collation allows users to specify language-specific rules for string comparison, such as rules for letter-case and accent marks. 829 830 See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation/) 831 */ 832 struct Collation { 833 /// 834 enum Alternate : string { 835 /// Whitespace and punctuation are considered base characters 836 nonIgnorable = "non-ignorable", 837 /// Whitespace and punctuation are not considered base characters and are only distinguished at strength levels greater than 3 838 shifted = "shifted", 839 } 840 841 /// 842 enum MaxVariable : string { 843 /// Both whitespaces and punctuation are “ignorable”, i.e. not considered base characters. 844 punct = "punct", 845 /// Whitespace are “ignorable”, i.e. not considered base characters. 846 space = "space" 847 } 848 849 /** 850 The ICU locale 851 852 See_Also: See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation-locales-defaults/#collation-languages-locales) for a list of supported locales. 853 854 To specify simple binary comparison, specify locale value of "simple". 855 */ 856 string locale; 857 /// The level of comparison to perform. Corresponds to ICU Comparison Levels. 858 @embedNullable Nullable!int strength; 859 /// Flag that determines whether to include case comparison at strength level 1 or 2. 860 @embedNullable Nullable!bool caseLevel; 861 /// A flag that determines sort order of case differences during tertiary level comparisons. 862 @embedNullable Nullable!string caseFirst; 863 /// Flag that determines whether to compare numeric strings as numbers or as strings. 864 @embedNullable Nullable!bool numericOrdering; 865 /// Field that determines whether collation should consider whitespace and punctuation as base characters for purposes of comparison. 866 @embedNullable Nullable!Alternate alternate; 867 /// Field that determines up to which characters are considered ignorable when `alternate: "shifted"`. Has no effect if `alternate: "non-ignorable"` 868 @embedNullable Nullable!MaxVariable maxVariable; 869 /** 870 Flag that determines whether strings with diacritics sort from back of the string, such as with some French dictionary ordering. 871 872 If `true` compare from back to front, otherwise front to back. 873 */ 874 @embedNullable Nullable!bool backwards; 875 /// Flag that determines whether to check if text require normalization and to perform normalization. Generally, majority of text does not require this normalization processing. 876 @embedNullable Nullable!bool normalization; 877 } 878 879 /// 880 struct CursorInitArguments { 881 /// Specifies the initial batch size for the cursor. Or null for server 882 /// default value. 883 @embedNullable Nullable!int batchSize; 884 } 885 886 /// UDA to unset a nullable field if the server wire version doesn't at least 887 /// match the given version. (inclusive) 888 /// 889 /// Use with $(LREF enforceWireVersionConstraints) 890 struct MinWireVersion 891 { 892 /// 893 WireVersion v; 894 } 895 896 /// ditto 897 MinWireVersion since(WireVersion v) @safe { return MinWireVersion(v); } 898 899 /// UDA to unset a nullable field if the server wire version is newer than the 900 /// given version. (inclusive) 901 /// 902 /// Use with $(LREF enforceWireVersionConstraints) 903 struct MaxWireVersion 904 { 905 /// 906 WireVersion v; 907 } 908 /// ditto 909 MaxWireVersion until(WireVersion v) @safe { return MaxWireVersion(v); } 910 911 /// Unsets nullable fields not matching the server version as defined per UDAs. 912 void enforceWireVersionConstraints(T)(ref T field, WireVersion serverVersion) 913 @safe { 914 import std.traits : getUDAs; 915 916 foreach (i, ref v; field.tupleof) { 917 enum minV = getUDAs!(field.tupleof[i], MinWireVersion); 918 enum maxV = getUDAs!(field.tupleof[i], MaxWireVersion); 919 920 static foreach (min; minV) 921 if (serverVersion < min.v) 922 v.nullify(); 923 924 static foreach (max; maxV) 925 if (serverVersion > max.v) 926 v.nullify(); 927 } 928 } 929 930 /// 931 unittest 932 { 933 struct SomeMongoCommand 934 { 935 @embedNullable @since(WireVersion.v34) 936 Nullable!int a; 937 938 @embedNullable @until(WireVersion.v30) 939 Nullable!int b; 940 } 941 942 SomeMongoCommand cmd; 943 cmd.a = 1; 944 cmd.b = 2; 945 assert(!cmd.a.isNull); 946 assert(!cmd.b.isNull); 947 948 SomeMongoCommand test = cmd; 949 enforceWireVersionConstraints(test, WireVersion.v30); 950 assert(test.a.isNull); 951 assert(!test.b.isNull); 952 953 test = cmd; 954 enforceWireVersionConstraints(test, WireVersion.v32); 955 assert(test.a.isNull); 956 assert(test.b.isNull); 957 958 test = cmd; 959 enforceWireVersionConstraints(test, WireVersion.v34); 960 assert(!test.a.isNull); 961 assert(test.b.isNull); 962 } 963 964 /** 965 Represents available options for an aggregate call 966 967 See_Also: $(LINK https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/) 968 969 Standards: $(LINK https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/crud/crud.rst#api) 970 */ 971 struct AggregateOptions { 972 // non-optional since 3.6 973 // get/set by `batchSize`, undocumented in favor of that field 974 CursorInitArguments cursor; 975 976 /// Specifies the initial batch size for the cursor. 977 ref inout(Nullable!int) batchSize() 978 @property inout @safe pure nothrow @nogc @ignore { 979 return cursor.batchSize; 980 } 981 982 // undocumented because this field isn't a spec field because it is 983 // out-of-scope for a driver 984 @embedNullable Nullable!bool explain; 985 986 /** 987 Enables writing to temporary files. When set to true, aggregation 988 operations can write data to the _tmp subdirectory in the dbPath 989 directory. 990 */ 991 @embedNullable Nullable!bool allowDiskUse; 992 993 /** 994 Specifies a time limit in milliseconds for processing operations on a 995 cursor. If you do not specify a value for maxTimeMS, operations will not 996 time out. 997 */ 998 @embedNullable Nullable!long maxTimeMS; 999 1000 /** 1001 If true, allows the write to opt-out of document level validation. 1002 This only applies when the $out or $merge stage is specified. 1003 */ 1004 @embedNullable Nullable!bool bypassDocumentValidation; 1005 1006 /** 1007 Specifies the read concern. Only compatible with a write stage. (e.g. 1008 `$out`, `$merge`) 1009 1010 Aggregate commands do not support the $(D ReadConcern.Level.linearizable) 1011 level. 1012 1013 Standards: $(LINK https://github.com/mongodb/specifications/blob/7745234f93039a83ae42589a6c0cdbefcffa32fa/source/read-write-concern/read-write-concern.rst) 1014 */ 1015 @embedNullable Nullable!ReadConcern readConcern; 1016 1017 /// Specifies a collation. 1018 @embedNullable Nullable!Collation collation; 1019 1020 /** 1021 The index to use for the aggregation. The index is on the initial 1022 collection / view against which the aggregation is run. 1023 1024 The hint does not apply to $lookup and $graphLookup stages. 1025 1026 Specify the index either by the index name as a string or the index key 1027 pattern. If specified, then the query system will only consider plans 1028 using the hinted index. 1029 */ 1030 @embedNullable Nullable!Bson hint; 1031 1032 /** 1033 Users can specify an arbitrary string to help trace the operation 1034 through the database profiler, currentOp, and logs. 1035 */ 1036 @embedNullable Nullable!string comment; 1037 }