1 module mongo; 2 3 import mongoaux.definitions; 4 import std.traits; 5 6 // TODO: Check date handling. 7 // TODO: Change exceptions. It should be possible to retry on failure. 8 // TODO: Add rest of unittests. 9 // TODO: Reply handling. 10 // TODO: Handle MongoDocCreated and MongoDocUpdated correctly. 11 // TODO: Remove the clients that used del from the pooler toDestroy to avoid 12 // consuming a lot of memory. Be careful not to remove it twice. 13 14 15 unittest { 16 // TODO: Check BSONs on this test are deleted. 17 18 // Use a pool to insert values, find them and delete them. 19 auto pool = MongoPool("mongodb://localhost"); 20 scope(exit) pool.unlock(); 21 foreach(i; 0..4) { 22 auto client = pool.lock(); 23 scope(exit) client.unlock(); 24 // Not using the * would give compilation errors because indexes for 25 // pointers are for memory locations. 26 auto collection = (*(*client) ["newBase"])["newCollection"]; 27 28 if(collection.count) { 29 collection.drop(); 30 } 31 32 enum ExampleStringEnum : string { 33 A = "A", 34 B = "B" 35 } 36 37 enum ExampleIntEnum { 38 A, 39 B 40 } 41 42 struct ExampleStruct { 43 int intVal = 1; 44 long longVal = 2L; 45 double doubleVal = 3.0; 46 int [] arrayVal = [1,2,3]; 47 int [][] multidimArray = [[1,2],[3,4]]; 48 string stringVal = "Text"; 49 int [string] aa; 50 ExampleStringEnum strEnum = ExampleStringEnum.A; 51 ExampleIntEnum intEnum = ExampleIntEnum.A; 52 } 53 // Insert empty BSON (it has only default values) 54 // It'll have an _id in Mongo. 55 auto toInsert = ExampleStruct(); 56 collection.insert(toInsert); 57 assert(collection.count == 1); 58 // Check that it has default value when converted back to ExampleStruct 59 assert(collection.findOne.as!ExampleStruct == toInsert); 60 collection.deleteOne(); 61 assert(collection.count == 0); 62 63 // Add a non-default value. 64 toInsert.longVal = 5; 65 collection.insert(toInsert); 66 assert(collection.count == 1); 67 auto retValue = collection.findOne; 68 assert(retValue.key!long(`longVal`) == 5L); 69 // Check conversion again. 70 assert(retValue.as!ExampleStruct == toInsert); 71 // Be careful, this doesn't remove the value because deleteOne creates a 72 // query with all the values in ExampleStruct. Insert didn't insert default 73 // ones. 74 collection.deleteOne(toInsert); 75 assert(collection.count == 1); 76 77 // Correct way: should have just the keys present in Mongo. 78 // In this specific case, deleteOne without arguments would work too. 79 struct CorrectQuery { 80 long longVal = 5; 81 } 82 collection.deleteOne(CorrectQuery()); 83 assert(collection.count == 0); 84 85 // Add array values. 86 toInsert.arrayVal = [5,6,7,8]; 87 toInsert.multidimArray = [[7,8,9],[10,11,12]]; 88 toInsert.aa = ["one" : 1]; 89 collection.insert(toInsert); 90 91 // Reuse retValue. 92 retValue.unlock(); 93 retValue = collection.findOne; 94 assert(retValue.key!(int[])(`arrayVal`) == [5,6,7,8]); 95 assert(retValue.key!(int[][])(`multidimArray`) == [[7,8,9],[10,11,12]]); 96 assert(retValue.key!(int[string])(`aa`)["one"] == 1); 97 collection.deleteOne; 98 99 // Empty arrays. 100 toInsert.arrayVal = []; 101 collection.insert(toInsert); 102 103 // Reuse retValue. 104 retValue.unlock(); 105 retValue = collection.findOne; 106 scope(exit) retValue.unlock(); 107 assert(retValue.key!(int[])(`arrayVal`) == []); 108 assert(retValue.as!ExampleStruct == toInsert); 109 110 collection.deleteOne; 111 112 // Update. 113 114 toInsert.intVal = 4; 115 collection.insert(toInsert); 116 struct Query { 117 struct I { 118 @("$eq") int equalsTo = 4; 119 } 120 I intVal; 121 } 122 123 struct Update { 124 struct I { 125 int intVal = 2; 126 } 127 @("$set") I update; 128 } 129 auto upd = Update(); // Takes a ref so it can process UDAs. 130 collection.update(Query(), upd); 131 assert(collection.findOne!ExampleStruct.intVal == 2); 132 } 133 } 134 135 unittest { 136 // Manually built BSONs. 137 auto pool = MongoPool("mongodb://localhost"); 138 scope(exit) pool.unlock(); 139 auto client = pool.lock(); 140 scope(exit) client.unlock(); 141 auto collection = (*(*client) ["newBase"])["newCollection"]; 142 143 if(collection.count){ 144 collection.drop(); 145 } 146 147 auto toInsert = empty(); scope(exit) toInsert.unlock(); 148 toInsert.append(`intVal`, 4); 149 toInsert.append(`longVal`, 9L); 150 toInsert.append(`boolVal`, true); 151 toInsert.append(`stringVal`, "Hello"); 152 toInsert.append(`doubleVal`, 5.0); 153 auto subDocument = empty(); scope(exit) subDocument.unlock(); 154 subDocument.append(`subIntVal`, 7); 155 toInsert.append(`subDoc`, subDocument); 156 import std.datetime; 157 // TODO: Change it to use just Unix timestamps. 158 toInsert.append(`dateVal`, Clock.currTime (UTC ())); 159 toInsert.append(`arrayVal`, [1, 2, 3, 4]); 160 collection.insert(toInsert); 161 162 auto foundVal = collection.findOne(toInsert); 163 scope(exit) foundVal.unlock(); 164 foreach (key, value; foundVal.byKeyValue) { 165 switch(key) { 166 case `_id`: 167 //writeln(value.as!bson_oid_t); 168 break; 169 case `intVal`: 170 assert(value.as!int == 4); 171 break; 172 case `longVal`: 173 assert(value.as!long == 9); 174 break; 175 case `boolVal`: 176 assert(value.as!bool == true); 177 break; 178 case `stringVal`: 179 assert(value.as!string == "Hello"); 180 break; 181 case `doubleVal`: 182 import std.math : approxEqual; 183 assert(value.as!double.approxEqual(5.0)); 184 break; 185 case `subDoc`: 186 /+ 187 auto doc = value.as!BSON; scope(exit) doc.unlock(); 188 writeln(doc); 189 +/ 190 break; 191 case `dateVal`: 192 //writeln(value.as!DateTime); 193 break; 194 case `arrayVal`: 195 assert(value.as!(int[]) == [1,2,3,4]); 196 break; 197 default: 198 assert(0, text(`TODO: `, key)); 199 } 200 } 201 } 202 203 unittest { 204 // Nested documents. 205 struct Nested { 206 struct Internal { 207 int a = 0; 208 } 209 Internal internal; 210 } 211 auto pool = MongoPool("mongodb://localhost"); 212 scope(exit) pool.unlock(); 213 auto client = pool.lock(); 214 scope(exit) client.unlock(); 215 auto collection = (*(*client) ["newBase"])["newCollection"]; 216 217 if(collection.count){ 218 collection.drop(); 219 } 220 auto toInsert = Nested(Nested.Internal(4)); 221 collection.insert(toInsert); 222 auto found = collection.findOne; 223 scope(exit) found.unlock(); 224 assert(found.as!Nested == Nested(Nested.Internal(4))); 225 } 226 227 unittest { 228 // Use object ids 229 auto pool = MongoPool("mongodb://localhost"); 230 scope(exit) pool.unlock(); 231 auto client = pool.lock(); 232 scope(exit) client.unlock(); 233 auto collection = (*(*client) ["newBase"])["newCollection"]; 234 235 if(collection.count) { 236 collection.drop(); 237 } 238 239 struct WithId { 240 int a; 241 bson_oid_t _id; 242 } 243 auto str = "aaaaaabbbbbbccccccdddddd"; 244 auto toInsert = WithId(5, str.toId); 245 collection.insert(toInsert); 246 auto retValue = collection.findOne; 247 scope(exit) retValue.unlock(); 248 assert(retValue.as!WithId == toInsert); 249 collection.deleteOne; 250 251 struct WithStringId { 252 string _id; 253 } 254 auto toInsert2 = WithStringId("123456789012345678901234"); 255 collection.insert(toInsert2); 256 auto retValue2 = collection.findOne; 257 scope(exit) retValue2.unlock(); 258 assert(retValue2.as!WithStringId == toInsert2); 259 } 260 261 unittest { 262 // Without any BSON. 263 struct ExampleStruct { 264 int a = 0; 265 long b = 1; 266 } 267 auto pool = MongoPool("mongodb://localhost"); 268 scope(exit) pool.unlock(); 269 auto client = pool.lock(); 270 scope(exit) client.unlock(); 271 auto collection = (*(*client) ["newBase"])["newCollection"]; 272 273 if(collection.count) { 274 collection.drop(); 275 } 276 277 auto toInsert = ExampleStruct(1,2); 278 collection.insert(toInsert); 279 assert(collection.findOne!ExampleStruct == toInsert); 280 281 foreach(doc; collection.find!ExampleStruct) { 282 assert(doc == toInsert); 283 } 284 } 285 286 unittest { 287 // Bulk operations. 288 struct ExampleStruct { 289 int a = 0; 290 long b = 1; 291 string _id; 292 } 293 294 auto pool = MongoPool("mongodb://localhost"); 295 scope(exit) pool.unlock(); 296 auto client = pool.lock(); 297 scope(exit) client.unlock(); 298 auto collection = (*(*client) ["newBase"])["newCollection"]; 299 300 if(collection.count) { 301 collection.drop(); 302 } 303 304 ExampleStruct [] dataRange; 305 foreach(int i; 0..10) { 306 string id; 307 foreach(j; 0..24) { 308 id ~= i.to!string; 309 } 310 dataRange ~= ExampleStruct(i, i, id); 311 } 312 313 collection.insert(dataRange); 314 assert(collection.count == 10); 315 uint i = 0; 316 // Note: Might fail if Mongo sends them in other order, that wouldn't be 317 // wrong. 318 foreach(element; collection.find!ExampleStruct) { 319 assert(element.a == i); 320 assert(element.b == i); 321 i++; 322 } 323 324 dataRange ~= ExampleStruct(10,10, "0123456789abcdef01234567"); 325 struct IdQuery { 326 string _id; 327 } 328 struct SetValue { 329 struct I { 330 int a = 0; 331 long b = 0; 332 } 333 @("$set") I val; // Equivalent to adding "$set" : {"a" : 0, "b" : 0} on a BSON 334 } 335 336 struct Upsert { 337 bool upsert = true; 338 } 339 import std.algorithm : map; 340 auto queryRange = dataRange.map!(a => IdQuery(a._id)); 341 auto updateRange = dataRange.map!(a => SetValue(SetValue.I(a.a + 1))); 342 //writeln(collection.find!ExampleStruct); 343 collection.update(queryRange, updateRange, Upsert()); 344 //writeln(collection.find!ExampleStruct); 345 assert(collection.count == 11); 346 } 347 348 unittest { 349 // MongoDocUpdated and MongoDocCreated. 350 struct ExampleStruct { 351 @MongoDocCreated long created; 352 @MongoDocUpdated long updated; 353 } 354 355 auto pool = MongoPool("mongodb://localhost"); 356 scope(exit) pool.unlock(); 357 auto client = pool.lock(); 358 scope(exit) client.unlock(); 359 auto collection = (*(*client) ["newBase"])["newCollection"]; 360 361 if(collection.count) { 362 collection.drop(); 363 } 364 365 auto toInsert = ExampleStruct(0, 0); 366 collection.insert(toInsert); 367 assert(toInsert.created != 0 && toInsert.updated == 0); 368 } 369 370 unittest { 371 // Get created's id. 372 struct ExampleStruct { 373 int a = -1; 374 } 375 auto pool = MongoPool("mongodb://localhost"); 376 scope(exit) pool.unlock(); 377 auto client = pool.lock(); 378 scope(exit) client.unlock(); 379 auto collection = (*(*client) ["newBase"])["newCollection"]; 380 381 if(collection.count) { 382 collection.drop(); 383 } 384 385 ExampleStruct [] dataRange; 386 foreach(int i; 0..10) { 387 dataRange ~= ExampleStruct(i); 388 } 389 auto ids = collection.insert(dataRange); 390 391 struct Query { 392 bson_oid_t _id; 393 } 394 395 struct Result { 396 bson_oid_t _id; 397 int a; 398 } 399 import std.algorithm; 400 import std.array; 401 foreach(i, id; ids.map!(a => Query(a)).array) { 402 assert(collection 403 .findOne!(Result, Query)(id) 404 ._id == id._id 405 , `Didn't find inserted ids for a range`); 406 } 407 collection.drop(); 408 409 // Single insert with autogenerated id. 410 auto toInsert = ExampleStruct(10); 411 auto id = collection.insert(toInsert); 412 assert( 413 collection.findOne!(Result)()._id == id 414 , `Didn't find inserted id` 415 ); 416 417 collection.drop(); 418 // Returned should be the same as the manually inserted one. 419 auto toInsertManual = Result(); 420 bson_oid_init(&toInsertManual._id, null); 421 assert(toInsertManual._id == collection.insert(toInsertManual)); 422 collection.drop(); 423 424 // Also for ranges. 425 Result [] rangeWithIDs; 426 import std.range; 427 /// Generates 000000000000000000000000 111111111111111111111111... 428 auto ids2 = iota(10).map!(a => a.to!string.repeat(24).joiner.to!string.toId); 429 foreach(i; 0..10) { 430 rangeWithIDs ~= Result(ids[i], i.to!int); 431 } 432 assert (collection.insert(rangeWithIDs) == ids); 433 } 434 435 unittest { 436 struct B { 437 string hello = ""; 438 int boo; 439 } 440 struct A { 441 int blah; 442 B[] boos; 443 B[][string] assocBoos; 444 } 445 struct WithoutBoo { 446 string hello = ""; 447 } 448 struct AWithoutBoo { 449 WithoutBoo[] boos; 450 WithoutBoo[][string] assocBoos; 451 } 452 auto pool = MongoPool("mongodb://localhost"); 453 scope(exit) pool.unlock(); 454 auto client = pool.lock(); 455 scope(exit) client.unlock(); 456 auto collection = (*(*client) ["newBase"])["newCollection"]; 457 458 if(collection.count) { 459 collection.drop(); 460 } 461 auto toInsert = A(); 462 toInsert.assocBoos["a"] ~= B("b"); 463 toInsert.boos ~= B("b2"); 464 collection.insert(toInsert); 465 // If it finds extra fields in WithoutBoo (in this case boo), it would error out. 466 auto foundBSON = collection.findOne!AWithoutBoo; 467 assert(foundBSON.assocBoos["a"][0] == WithoutBoo("b")); 468 assert(foundBSON.boos[0] == WithoutBoo("b2")); 469 470 // Insert a default substruct 471 toInsert.boos[0].hello = ""; 472 } 473 474 475 import std.conv : to, text; 476 shared static this () { 477 mongoc_init(); 478 } 479 // Warning: Can be called before class destructors. 480 shared static ~this () { 481 mongoc_cleanup(); 482 } 483 484 import std..string : toStringz; 485 import std.array : Appender; 486 /// Wrapper over mongoc_client_pool_t that allows getting a client with 487 /// lock(). 488 /// After usage this should be freed with unlock(). Note that this.unlock isn't 489 /// the reverse operation of this.lock. 490 /// Clients should use their unlock(). 491 struct MongoPool { 492 mongoc_client_pool_t* pool = null; 493 mongoc_uri_t* uri = null; 494 /// This is optional, allows cleaning when this.unlock() is called. 495 /// But it keeps growing. 496 debug Appender!(Client * []) toDestroy; 497 this(string connectionString) { 498 this.uri = mongoc_uri_new(connectionString.toStringz); 499 500 // Warning: If this happens, the destructor will be called too. 501 if(this.uri is null) 502 throw new Exception("Invalid mongo uri"); 503 504 this.pool = mongoc_client_pool_new(this.uri); 505 if(!this.pool) 506 throw new Exception("Error creating pool"); 507 } 508 509 /// Deallocates resources. 510 auto unlock() { 511 debug { 512 foreach (ref client; toDestroy.data) { 513 if (!client.deleted) { 514 import std.stdio; 515 "Warning: Didn't manually delete a client".writeln; 516 } 517 client.unlock(); 518 } 519 toDestroy.clear(); 520 } 521 522 if (this.uri) 523 mongoc_uri_destroy(this.uri); 524 if (this.pool) 525 mongoc_client_pool_destroy(this.pool); 526 this.uri = null; 527 this.pool = null; 528 } 529 530 Client * lock() { 531 auto mongocClient = mongoc_client_pool_pop(this.pool); 532 if(!mongocClient) { 533 throw new Exception(`Error creating client from pool`); 534 } 535 auto toReturn = new Client(mongocClient, this.pool); 536 debug { 537 toDestroy ~= toReturn; 538 } 539 return toReturn; 540 } 541 } 542 543 struct Client { 544 mongoc_client_t * client; 545 Appender!(Database * []) toDestroy; 546 // Null if this wasn't created from a pooler. 547 mongoc_client_pool_t * parentPool = null; 548 bool deleted = false; // Prevents deleting multiple times. 549 @disable this(); 550 this(string uri) { 551 this.client = mongoc_client_new(uri.toStringz); 552 if(!client) { 553 throw new Exception(`Error creating client.`); 554 } 555 } 556 this(mongoc_client_t * client, mongoc_client_pool_t * pool) { 557 assert(client && pool); 558 this.parentPool = pool; 559 this.client = client; 560 } 561 auto opIndex(string baseName) { 562 assert(client); 563 auto toReturn = new Database( 564 mongoc_client_get_database(client, baseName.toStringz) 565 ); 566 toDestroy ~= toReturn; 567 return toReturn; 568 } 569 /// Deallocates resources. 570 /// If this has a parentPool then it doesn't destroy this.client 571 auto unlock() { 572 if(deleted) return; 573 assert(client); 574 foreach(ref database; toDestroy.data) { 575 database.unlock(); 576 } 577 toDestroy.clear(); 578 if(this.parentPool) { 579 mongoc_client_pool_push(this.parentPool, this.client); 580 parentPool = null; 581 } else { 582 mongoc_client_destroy(client); 583 } 584 client = null; 585 deleted = true; 586 } 587 } 588 struct Database { 589 @disable this(); 590 mongoc_database_t * database; 591 Appender!(Collection []) toDestroy; 592 this(mongoc_database_t * database) { 593 this.database = database; 594 } 595 auto opIndex(string collectionName) { 596 assert(database); 597 auto toReturn = Collection( 598 mongoc_database_get_collection( 599 database, collectionName.toStringz 600 ) 601 ); 602 toDestroy ~= toReturn; 603 return toReturn; 604 } 605 auto unlock() { 606 assert(database); 607 foreach(ref collection; toDestroy.data) { 608 collection.unlock(); 609 } 610 toDestroy.clear; 611 mongoc_database_destroy(database); 612 database = null; 613 } 614 } 615 616 // Used for automatic setting of fields when doing operations. 617 // Note: As of now it updates the MongoDocUpdated on findAndModify and 618 // update and MongoDocCreated on insert. 619 // Maybe one should use https://docs.mongodb.com/manual/reference/operator/update/setOnInsert/ and $set with $currentDate 620 enum MongoDocCreated; 621 enum MongoDocUpdated; 622 623 struct Collection { 624 @disable this(); 625 mongoc_collection_t * collection; 626 this(mongoc_collection_t * collection) { 627 this.collection = collection; 628 } 629 auto unlock() { 630 assert (collection); 631 mongoc_collection_destroy(collection); 632 collection = null; 633 } 634 635 auto drop() { 636 assert(collection); 637 bson_error_t error; 638 if(!mongoc_collection_drop(collection, &error)) { 639 throw new Exception(text(`Error in drop: `, error)); 640 } 641 } 642 643 long count(T1 = BSON)( 644 T1 query = empty!T1() 645 , mongoc_query_flags_t flags = mongoc_query_flags_t.NONE 646 , long skip = 0 647 , long limit = 0 648 , const mongoc_read_prefs_t * readPrefs = null 649 ) { 650 assert(collection); 651 bson_error_t error; 652 long retValue = mongoc_collection_count( 653 collection 654 , flags 655 , ScopedBSON(query, false, false).data 656 , skip 657 , limit 658 , readPrefs 659 , &error 660 ); 661 if (retValue == -1) { 662 throw new Exception(text(`Error in Collection.count: `, error.message)); 663 } else { 664 return retValue; 665 } 666 } 667 668 /// Convenience method for calling mongoc's insert with the upsert flag. 669 auto findAndModify(T1 = BSON, T2 = BSON, T3 = BSON, T4 = BSON)( 670 ref T1 query 671 , T2 update 672 , T3 sort = empty!T3() 673 , T4 fields = empty!T4() 674 , bool remove = false 675 , bool upsert = false 676 , bool _new = true 677 , BSON reply = empty() 678 ) { 679 // Note: this assumes every document is updated, not created. 680 processMongoDocUDAs!MongoDocUpdated(update); 681 bson_error_t error; 682 if(!mongoc_collection_find_and_modify( 683 this.collection 684 // Don't ignore defaults for all the parameters. 685 , ScopedBSON(query, false, false).data 686 , ScopedBSON(sort, false).data 687 , ScopedBSON(update, false).data 688 , ScopedBSON(fields, false).data 689 , remove 690 , upsert 691 , _new 692 , reply.data 693 , &error 694 )) { 695 throw new Exception(text(`Error on findAndModify: `, error)); 696 } 697 } 698 699 /// Takes either one query and document or 700 /// a range of queries and another of documents.. 701 auto update(T1 = BSON, T2 = BSON, T3 = BSON)( 702 T1 queries 703 , ref T2 documents // ref so that MongoDocUpdated can be set. 704 , T3 options = empty!T3() 705 ){ 706 static if(!isInputRange!T1) { 707 // Single document update. 708 alias query = queries; 709 alias document = documents; 710 711 bson_error_t error; 712 processMongoDocUDAs!MongoDocUpdated(document); 713 if(!mongoc_collection_update_one( 714 this.collection 715 , ScopedBSON(query, false, false).data 716 , ScopedBSON(document, false, false).data 717 , ScopedBSON(options, false).data 718 , ScopedBSON(empty(), false).data //Reply 719 , &error 720 )) { 721 throw new Exception(text(`Error inserting: `, error.message)); 722 } 723 } else { 724 // Queries and documents are ranges => Bulk update. 725 static assert( 726 isInputRange!T2 727 , `Both queries and documents should be ranges or single data` 728 ); 729 730 assert(this.collection); 731 mongoc_bulk_operation_t* bulk; 732 bulk = mongoc_collection_create_bulk_operation_with_opts( 733 this.collection 734 , null // opts 735 ); 736 if(!bulk) throw new Exception(`Error creating bulk operation`); 737 scope(exit) mongoc_bulk_operation_destroy(bulk); 738 bson_error_t error; 739 740 // Note: Name clash with empty. 741 import std.range : empty, front, popFront; 742 foreach(ref document; documents) { 743 // Assumes just setting updated is enough. 744 processMongoDocUDAs!MongoDocUpdated(document); 745 assert(!queries.empty, `More documents than queries`); 746 if(!mongoc_bulk_operation_update_one_with_opts( 747 bulk 748 , ScopedBSON(queries.front, false, false).data 749 , ScopedBSON(document, false, false).data 750 , ScopedBSON(options, false).data 751 , &error 752 )) { 753 throw new Exception(text( 754 `Error adding document to bulk update ` 755 , error.message) 756 ); 757 } 758 queries.popFront(); 759 } 760 assert(queries.empty, `More queries than documents`); 761 if(!mongoc_bulk_operation_execute( 762 bulk 763 , null /*reply that must be freed*/ 764 , &error 765 )) { 766 throw new Exception(text(`Error executing bulk operation`, error)); 767 } 768 } 769 } 770 771 /// If toInsert is a range, then bulk inserts are used. 772 auto insert(T1, T2 = BSON)( 773 ref T1 toInsert 774 , T2 options = empty!T2() 775 , BSON reply = empty() 776 ) { 777 assert(this.collection); 778 static if(isForwardRange!T1) { 779 // Ranges are inserted in bulk. 780 import std.algorithm : count; 781 auto amount = count(toInsert); 782 assert(amount > 0); 783 bson_t* [] toSend = []; 784 import std.range.primitives : hasLength; 785 static if (hasLength!T1) { 786 toSend.reserve(toInsert.length); 787 } 788 Appender!(bson_oid_t []) toReturn; 789 // Possible optimization: Create array on the stack 790 // instead of allocating BSONs. 791 foreach(ref element; toInsert) { 792 processMongoDocUDAs!MongoDocCreated(element); 793 auto toAppend = element.toBSON; 794 if (!toAppend.data.hasID) { 795 bson_oid_t id; 796 bson_oid_init(&id, null); 797 toAppend.append(`_id`, id); 798 toReturn ~= id; 799 } else { 800 toReturn ~= findID(toAppend.data); 801 } 802 toSend ~= toAppend.data; 803 } 804 this.insertMany(toSend, options, reply); 805 foreach(toDestroy; toSend) { 806 bson_destroy(toDestroy); 807 } 808 return toReturn.data; 809 } else { 810 // Single document insertion. 811 bson_error_t error; 812 processMongoDocUDAs!MongoDocCreated(toInsert); 813 auto toInsertAsBSON = ScopedBSON(toInsert, true, false); 814 bson_oid_t toReturn; 815 if (!toInsertAsBSON.data.hasID) { 816 bson_oid_init(&toReturn, null); 817 toInsertAsBSON.append(`_id`, toReturn); 818 } else { 819 toReturn = findID(toInsertAsBSON.data); 820 } 821 822 if(!mongoc_collection_insert_one( 823 this.collection 824 , toInsertAsBSON.data 825 , ScopedBSON(options, false).data 826 , reply.data 827 , &error 828 )) { 829 throw new Exception(text(`Error inserting: `, error)); 830 } 831 return toReturn; 832 } 833 } 834 835 836 /// Useful if you already have an array of bson_t, otherwise it's better 837 /// to use insert with a range as it builds that array. 838 auto insertMany(T = BSON)( 839 bson_t * [] toInsert 840 , T options = empty!T() 841 , BSON reply = empty() 842 ) { 843 bson_error_t error; 844 assert(this.collection); 845 if(!mongoc_collection_insert_many( 846 this.collection 847 , toInsert.ptr 848 , toInsert.length 849 , ScopedBSON(options, false).data 850 , reply.data 851 , &error 852 )) { 853 throw new Exception(text(`Error bulk-inserting: `, error)); 854 } 855 } 856 857 /// Convenience function for getting just one element from a find. 858 /// Must be deleted with unlock(). 859 ReturnType findOne(ReturnType = BSON, T1 = BSON, T2 = BSON)( 860 T1 filter = empty!T1() 861 , T2 options = empty!T2() 862 , const mongoc_read_prefs_t * readPrefs = null 863 ) { 864 auto cursor = this.find(filter, options, readPrefs); 865 scope(exit) cursor.unlock(); 866 if(cursor.empty) { 867 throw new Exception(`Didn't find on findOne: ` ~ filter.to!string); 868 } 869 auto data = bson_copy(cursor.front.data); 870 if(!data) { 871 throw new Exception(`Couldn't allocate BSON`); 872 } 873 static if(is(ReturnType == BSON)) { 874 return BSON(data); 875 } else { 876 return(ScopedBSON(BSON(data)).as!ReturnType); 877 } 878 } 879 880 auto find(ReturnType = BSON, T1 = BSON, T2 = BSON)( 881 T1 filter = empty!T1() 882 , T2 options = empty!T2() 883 , const mongoc_read_prefs_t * readPrefs = null 884 ) { 885 assert(this.collection); 886 return Cursor!ReturnType(mongoc_collection_find_with_opts( 887 this.collection 888 , ScopedBSON(filter, false, false).data 889 , ScopedBSON(options, false).data 890 , readPrefs 891 )); 892 } 893 894 auto deleteOne(T1 = BSON,T2 = BSON)( 895 T1 selector = empty!T1() 896 , T2 options = empty!T2() 897 , BSON reply = empty() 898 ) { 899 assert(collection); 900 bson_error_t error; 901 if(!mongoc_collection_delete_one ( 902 this.collection 903 , ScopedBSON(selector, false, false).data 904 , ScopedBSON(options).data 905 , reply.data 906 , &error 907 )) { 908 throw new Exception(text(`Couldn't delete `, error)); 909 } 910 } 911 912 /// Sets the fields with UDA of struct T with the current unix time. 913 /// Does nothing if T is a BSON. 914 void processMongoDocUDAs(alias UDA, T)(ref T val) { 915 static if(__traits(isPOD, T) && isAggregateType!T) { 916 alias created = getSymbolsByUDA!(T, UDA); 917 static assert( 918 created.length < 2 919 , `Are you sure you want several ` ~ UDA.stringof 920 ~ ` symbols in ` ~ S.stringof ~ `?` 921 ); 922 import std.datetime; 923 static if(created.length) { 924 static assert(is(typeof(created [0]) == long)); 925 // Eg. val.insertedTime = Clock.currTime(UTC()).toUnixTime; 926 mixin( 927 `val.` ~ __traits(identifier, created [0]) 928 ~ ` = Clock.currTime(UTC()).toUnixTime;` 929 ); 930 } 931 } else { 932 static assert( 933 is(T == BSON) 934 , `Cannot process mongo UDAs for type ` ~ T.stringof 935 ); 936 } 937 } 938 } 939 940 enum MongoKeep; 941 942 /// Checks whether the instance of 'Type' has the default value on 'field' 943 /// if it does and isn't marked with the MongoKeep UDA does nothing, 944 /// else appends that field to the BSON. 945 /// In case the field is a struct or array, it checks recursively. 946 bool checkIfDefault(string field, Type)( 947 Type instance 948 , ref BSON toAppendTo 949 , bool ignoreDefaults 950 ) { 951 auto instanceField = __traits(getMember, instance, field); 952 953 mixin(`alias SubField = Type.` ~ field ~ `;`); 954 alias SubType = typeof(SubField); 955 // Save only the fields with non default values or the ones that 956 // have the @MongoKeep UDA. 957 if( 958 (!ignoreDefaults) 959 || instanceField != __traits(getMember, Type.init, field) 960 || hasUDA!(mixin(`Type.` ~ field), MongoKeep) 961 ) { 962 // Fields with string UDAs should append documents with the document key 963 // as a string, for example @("$set") int a = 3; 964 // would add a $set : { "a" : 3 } 965 toAppendTo.append(strUDA!(Type, field), instanceField, ignoreDefaults); 966 return true; 967 } 968 return false; 969 } 970 971 /// Fills a BSON with the members from a struct. 972 /// if ignoreDefaults is true, then values with the default value aren't added. 973 /// this behavior is useful to toggle it on insertions but off for option 974 /// or query parameters. 975 bool fillBSON(Type)(Type instance, ref BSON toFill, bool ignoreDefaults = true) { 976 bool toReturn = false; 977 static foreach(field; FieldNameTuple!Type) { 978 toReturn |= checkIfDefault!field(instance, toFill, ignoreDefaults); 979 } 980 return toReturn; 981 } 982 983 // Search for a string UDA and return it's value. 984 string strUDA (alias Type, string fieldName)() { 985 string toReturn = fieldName; 986 mixin(`alias FieldType = Type.` ~ fieldName ~ `;`); 987 static foreach(uda; __traits(getAttributes, mixin(`Type.` ~ fieldName))) { 988 static if(is(typeof(uda) == string)) { 989 toReturn = uda; 990 } 991 } 992 return toReturn; 993 } 994 995 /// ignoreDefaults does nothing, is just for compatibility with the other toBSON. 996 auto ref toBSON(BSON ob, bool ignoreDefaults = true) {return ob;} 997 /// Converts a POD struct to a BSON object. 998 auto toBSON(Type)(Type instance, bool ignoreDefaults = true) { 999 static assert(!is(Type == BSON)); 1000 static assert( 1001 __traits(isPOD, Type) && isAggregateType!Type 1002 , `bson(instance) is only implemented for POD structs` 1003 ); 1004 1005 auto toReturn = empty!(BSON, true)(); 1006 instance.fillBSON(toReturn, ignoreDefaults); 1007 return toReturn; 1008 } 1009 1010 /// A BSON that is destroyed if created from a struct on its destructor and 1011 /// just a wrapper over an existing BSON otherwise. 1012 struct ScopedBSON { 1013 BSON bson; 1014 bool deleteOnDestructor = false; 1015 @disable this(); 1016 // unused is for consistency with the other constructor. 1017 this(ref BSON other, bool unused = true, bool allowNull = true){ 1018 if (!other.data && !allowNull){ 1019 this.bson = empty!(BSON, true); 1020 deleteOnDestructor = true; 1021 } else { 1022 this.bson = other; 1023 } 1024 } 1025 this(S)(S other, bool ignoreDefaults = true, bool unused = true){ 1026 this.bson = other.toBSON(ignoreDefaults); 1027 deleteOnDestructor = true; 1028 } 1029 ~this(){ 1030 if(deleteOnDestructor){ 1031 bson.unlock(); 1032 } 1033 } 1034 alias bson this; 1035 1036 } 1037 1038 /// Used for iterating mongoc_cursor_ts as input ranges. 1039 struct Cursor(ElementType = BSON){ 1040 mongoc_cursor_t * cursor; 1041 @disable this(); 1042 this(mongoc_cursor_t * cursor) { 1043 this._front = .empty(); 1044 this.cursor = cursor; 1045 popFront (); 1046 } 1047 // Note: _front.unlock () shouldn't be called, mongoc sets it automatically. 1048 BSON _front; 1049 bool empty = false; 1050 auto popFront () { 1051 if(!mongoc_cursor_next(cursor, &_front.data)) { 1052 bson_error_t error; 1053 if (mongoc_cursor_error(this.cursor, &error)) { 1054 throw new Exception(`Cursor error`); 1055 } else { 1056 // Empty cursor. 1057 this.unlock(); 1058 } 1059 } 1060 } 1061 1062 ElementType front () { 1063 static if(is(ElementType == BSON)) { 1064 return _front; 1065 } else { 1066 return _front.as!ElementType; 1067 } 1068 } 1069 1070 /// Called automatically on exhaustion, useful if not all the elements are 1071 /// desired. 1072 void unlock() { 1073 if(this.cursor) { 1074 mongoc_cursor_destroy(this.cursor); 1075 this.cursor = null; 1076 empty = true; 1077 } 1078 } 1079 } 1080 import std.range : isInputRange, isForwardRange; 1081 static assert(isInputRange!(Cursor!BSON)); 1082 1083 /// Creates a BSON from a JSON string. 1084 /// The BSON needs to be destroyed manually with unlock() 1085 BSON fromJSON(string json) { 1086 bson_error_t error; 1087 bson_t * data; 1088 data = bson_new_from_json( 1089 cast (const ubyte*) json 1090 , json.length.to!ssize_t 1091 , &error 1092 ); 1093 if (!data) { 1094 throw new Exception(text(`Error converting JSON to BSON: `, error)); 1095 } 1096 auto toReturn = BSON(data); 1097 return toReturn; 1098 } 1099 1100 auto empty(Type = BSON, bool initialize = false)() { 1101 static if(is(Type == BSON)) { 1102 static if(!initialize) { 1103 return BSON(null); 1104 } else { 1105 auto toReturn = BSON(null); 1106 toReturn.initialize(); 1107 return toReturn; 1108 } 1109 } else { 1110 return Type.init; 1111 } 1112 } 1113 1114 bool hasID(const bson_t* val) { 1115 assert(val); 1116 return bson_has_field(val, `_id`.toStringz); 1117 } 1118 1119 /// Returns the _id field of a bson_t, it must have it and be of type OID. 1120 bson_oid_t findID(const bson_t* val) { 1121 bson_iter_t idPtr; 1122 bool success = bson_iter_init_find(&idPtr, val, `_id`); 1123 assert(success, `Didn't find _id field but should have one`); 1124 assert(bson_iter_type(&idPtr) == bson_type_t.BSON_TYPE_OID); 1125 return *bson_iter_oid(&idPtr); 1126 } 1127 1128 struct BSON { 1129 @disable this(); 1130 // Could use a statically allocated bson_t but it gave problems with automatic 1131 // destruction on scope exits. 1132 bson_t * data = null; 1133 this(bson_t * data) { 1134 this.data = data; 1135 } 1136 /// empty is used for creating Bsons without fields. Structs cannot have a 1137 /// constructor without parameters, so it's a workaround. 1138 this(Args...)(Args args) { 1139 static assert ( 1140 args.length % 2 == 0 1141 , `BSON constructor requires an even number of parameters` 1142 ); 1143 this.initialize(); 1144 assert (0, `TODO`); 1145 } 1146 /// Used to new this.data. 1147 auto initialize() { 1148 this.data = bson_new (); 1149 if (!data) { 1150 throw new Exception(`Error creating Bson`); 1151 } 1152 } 1153 auto unlock() { 1154 if(this.data) { 1155 bson_destroy(data); 1156 this.data = null; 1157 } 1158 } 1159 string toString() const { 1160 if(!data) return `null`; 1161 auto str = bson_as_canonical_extended_json(data, null); 1162 if (!str) { 1163 throw new Exception(`Error creating string from BSON`); 1164 } 1165 scope(exit) bson_free(str); 1166 return str.to!string; 1167 } 1168 1169 /// name must be ASCII 1170 void append(T)(string name, T val, bool ignoreSubDocDefaults = false) { 1171 if(!data) this.initialize(); 1172 import std.ascii : isASCII; 1173 import std.algorithm : all; 1174 debug assert(name.all!isASCII); 1175 bool success = true; 1176 import std.datetime : SysTime; 1177 import std.range : isInputRange; 1178 // Abstracts the common pattern of append operations. 1179 bool appendOp(alias fun, Args...)(Args args){ 1180 assert(data); 1181 return fun( 1182 data 1183 , name.toStringz 1184 , name.length.to!int 1185 , args 1186 ); 1187 } 1188 static if(is(OriginalType!T == string)) { 1189 if(name == `_id`) { // Automatic conversion to bson_oid_t 1190 auto toAppend = val.toId; 1191 success = appendOp!bson_append_oid( 1192 &toAppend 1193 ); 1194 } else { 1195 success = appendOp!bson_append_utf8( 1196 val.toStringz 1197 , val.length.to!int 1198 ); 1199 } 1200 } else static if(is(T == SysTime)) { 1201 // 4 parameters but must cast to Unix time. 1202 success = appendOp!bson_append_date_time( 1203 val.toUnixTime 1204 ); 1205 } else static if(is(T == BSON)) { 1206 success = appendOp!bson_append_document(val.data); 1207 } else static if(isInputRange!T) { 1208 // Append as array: 1209 BSON arr = empty!(BSON, true)(); 1210 // TODO: Check if needed: //scope(exit) arr.unlock(); 1211 1212 success = appendOp!bson_append_array_begin(arr.data); 1213 foreach(i, element; val) { 1214 arr.append(i.to!string, element, ignoreSubDocDefaults); 1215 } 1216 success &= bson_append_array_end( 1217 data 1218 , arr.data 1219 ); 1220 } else static if(isAssociativeArray!T && is(T Key: Key[Value], Value)){ 1221 BSON arr = empty!(BSON, true)(); 1222 success = appendOp!bson_append_array_begin(arr.data); 1223 foreach(key, value; val) { 1224 arr.append(key, value, ignoreSubDocDefaults); 1225 } 1226 success &= bson_append_array_end( 1227 data 1228 , arr.data 1229 ); 1230 1231 } else static if(is(T == bson_oid_t)){ 1232 success = appendOp!bson_append_oid(&val); 1233 } else static if(isAggregateType!T && !is(T == bson_oid_t)) { 1234 BSON toAppend = val.toBSON(ignoreSubDocDefaults); 1235 success = appendOp!bson_append_document(toAppend.data); 1236 // TODO: Check if toAppend.unlock is needed. 1237 } else { 1238 // 4 parameter appends. 1239 static if(is(OriginalType!T == int)) { 1240 alias fun = bson_append_int32; 1241 } else static if(is(OriginalType!T == long)) { 1242 alias fun = bson_append_int64; 1243 } else static if(is(OriginalType!T == bool)) { 1244 alias fun = bson_append_bool; 1245 } else static if(is(OriginalType!T == double)) { 1246 alias fun = bson_append_double; 1247 } else { 1248 static assert(0, `Unrecognised type for appending to BSON `~ T.stringof); 1249 } 1250 success = appendOp!fun(val); 1251 } 1252 if (!success) { 1253 throw new Exception(`Error appending`); 1254 } 1255 } 1256 1257 auto byKeyValue() { 1258 return BSONIter(data); 1259 } 1260 1261 // Note, this does seem to be O(n). 1262 auto key(Type)(string key) { 1263 assert(data); 1264 bson_iter_t iterator; 1265 if(!bson_iter_init_find(&iterator, data, key.toStringz)) { 1266 throw new Exception(`Problem looking for key ` ~ key ~ ` in BSON`); 1267 } 1268 return bson_iter_value(&iterator).as!Type; 1269 1270 } 1271 T as(T)() { 1272 static assert( 1273 __traits(isPOD, T) && isAggregateType!T 1274 , `BSON.as is made for POD structs. Check the code before using it with ` 1275 ~ T.stringof 1276 ); 1277 alias typeFields = FieldNameTuple!T; 1278 T toReturn; 1279 foreach(key, value; this.byKeyValue){ 1280 outerSwitch: switch(key) { 1281 static foreach(field; typeFields) { 1282 case field: 1283 alias FieldType = typeof(mixin(`T.` ~ field)); 1284 FieldType toAssign; 1285 static if (field == `_id` && is(FieldType == string)) { 1286 toAssign = value.as!bson_oid_t.fromId; 1287 } else { 1288 toAssign = value.as!FieldType; 1289 } 1290 enum fieldToAssign = `toReturn.` ~ field; 1291 mixin(fieldToAssign ~ ` = toAssign;`); 1292 break outerSwitch; 1293 } 1294 default: 1295 // _id is the only field that is allowed to be on the BSON 1296 // and not on the struct, if bo has some other field that the 1297 // struct doesn't, an exception is thrown. 1298 if(key != `_id`){ 1299 throw new Exception (`Found member of BSON that is not in ` 1300 ~ T.stringof ~ ` : ` ~ key); 1301 } 1302 } 1303 } 1304 return toReturn; 1305 } 1306 //alias data this; 1307 } 1308 1309 /// Used just for 'as' function. 1310 struct DateTime { 1311 long value; 1312 alias value this; 1313 } 1314 1315 /// Converts a bson_value_t to another type. 1316 /// If BSON is used as a type, it assumes it's a document. 1317 /// Make sure to unlock() that BSON. 1318 auto as(type)(bson_value_t * val) { 1319 assert(val); 1320 auto vval = val.value; 1321 auto vtype = val.value_type; 1322 static if(is(OriginalType!type == int)){ 1323 assert(vtype == bson_type_t.BSON_TYPE_INT32); 1324 return to!type(vval.v_int32); 1325 } else static if(is(OriginalType!type == long)) { 1326 assert(vtype == bson_type_t.BSON_TYPE_INT64); 1327 return to!type(vval.v_int64); 1328 } else static if(is(OriginalType!type == bool)) { 1329 assert(vtype == bson_type_t.BSON_TYPE_BOOL); 1330 return to!type(vval.v_bool); 1331 } else static if(is(OriginalType!type == string)) { 1332 assert(vtype == bson_type_t.BSON_TYPE_UTF8); 1333 auto toReturn = vval.v_utf8.str[0..vval.v_utf8.len]; 1334 return to!type(toReturn.to!string); 1335 } else static if(is(OriginalType!type == double)) { 1336 assert(vtype == bson_type_t.BSON_TYPE_DOUBLE); 1337 return to!type(vval.v_double); 1338 } else static if(is(type == bson_oid_t)) { 1339 assert(vtype == bson_type_t.BSON_TYPE_OID); 1340 return vval.v_oid; 1341 } else static if(is(type == BSON)) { 1342 assert(vtype == bson_type_t.BSON_TYPE_DOCUMENT); 1343 auto toReturn = bson_new_from_data(vval.v_doc.data, vval.v_doc.data_len); 1344 return BSON(toReturn); 1345 } else static if(is(type == DateTime)) { 1346 assert(vtype == bson_type_t.BSON_TYPE_DATE_TIME); 1347 return DateTime(vval.v_datetime); 1348 } else static if(isArray!type) { 1349 assert(vtype == bson_type_t.BSON_TYPE_ARRAY); 1350 auto asDoc = bson_new_from_data(vval.v_doc.data, vval.v_doc.data_len); 1351 auto asBSON = BSON(asDoc); scope(exit) asBSON.unlock(); 1352 import std.array : Appender; 1353 import std.range : ElementType; 1354 Appender!type toReturn; 1355 foreach(element; BSONIter(asBSON.data)) { 1356 import std.algorithm : map; 1357 toReturn ~= element.value.as!(ElementType!type); 1358 } 1359 return toReturn.data; 1360 } else static if(isAssociativeArray!type && is(type Value: Value[Key], Key)) { 1361 assert(vtype == bson_type_t.BSON_TYPE_ARRAY); 1362 auto asDoc = bson_new_from_data(vval.v_doc.data, vval.v_doc.data_len); 1363 auto asBSON = BSON(asDoc); scope(exit) asBSON.unlock(); 1364 type toReturn; 1365 foreach(key, value; BSONIter(asBSON.data)) { 1366 toReturn[key] = value.as!Value; 1367 } 1368 return toReturn; 1369 } else static if(isAggregateType!type) { 1370 assert(vtype == bson_type_t.BSON_TYPE_DOCUMENT); 1371 auto docBSON = val.as!BSON; 1372 scope(exit) docBSON.unlock(); 1373 return docBSON.as!type; 1374 } else static assert (0, `TODO: as!` ~ type.stringof); 1375 } 1376 1377 /// Empty if constructed with null. 1378 struct BSONIter { 1379 bson_iter_t iter; 1380 @disable this(); 1381 bool empty = false; 1382 auto front() { 1383 assert(!empty); 1384 import std.typecons : Tuple; 1385 return Tuple!(string, `key`, bson_value_t *, `value`)( 1386 bson_iter_key(&iter).to!string 1387 , bson_iter_value(&iter) 1388 ); 1389 } 1390 auto popFront () { 1391 assert(!empty); 1392 if(!bson_iter_next(&iter)) { 1393 this.empty = true; 1394 } 1395 } 1396 this(bson_t * toIterate) { 1397 if(!toIterate) { 1398 empty = true; 1399 } else { 1400 if(bson_iter_init (&iter, toIterate)) { 1401 popFront(); 1402 } else { 1403 throw new Exception (`Failed to create BSON iterator`); 1404 } 1405 } 1406 } 1407 } 1408 1409 /// Converts a string to an object id. 1410 bson_oid_t toId(string representation) { 1411 bson_oid_t toReturn; 1412 auto cstr = representation.toStringz; 1413 if(!bson_oid_is_valid(cstr, representation.length)) { 1414 throw new Exception(`Invalid string for _id conversion`); 1415 } 1416 bson_oid_init_from_string(&toReturn, cstr); 1417 return toReturn; 1418 } 1419 1420 string fromId(bson_oid_t id) { 1421 char[25] toInsertTo; //Includes \0 1422 bson_oid_to_string(&id, toInsertTo.ptr); 1423 return toInsertTo[0..24].to!string; 1424 }