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 }