1 /// This module provides the database utility tools which make the whole project useful.
2 module mongoschema.db;
3 
4 import core.time;
5 
6 import mongoschema;
7 
8 import std.traits;
9 import std.typecons : BitFlags, tuple, Tuple;
10 
11 /// Range for iterating over a collection using a Schema.
12 struct DocumentRange(Schema)
13 {
14 	alias Cursor = MongoCursor!Bson;
15 
16 	private Cursor _cursor;
17 
18 	public this(Cursor cursor)
19 	{
20 		_cursor = cursor;
21 	}
22 
23 	/**
24 		Returns true if there are no more documents for this cursor.
25 
26 		Throws: An exception if there is a query or communication error.
27 	*/
28 	@property bool empty()
29 	{
30 		return _cursor.empty;
31 	}
32 
33 	/**
34 		Returns the current document of the response.
35 
36 		Use empty and popFront to iterate over the list of documents using an
37 		input range interface. Note that calling this function is only allowed
38 		if empty returns false.
39 	*/
40 	@property Schema front()
41 	{
42 		return fromSchemaBson!Schema(_cursor.front);
43 	}
44 
45 	/**
46 		Controls the order in which the query returns matching documents.
47 
48 		This method must be called before starting to iterate, or an exeption
49 		will be thrown. If multiple calls to $(D sort()) are issued, only
50 		the last one will have an effect.
51 
52 		Params:
53 			order = A BSON object convertible value that defines the sort order
54 				of the result. This BSON object must be structured according to
55 				the MongoDB documentation (see below).
56 
57 		Returns: Reference to the modified original curser instance.
58 
59 		Throws:
60 			An exception if there is a query or communication error.
61 			Also throws if the method was called after beginning of iteration.
62 
63 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort)
64 	*/
65 	auto sort(T)(T order)
66 	{
67 		_cursor.sort(serializeToBson(order));
68 		return this;
69 	}
70 
71 	/**
72 		Limits the number of documents that the cursor returns.
73 
74 		This method must be called before beginnig iteration in order to have
75 		effect. If multiple calls to limit() are made, the one with the lowest
76 		limit will be chosen.
77 
78 		Params:
79 			count = The maximum number number of documents to return. A value
80 				of zero means unlimited.
81 
82 		Returns: the same cursor
83 
84 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit)
85 	*/
86 	auto limit(size_t count)
87 	{
88 		_cursor.limit(count);
89 		return this;
90 	}
91 
92 	/**
93 		Skips a given number of elements at the beginning of the cursor.
94 
95 		This method must be called before beginnig iteration in order to have
96 		effect. If multiple calls to skip() are made, the one with the maximum
97 		number will be chosen.
98 
99 		Params:
100 			count = The number of documents to skip.
101 
102 		Returns: the same cursor
103 
104 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip)
105 	*/
106 	auto skip(int count)
107 	{
108 		_cursor.skip(count);
109 		return this;
110 	}
111 
112 	/**
113 		Advances the cursor to the next document of the response.
114 
115 		Note that calling this function is only allowed if empty returns false.
116 	*/
117 	void popFront()
118 	{
119 		_cursor.popFront();
120 	}
121 
122 	/**
123 		Iterates over all remaining documents.
124 
125 		Note that iteration is one-way - elements that have already been visited
126 		will not be visited again if another iteration is done.
127 
128 		Throws: An exception if there is a query or communication error.
129 	*/
130 	int opApply(int delegate(Schema doc) del)
131 	{
132 		while (!_cursor.empty)
133 		{
134 			auto doc = _cursor.front;
135 			_cursor.popFront();
136 			if (auto ret = del(fromSchemaBson!Schema(doc)))
137 				return ret;
138 		}
139 		return 0;
140 	}
141 }
142 
143 /// Exception thrown if a document could not be found.
144 class DocumentNotFoundException : Exception
145 {
146 	///
147 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe
148 	{
149 		super(msg, file, line);
150 	}
151 }
152 
153 ///
154 struct PipelineUnwindOperation
155 {
156 	/// Field path to an array field. To specify a field path, prefix the field name with a dollar sign $.
157 	string path;
158 	/// Optional. The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $.
159 	string includeArrayIndex = null;
160 }
161 
162 ///
163 struct SchemaPipeline
164 {
165 @safe:
166 	this(MongoCollection collection)
167 	{
168 		_collection = collection;
169 	}
170 
171 	/// Passes along the documents with only the specified fields to the next stage in the pipeline. The specified fields can be existing fields from the input documents or newly computed fields.
172 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
173 	SchemaPipeline project(Bson specifications)
174 	{
175 		assert(!finalized);
176 		pipeline ~= Bson(["$project": specifications]);
177 		return this;
178 	}
179 
180 	/// ditto
181 	SchemaPipeline project(T)(T specifications)
182 	{
183 		return project(serializeToBson(specifications));
184 	}
185 
186 	/// Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage.
187 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match
188 	SchemaPipeline match(Bson query)
189 	{
190 		assert(!finalized);
191 		pipeline ~= Bson(["$match": query]);
192 		return this;
193 	}
194 
195 	/// ditto
196 	SchemaPipeline match(T)(Query!T query)
197 	{
198 		return match(query._query);
199 	}
200 
201 	/// ditto
202 	SchemaPipeline match(T)(T[string] query)
203 	{
204 		return match(serializeToBson(query));
205 	}
206 
207 	/// Restricts the contents of the documents based on information stored in the documents themselves.
208 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact
209 	SchemaPipeline redact(Bson expression)
210 	{
211 		assert(!finalized);
212 		pipeline ~= Bson(["$redact": expression]);
213 		return this;
214 	}
215 
216 	/// ditto
217 	SchemaPipeline redact(T)(T expression)
218 	{
219 		return redact(serializeToBson(expression));
220 	}
221 
222 	/// Limits the number of documents passed to the next stage in the pipeline.
223 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit
224 	SchemaPipeline limit(size_t count)
225 	{
226 		assert(!finalized);
227 		pipeline ~= Bson(["$limit": Bson(count)]);
228 		return this;
229 	}
230 
231 	/// Skips over the specified number of documents that pass into the stage and passes the remaining documents to the next stage in the pipeline.
232 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip
233 	SchemaPipeline skip(size_t count)
234 	{
235 		assert(!finalized);
236 		pipeline ~= Bson(["$skip": Bson(count)]);
237 		return this;
238 	}
239 
240 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
241 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
242 	SchemaPipeline unwind(string path)
243 	{
244 		assert(!finalized);
245 		pipeline ~= Bson(["$unwind": Bson(path)]);
246 		return this;
247 	}
248 
249 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
250 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
251 	SchemaPipeline unwind(PipelineUnwindOperation op)
252 	{
253 		assert(!finalized);
254 		Bson opb = Bson(["path": Bson(op.path)]);
255 		if (op.includeArrayIndex !is null)
256 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
257 		pipeline ~= Bson(["$unwind": opb]);
258 		return this;
259 	}
260 
261 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
262 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
263 	SchemaPipeline unwind(PipelineUnwindOperation op, bool preserveNullAndEmptyArrays)
264 	{
265 		assert(!finalized);
266 		Bson opb = Bson([
267 				"path": Bson(op.path),
268 				"preserveNullAndEmptyArrays": Bson(preserveNullAndEmptyArrays)
269 				]);
270 		if (op.includeArrayIndex !is null)
271 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
272 		pipeline ~= Bson(["$unwind": opb]);
273 		return this;
274 	}
275 
276 	/// Groups documents by some specified expression and outputs to the next stage a document for each distinct grouping. The output documents contain an _id field which contains the distinct group by key. The output documents can also contain computed fields that hold the values of some accumulator expression grouped by the $group‘s _id field. $group does not order its output documents.
277 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
278 	SchemaPipeline group(Bson id, Bson accumulators)
279 	{
280 		assert(!finalized);
281 		accumulators["_id"] = id;
282 		pipeline ~= Bson(["$group": accumulators]);
283 		return this;
284 	}
285 
286 	/// ditto
287 	SchemaPipeline group(K, T)(K id, T[string] accumulators)
288 	{
289 		return group(serializeToBson(id), serializeToBson(accumulators));
290 	}
291 
292 	/// Groups all documents into one specified with the accumulators. Basically just runs group(null, accumulators)
293 	SchemaPipeline groupAll(Bson accumulators)
294 	{
295 		assert(!finalized);
296 		accumulators["_id"] = Bson(null);
297 		pipeline ~= Bson(["$group": accumulators]);
298 		return this;
299 	}
300 
301 	/// ditto
302 	SchemaPipeline groupAll(T)(T[string] accumulators)
303 	{
304 		return groupAll(serializeToBson(accumulators));
305 	}
306 
307 	/// Randomly selects the specified number of documents from its input.
308 	/// Warning: $sample may output the same document more than once in its result set.
309 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sample/#pipe._S_sample
310 	SchemaPipeline sample(size_t count)
311 	{
312 		assert(!finalized);
313 		pipeline ~= Bson(["$sample": Bson(count)]);
314 		return this;
315 	}
316 
317 	/// Sorts all input documents and returns them to the pipeline in sorted order.
318 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort
319 	SchemaPipeline sort(Bson sorter)
320 	{
321 		assert(!finalized);
322 		pipeline ~= Bson(["$sort": sorter]);
323 		return this;
324 	}
325 
326 	/// ditto
327 	SchemaPipeline sort(T)(T sorter)
328 	{
329 		return sort(serializeToBson(sorter));
330 	}
331 
332 	/// Outputs documents in order of nearest to farthest from a specified point.
333 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear
334 	SchemaPipeline geoNear(Bson options)
335 	{
336 		assert(!finalized);
337 		pipeline ~= Bson(["$geoNear": options]);
338 		return this;
339 	}
340 
341 	/// ditto
342 	SchemaPipeline geoNear(T)(T options)
343 	{
344 		return geoNear(serializeToBson(options));
345 	}
346 
347 	/// Performs a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing. The $lookup stage does an equality match between a field from the input documents with a field from the documents of the “joined” collection.
348 	/// To each input document, the $lookup stage adds a new array field whose elements are the matching documents from the “joined” collection. The $lookup stage passes these reshaped documents to the next stage.
349 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup
350 	SchemaPipeline lookup(Bson options)
351 	{
352 		assert(!finalized);
353 		pipeline ~= Bson(["$lookup": options]);
354 		return this;
355 	}
356 
357 	/// ditto
358 	SchemaPipeline lookup(T)(T options)
359 	{
360 		return lookup(serializeToBson(options));
361 	}
362 
363 	/// Takes the documents returned by the aggregation pipeline and writes them to a specified collection. The $out operator must be the last stage in the pipeline. The $out operator lets the aggregation framework return result sets of any size.
364 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out
365 	SchemaPipeline outputTo(string outputCollection)
366 	{
367 		assert(!finalized);
368 		debug finalized = true;
369 		pipeline ~= Bson(["$out": Bson(outputCollection)]);
370 		return this;
371 	}
372 
373 	/// Returns statistics regarding the use of each index for the collection. If running with access control, the user must have privileges that include indexStats action.
374 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/#pipe._S_indexStats
375 	SchemaPipeline indexStats()
376 	{
377 		assert(!finalized);
378 		pipeline ~= Bson(["$indexStats": Bson.emptyObject]);
379 		return this;
380 	}
381 
382 	Bson run() @trusted // workaround because old vibe.d versions mistagged aggregate safety
383 	{
384 		debug finalized = true;
385 		return _collection.aggregate(pipeline);
386 	}
387 
388 	DocumentRange!T collect(T = Bson)(AggregateOptions options = AggregateOptions.init)
389 	{
390 		debug finalized = true;
391 		return _collection.aggregate!T(pipeline, options);
392 	}
393 
394 private:
395 	bool finalized = false;
396 	Bson[] pipeline;
397 	MongoCollection _collection;
398 }
399 
400 /// Mixin for functions for interacting with Mongo collections.
401 mixin template MongoSchema()
402 {
403 	import std.typecons : Nullable;
404 	import std.range : isInputRange, ElementType;
405 
406 	static MongoCollection _schema_collection_;
407 	private BsonObjectID _schema_object_id_;
408 
409 	@property static MongoCollection collection() @safe
410 	{
411 		return _schema_collection_;
412 	}
413 
414 	/// Returns: the _id value (if set by save or find)
415 	@property ref BsonObjectID bsonID() @safe return
416 	{
417 		return _schema_object_id_;
418 	}
419 
420 	/// ditto
421 	@property const(BsonObjectID) bsonID() const @safe
422 	{
423 		return _schema_object_id_;
424 	}
425 
426 	/// Inserts or updates an existing value.
427 	void save()
428 	{
429 		if (_schema_object_id_.valid)
430 		{
431 			collection.update(Bson(["_id": Bson(_schema_object_id_)]),
432 					this.toSchemaBson(), UpdateFlags.upsert);
433 		}
434 		else
435 		{
436 			_schema_object_id_ = BsonObjectID.generate;
437 			auto bson = this.toSchemaBson();
438 			collection.insert(bson);
439 		}
440 	}
441 
442 	/// Inserts or merges into an existing value.
443 	void merge()
444 	{
445 		if (_schema_object_id_.valid)
446 		{
447 			collection.update(Bson(["_id": Bson(_schema_object_id_)]),
448 					Bson(["$set": this.toSchemaBson()]), UpdateFlags.upsert);
449 		}
450 		else
451 		{
452 			_schema_object_id_ = BsonObjectID.generate;
453 			auto bson = this.toSchemaBson();
454 			collection.insert(bson);
455 		}
456 	}
457 
458 	/// Removes this object from the collection. Returns false when _id of this is not set.
459 	bool remove() @trusted const
460 	{
461 		if (!_schema_object_id_.valid)
462 			return false;
463 		collection.remove(Bson(["_id": Bson(_schema_object_id_)]), DeleteFlags.SingleRemove);
464 		return true;
465 	}
466 
467 	/// Tries to find one document in the collection.
468 	/// Throws: DocumentNotFoundException if not found
469 	static Bson findOneOrThrow(Query!(typeof(this)) query)
470 	{
471 		return findOneOrThrow(query._query);
472 	}
473 
474 	/// ditto
475 	static Bson findOneOrThrow(T)(T query)
476 	{
477 		Bson found = collection.findOne(query);
478 		if (found.isNull)
479 			throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof);
480 		return found;
481 	}
482 
483 	/// Finds one element with the object id `id`.
484 	/// Throws: DocumentNotFoundException if not found
485 	static typeof(this) findById(BsonObjectID id)
486 	{
487 		return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id": Bson(id)])));
488 	}
489 
490 	/// Finds one element with the hex id `id`.
491 	/// Throws: DocumentNotFoundException if not found
492 	static typeof(this) findById(string id)
493 	{
494 		return findById(BsonObjectID.fromString(id));
495 	}
496 
497 	/// Finds one element using a query.
498 	/// Throws: DocumentNotFoundException if not found
499 	static typeof(this) findOne(Query!(typeof(this)) query)
500 	{
501 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
502 	}
503 
504 	/// ditto
505 	static typeof(this) findOne(T)(T query)
506 	{
507 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
508 	}
509 
510 	/// Tries to find a document by the _id field and returns a Nullable which `isNull` if it could not be found. Otherwise it will be the document wrapped in the nullable.
511 	static Nullable!(typeof(this)) tryFindById(BsonObjectID id)
512 	{
513 		Bson found = collection.findOne(Bson(["_id": Bson(id)]));
514 		if (found.isNull)
515 			return Nullable!(typeof(this)).init;
516 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
517 	}
518 
519 	/// ditto
520 	static Nullable!(typeof(this)) tryFindById(string id)
521 	{
522 		return tryFindById(BsonObjectID.fromString(id));
523 	}
524 
525 	/// Tries to find a document in this collection. It will return a Nullable which `isNull` if the document could not be found. Otherwise it will be the document wrapped in the nullable.
526 	static Nullable!(typeof(this)) tryFindOne(Query!(typeof(this)) query)
527 	{
528 		return tryFindOne(query._query);
529 	}
530 
531 	/// ditto
532 	static Nullable!(typeof(this)) tryFindOne(T)(T query)
533 	{
534 		Bson found = collection.findOne(query);
535 		if (found.isNull)
536 			return Nullable!(typeof(this)).init;
537 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
538 	}
539 
540 	/// Tries to find a document by the _id field and returns a default value if it could not be found.
541 	static typeof(this) tryFindById(BsonObjectID id, typeof(this) defaultValue)
542 	{
543 		Bson found = collection.findOne(Bson(["_id": Bson(id)]));
544 		if (found.isNull)
545 			return defaultValue;
546 		return fromSchemaBson!(typeof(this))(found);
547 	}
548 
549 	/// ditto
550 	static typeof(this) tryFindById(string id, typeof(this) defaultValue)
551 	{
552 		return tryFindById(BsonObjectID.fromString(id), defaultValue);
553 	}
554 
555 	/// Tries to find a document in this collection. It will return a default value if the document could not be found.
556 	static typeof(this) tryFindOne(Query!(typeof(this)) query, typeof(this) defaultValue)
557 	{
558 		return tryFindOne(query._query, defaultValue);
559 	}
560 
561 	/// ditto
562 	static typeof(this) tryFindOne(T)(T query, typeof(this) defaultValue)
563 	{
564 		Bson found = collection.findOne(query);
565 		if (found.isNull)
566 			return defaultValue;
567 		return fromSchemaBson!(typeof(this))(found);
568 	}
569 
570 	/// Finds one or more elements using a query.
571 	static typeof(this)[] find(Query!(typeof(this)) query,
572 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
573 	{
574 		return find(query._query, flags, num_skip, num_docs_per_chunk);
575 	}
576 
577 	/// ditto
578 	static typeof(this)[] find(T)(T query, QueryFlags flags = QueryFlags.None,
579 			int num_skip = 0, int num_docs_per_chunk = 0)
580 	{
581 		typeof(this)[] values;
582 		foreach (entry; collection.find(query, null, flags, num_skip, num_docs_per_chunk))
583 		{
584 			values ~= fromSchemaBson!(typeof(this))(entry);
585 		}
586 		return values;
587 	}
588 
589 	/// Finds one or more elements using a query as range.
590 	static DocumentRange!(typeof(this)) findRange(Query!(typeof(this)) query,
591 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
592 	{
593 		return findRange(query._query, flags, num_skip, num_docs_per_chunk);
594 	}
595 
596 	/// ditto
597 	static DocumentRange!(typeof(this)) findRange(T)(T query,
598 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
599 	{
600 		return DocumentRange!(typeof(this))(collection.find(serializeToBson(query),
601 				null, flags, num_skip, num_docs_per_chunk));
602 	}
603 
604 	/// Queries all elements from the collection as range.
605 	static DocumentRange!(typeof(this)) findAll()
606 	{
607 		return DocumentRange!(typeof(this))(collection.find());
608 	}
609 
610 	/// Inserts many documents at once. The resulting IDs of the symbols will be generated by the server and not known to the caller.
611 	static void insertMany(T)(T documents, InsertFlags options = InsertFlags.none)
612 			if (isInputRange!T && is(ElementType!T : typeof(this)))
613 	{
614 		import std.array : array;
615 		import std.algorithm : map;
616 
617 		if (documents.empty)
618 			return;
619 		collection.insert(documents.map!((a) {
620 				a.bsonID = BsonObjectID.init;
621 				return a.toSchemaBson;
622 			}).array, options); // .array needed because of vibe-d issue #2185
623 	}
624 
625 	/// Updates a document.
626 	static void update(U)(Query!(typeof(this)) query, U update, UpdateFlags options = UpdateFlags
627 			.none)
628 	{
629 		update(query._query, update, options);
630 	}
631 
632 	/// ditto
633 	static void update(T, U)(T query, U update, UpdateFlags options = UpdateFlags.none)
634 	{
635 		collection.update(query, update, options);
636 	}
637 
638 	/// Updates a document or inserts it when not existent. Shorthand for `update(..., UpdateFlags.upsert)`
639 	static void upsert(U)(Query!(typeof(this)) query, U upsert,
640 			UpdateFlags options = UpdateFlags.upsert)
641 	{
642 		upsert(query._query, upsert, options);
643 	}
644 
645 	/// ditto
646 	static void upsert(T, U)(T query, U update, UpdateFlags options = UpdateFlags.upsert)
647 	{
648 		collection.update(query, update, options);
649 	}
650 
651 	/// Deletes one or any amount of documents matching the selector based on the flags.
652 	static void remove(Query!(typeof(this)) query, DeleteFlags flags = DeleteFlags.none)
653 	{
654 		remove(query._query, flags);
655 	}
656 
657 	/// ditto
658 	static void remove(T)(T selector, DeleteFlags flags = DeleteFlags.none)
659 	{
660 		collection.remove(selector, flags);
661 	}
662 
663 	/// Removes all documents from this collection.
664 	static void removeAll()
665 	{
666 		collection.remove();
667 	}
668 
669 	/// Drops the entire collection and all indices in the database.
670 	static void dropTable()
671 	{
672 		collection.drop();
673 	}
674 
675 	/// Returns the count of documents in this collection matching this query.
676 	static auto count(Query!(typeof(this)) query)
677 	{
678 		return count(query._query);
679 	}
680 
681 	/// ditto
682 	static auto count(T)(T query)
683 	{
684 		return collection.count(query);
685 	}
686 
687 	/// Returns the count of documents in this collection.
688 	static auto countAll()
689 	{
690 		import vibe.data.bson : Bson;
691 
692 		return collection.count(Bson.emptyObject);
693 	}
694 
695 	/// Start of an aggregation call. Returns a pipeline with typesafe functions for modifying the pipeline and running it at the end.
696 	/// Examples:
697 	/// --------------------
698 	/// auto groupResults = Book.aggregate.groupAll([
699 	/// 	"totalPrice": Bson([
700 	/// 		"$sum": Bson([
701 	/// 			"$multiply": Bson([Bson("$price"), Bson("$quantity")])
702 	/// 		])
703 	/// 	]),
704 	/// 	"averageQuantity": Bson([
705 	/// 		"$avg": Bson("$quantity")
706 	/// 	]),
707 	/// 	"count": Bson(["$sum": Bson(1)])
708 	/// ]).run;
709 	/// --------------------
710 	static SchemaPipeline aggregate()
711 	{
712 		return SchemaPipeline(collection);
713 	}
714 }
715 
716 /// Binds a MongoCollection to a Schema. Can only be done once!
717 void register(T)(MongoCollection collection) @safe
718 {
719 	T obj = T.init;
720 
721 	static if (hasMember!(T, "_schema_collection_"))
722 	{
723 		(() @trusted {
724 			assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!");
725 			T._schema_collection_ = collection;
726 		})();
727 	}
728 
729 	static foreach (memberName; getSerializableMembers!obj)
730 	{
731 		{
732 			alias member = __traits(getMember, obj, memberName);
733 
734 			string name = memberName;
735 			static if (hasUDA!(member, schemaName))
736 			{
737 				static assert(getUDAs!(member, schemaName)
738 						.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
739 				name = getUDAs!(member, schemaName)[0].name;
740 			}
741 
742 			ulong expires = 0LU;
743 			bool force;
744 
745 			static if (hasUDA!(member, mongoForceIndex))
746 			{
747 				force = true;
748 			}
749 			static if (hasUDA!(member, mongoExpire))
750 			{
751 				static assert(getUDAs!(member, mongoExpire)
752 						.length == 1, "Member '" ~ memberName ~ "' can only have one expiry value!");
753 				expires = getUDAs!(member, mongoExpire)[0].seconds;
754 			}
755 
756 
757 			static if (is(IndexOptions))
758 			{
759 				IndexOptions indexOptions;
760 				static if (hasUDA!(member, mongoBackground))
761 				{
762 					indexOptions.background = true;
763 				}
764 				static if (hasUDA!(member, mongoDropDuplicates))
765 				{
766 					indexOptions.dropDups = true;
767 				}
768 				static if (hasUDA!(member, mongoSparse))
769 				{
770 					indexOptions.sparse = true;
771 				}
772 				static if (hasUDA!(member, mongoUnique))
773 				{
774 					indexOptions.unique = true;
775 				}
776 				static if (hasUDA!(member, mongoExpire))
777 				{
778 					indexOptions.expireAfterSeconds = cast(int)expires;
779 				}
780 			}
781 			else
782 			{
783 				IndexFlags flags = IndexFlags.None;
784 				static if (hasUDA!(member, mongoBackground))
785 				{
786 					flags |= IndexFlags.Background;
787 				}
788 				static if (hasUDA!(member, mongoDropDuplicates))
789 				{
790 					flags |= IndexFlags.DropDuplicates;
791 				}
792 				static if (hasUDA!(member, mongoSparse))
793 				{
794 					flags |= IndexFlags.Sparse;
795 				}
796 				static if (hasUDA!(member, mongoUnique))
797 				{
798 					flags |= IndexFlags.Unique;
799 				}
800 				static if (hasUDA!(member, mongoExpire))
801 				{
802 					flags |= IndexFlags.ExpireAfterSeconds;
803 				}
804 
805 				if (flags != IndexFlags.None || force)
806 					collection.ensureIndex([tuple(name, 1)], flags, dur!"seconds"(expires));
807 			}
808 		}
809 	}
810 }
811 
812 unittest
813 {
814 	struct C
815 	{
816 		int a = 4;
817 	}
818 
819 	struct B
820 	{
821 		C cref;
822 	}
823 
824 	struct A
825 	{
826 		B bref;
827 	}
828 
829 	A a;
830 	a.bref.cref.a = 5;
831 	auto bson = a.toSchemaBson();
832 	assert(bson["bref"]["cref"]["a"].get!int == 5);
833 	A b = bson.fromSchemaBson!A();
834 	assert(b.bref.cref.a == 5);
835 }
836 
837 unittest
838 {
839 	static if (__VERSION__ >= 2076)
840 		import std.digest;
841 	else
842 		import std.digest.digest;
843 	import std.digest.sha;
844 	import std.datetime.systime;
845 
846 	enum Activity
847 	{
848 		High,
849 		Medium,
850 		Low
851 	}
852 
853 	enum Permission
854 	{
855 		A = 1,
856 		B = 2,
857 		C = 4
858 	}
859 
860 	struct UserSchema
861 	{
862 		string username = "Unnamed";
863 		@binaryType()
864 		string salt = "foobar";
865 		@encode("encodePassword")
866 		@binaryType()
867 		string password;
868 		@schemaName("date-created")
869 		SchemaDate dateCreated = SchemaDate.now;
870 		SysTime otherDate = BsonDate(1000000000).toSysTime();
871 		Activity activity = Activity.Medium;
872 		BitFlags!Permission permissions;
873 		Tuple!(string, string) name;
874 		ubyte x0;
875 		byte x1;
876 		ushort x2;
877 		short x3;
878 		uint x4;
879 		int x5;
880 		ulong x6;
881 		long x7;
882 		float x8;
883 		double x9;
884 		int[10] token;
885 
886 		Bson encodePassword(UserSchema user)
887 		{
888 			// TODO: Replace with something more secure
889 			return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt)));
890 		}
891 	}
892 
893 	auto user = UserSchema();
894 	user.password = "12345";
895 	user.username = "Bob";
896 	user.permissions = Permission.A | Permission.C;
897 	user.name = tuple("Bob", "Bobby");
898 	user.x0 = 7;
899 	user.x1 = 7;
900 	user.x2 = 7;
901 	user.x3 = 7;
902 	user.x4 = 7;
903 	user.x5 = 7;
904 	user.x6 = 7;
905 	user.x7 = 7;
906 	user.x8 = 7;
907 	user.x9 = 7;
908 	user.token[3] = 8;
909 	auto bson = user.toSchemaBson();
910 	assert(bson["username"].get!string == "Bob");
911 	assert(bson["date-created"].get!(BsonDate).value > 0);
912 	assert(bson["otherDate"].get!(BsonDate).value == 1000000000);
913 	assert(bson["activity"].get!(int) == cast(int) Activity.Medium);
914 	assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar");
915 	assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt));
916 	assert(bson["permissions"].get!(int) == 5);
917 	assert(bson["name"].get!(Bson[]).length == 2);
918 	assert(bson["x0"].get!int == 7);
919 	assert(bson["x1"].get!int == 7);
920 	assert(bson["x2"].get!int == 7);
921 	assert(bson["x3"].get!int == 7);
922 	assert(bson["x4"].get!int == 7);
923 	assert(bson["x5"].get!int == 7);
924 	assert(bson["x6"].get!long == 7);
925 	assert(bson["x7"].get!long == 7);
926 	assert(bson["x8"].get!double == 7);
927 	assert(bson["x9"].get!double == 7);
928 	assert(bson["token"].get!(Bson[]).length == 10);
929 	assert(bson["token"].get!(Bson[])[3].get!int == 8);
930 
931 	auto user2 = bson.fromSchemaBson!UserSchema();
932 	assert(user2.username == user.username);
933 	assert(user2.password != user.password);
934 	assert(user2.salt == user.salt);
935 	// dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time
936 	assert(user2.dateCreated != user.dateCreated);
937 	assert(user2.otherDate.stdTime == user.otherDate.stdTime);
938 	assert(user2.activity == user.activity);
939 	assert(user2.permissions == user.permissions);
940 	assert(user2.name == user.name);
941 	assert(user2.x0 == user.x0);
942 	assert(user2.x1 == user.x1);
943 	assert(user2.x2 == user.x2);
944 	assert(user2.x3 == user.x3);
945 	assert(user2.x4 == user.x4);
946 	assert(user2.x5 == user.x5);
947 	assert(user2.x6 == user.x6);
948 	assert(user2.x7 == user.x7);
949 	assert(user2.x8 == user.x8);
950 	assert(user2.x9 == user.x9);
951 	assert(user2.token == user.token);
952 }
953 
954 unittest
955 {
956 	struct Private
957 	{
958 	private:
959 		int x;
960 	}
961 
962 	struct Something
963 	{
964 		Private p;
965 	}
966 
967 	struct Public
968 	{
969 		int y;
970 	}
971 
972 	struct Something2
973 	{
974 		Public p;
975 	}
976 
977 	struct Something3
978 	{
979 		int y;
980 		@schemaIgnore Private p;
981 	}
982 
983 	struct SerializablePrivate
984 	{
985 		static Bson toBson(SerializablePrivate p)
986 		{
987 			return Bson(1);
988 		}
989 
990 		static SerializablePrivate fromBson(Bson bson)
991 		{
992 			return SerializablePrivate.init;
993 		}
994 	}
995 
996 	struct Something4
997 	{
998 		SerializablePrivate p;
999 	}
1000 
1001 	Something s;
1002 	Something2 s2;
1003 	Something3 s3;
1004 	Something4 s4;
1005 	static assert(__traits(compiles, { s2.toSchemaBson(); }));
1006 	static assert(!__traits(compiles, { s.toSchemaBson(); }),
1007 			"Private members are not serializable, so Private is empty and may not be used as type.");
1008 	static assert(__traits(compiles, { s3.toSchemaBson(); }),
1009 			"When adding schemaIgnore, empty structs must be ignored");
1010 	static assert(__traits(compiles, { s4.toSchemaBson(); }),
1011 			"Empty structs with custom (de)serialization must be ignored");
1012 }
1013 
1014 unittest
1015 {
1016 	import vibe.db.mongo.mongo;
1017 	import std.digest.sha;
1018 	import std.exception;
1019 	import std.array;
1020 
1021 	auto client = connectMongoDB("127.0.0.1");
1022 	auto database = client.getDatabase("test");
1023 	MongoCollection users = database["users"];
1024 	users.remove(); // Clears collection
1025 
1026 	struct User
1027 	{
1028 		mixin MongoSchema;
1029 
1030 		@mongoExpire(30) @mongoUnique string username;
1031 		@binaryType()
1032 		ubyte[] hash;
1033 		@schemaName("profile-picture")
1034 		string profilePicture;
1035 		auto registered = SchemaDate.now;
1036 	}
1037 
1038 	users.register!User;
1039 
1040 	assert(User.findAll().array.length == 0);
1041 
1042 	User user;
1043 	user.username = "Example";
1044 	user.hash = sha512Of("password123").dup;
1045 	user.profilePicture = "example-avatar.png";
1046 
1047 	assertNotThrown(user.save());
1048 
1049 	User user2;
1050 	user2.username = "Bob";
1051 	user2.hash = sha512Of("foobar").dup;
1052 	user2.profilePicture = "bob-avatar.png";
1053 
1054 	assertNotThrown(user2.save());
1055 
1056 	User faker;
1057 	faker.username = "Example";
1058 	faker.hash = sha512Of("PASSWORD").dup;
1059 	faker.profilePicture = "example-avatar.png";
1060 
1061 	assertThrown(faker.save());
1062 	// Unique username
1063 
1064 	faker.username = "Example_";
1065 	assertNotThrown(faker.save());
1066 
1067 	user.username = "NewExample";
1068 	user.save();
1069 
1070 	auto actualFakeID = faker.bsonID;
1071 	faker = User.findOne(["username": "NewExample"]);
1072 
1073 	assert(actualFakeID != faker.bsonID);
1074 
1075 	foreach (usr; User.findAll)
1076 	{
1077 		usr.profilePicture = "default.png"; // Reset all profile pictures
1078 		usr.save();
1079 	}
1080 	user = User.findOne(["username": "NewExample"]);
1081 	user2 = User.findOne(["username": "Bob"]);
1082 	faker = User.findOne(["username": "Example_"]);
1083 	assert(user.profilePicture == user2.profilePicture
1084 			&& user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png");
1085 
1086 	User user3;
1087 	user3.username = "User123";
1088 	user3.hash = sha512Of("486951").dup;
1089 	user3.profilePicture = "new.png";
1090 	User.upsert(["username": "User123"], user3.toSchemaBson);
1091 	user3 = User.findOne(["username": "User123"]);
1092 	assert(user3.hash == sha512Of("486951"));
1093 	assert(user3.profilePicture == "new.png");
1094 }
1095 
1096 unittest
1097 {
1098 	import vibe.db.mongo.mongo;
1099 	import mongoschema.aliases : name, ignore, unique, binary;
1100 	import std.digest.sha;
1101 	import std.digest.md;
1102 
1103 	auto client = connectMongoDB("127.0.0.1");
1104 
1105 	struct Permission
1106 	{
1107 		string name;
1108 		int priority;
1109 	}
1110 
1111 	struct User
1112 	{
1113 		mixin MongoSchema;
1114 
1115 		@unique string username;
1116 
1117 		@binary()
1118 		ubyte[] hash;
1119 		@binary()
1120 		ubyte[] salt;
1121 
1122 		@name("profile-picture")
1123 		string profilePicture = "default.png";
1124 
1125 		Permission[] permissions;
1126 
1127 	@ignore:
1128 		int sessionID;
1129 	}
1130 
1131 	auto coll = client.getCollection("test.users2");
1132 	coll.remove();
1133 	coll.register!User;
1134 
1135 	User register(string name, string password)
1136 	{
1137 		User user;
1138 		user.username = name;
1139 		user.salt = md5Of(name).dup;
1140 		user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup;
1141 		user.permissions ~= Permission("forum.access", 1);
1142 		user.save();
1143 		return user;
1144 	}
1145 
1146 	User find(string name)
1147 	{
1148 		return User.findOne(["username": name]);
1149 	}
1150 
1151 	User a = register("foo", "bar");
1152 	User b = find("foo");
1153 	assert(a == b);
1154 }