1 /**
2  * This module converts a package containing D Model definitions to
3  * $(REF SerializedModels, dorm,declarative)
4  */
5 module dorm.declarative.conversion;
6 
7 import dorm.annotations;
8 import dorm.declarative;
9 import dorm.exception;
10 import dorm.model;
11 import dorm.types;
12 
13 import std.algorithm : remove;
14 import std.conv;
15 import std.datetime;
16 import std.meta;
17 import std.traits;
18 import std.typecons;
19 
20 version (unittest) import dorm.model;
21 
22 /** 
23  * Entry point to the Model (class) to SerializedModels (declarative) conversion
24  * code. Manually calling this should not be neccessary as the
25  * $(REF RegisterModels, dorm,declarative,entrypoint) mixin will call this instead.
26  */
27 SerializedModels processModelsToDeclarations(alias mod)()
28 {
29 	SerializedModels ret;
30 
31 	static foreach (member; __traits(allMembers, mod))
32 	{
33 		static if (__traits(compiles, is(__traits(getMember, mod, member) : Model))
34 			&& is(__traits(getMember, mod, member) : Model)
35 			&& !__traits(isAbstractClass, __traits(getMember, mod, member)))
36 		{
37 			processModel!(
38 				__traits(getMember, mod, member),
39 				SourceLocation(__traits(getLocation, __traits(getMember, mod, member)))
40 			)(ret);
41 		}
42 	}
43 
44 	return ret;
45 }
46 
47 private void processModel(TModel : Model, SourceLocation loc)(
48 	ref SerializedModels models)
49 {
50 	ModelFormat serialized = DormLayout!TModel;
51 	serialized.definedAt = loc;
52 	models.models ~= serialized;
53 }
54 
55 private enum DormLayoutImpl(TModel : Model) = function() {
56 	ModelFormat serialized;
57 	serialized.tableName = DormModelTableName!TModel;
58 
59 	alias attributes = __traits(getAttributes, TModel);
60 
61 	static foreach (attribute; attributes)
62 	{
63 		static if (isDormModelAttribute!attribute)
64 		{
65 			static assert(false, "Unsupported attribute " ~ attribute.stringof ~ " on " ~ TModel.stringof ~ "." ~ member);
66 		}
67 	}
68 
69 	string errors;
70 
71 	static foreach (field; LogicalFields!(TModel, 2))
72 	{
73 		try
74 		{
75 			processField!(TModel, field, field)(serialized);
76 		}
77 		catch (Exception e)
78 		{
79 			errors ~= "\n\t" ~ e.msg;
80 		}
81 	}
82 
83 	static if (!__traits(isAbstractClass, TModel))
84 		errors ~= serialized.lint("\n\t");
85 
86 	struct Ret
87 	{
88 		string errors;
89 		ModelFormat ret;
90 	}
91 
92 	return Ret(errors, serialized);
93 }();
94 
95 template DormLayout(TModel : Model)
96 {
97 	private enum Impl = DormLayoutImpl!TModel;
98 	static if (Impl.errors.length)
99 		static assert(false, "Model Definition of `"
100 			~ TModel.stringof ~ "` contains errors:" ~ Impl.errors);
101 	else
102 		enum ModelFormat DormLayout = Impl.ret;
103 }
104 
105 /// Equivalent to `DormLayout!TModel.tableName`
106 enum DormModelTableName(TModel : Model) = TModel.stringof.toSnakeCase;
107 
108 enum DormFields(TModel : Model) = DormLayout!TModel.fields;
109 enum DormForeignKeys(TModel : Model) = (() {
110 	auto all = DormFields!TModel;
111 	foreach_reverse (i, field; all)
112 		if (!field.isForeignKey)
113 			all = all.remove(i);
114 	return all;
115 })();
116 
117 enum DormFieldIndex(TModel : Model, string sourceName) = findFieldIdx(DormFields!TModel, sourceName);
118 enum hasDormField(TModel : Model, string sourceName) = DormFieldIndex!(TModel, sourceName) != -1;
119 enum DormField(TModel : Model, string sourceName) = DormFields!TModel[DormFieldIndex!(TModel, sourceName)];
120 
121 enum DormPrimaryKeyIndex(TModel : Model) = findPrimaryFieldIdx(DormFields!TModel);
122 enum DormPrimaryKey(TModel : Model) = DormFields!TModel[DormPrimaryKeyIndex!TModel];
123 
124 /// Equivalent to DormPrimaryKey!TModel.sourceColumn, but does not analyze the
125 /// model, thus avoiding cyclic lookups. Note: does not do any sanity checks,
126 /// like checking if there are two primary keys.
127 template DormPrimaryKeyName(TModel : Model)
128 {
129 	static foreach (field; LogicalFields!TModel)
130 		static if (hasUDA!(__traits(getMember, TModel, field), primaryKey))
131 			enum DormPrimaryKeyName = field;
132 }
133 
134 /// Used internally, basically converts the field name to snake case or returns
135 /// the @columnName value if the field has been annotated with it.
136 static template DormColumnNameImpl(alias field)
137 {
138 	enum DormColumnNameImpl = {
139 		string ret = __traits(identifier, field).toSnakeCase;
140 
141 		alias attributes = __traits(getAttributes, field);
142 		static foreach (attribute; attributes)
143 			static if (is(typeof(attribute) == columnName))
144 				ret = attribute.name;
145 		return ret;
146 	}();
147 }
148 
149 /// Returns all fields with `@constructValue` annotations. Used internally in
150 /// Model to determine which fields to look at in the constructor.
151 enum string[] DormModelConstructors(TModel : Model) = {
152 	string[] ret;
153 	static foreach (field; LogicalFields!(TModel, 1))
154 	{{
155 		alias fieldAlias = __traits(getMember, TModel, field);
156 
157 		alias attributes = __traits(getAttributes, fieldAlias);
158 		static foreach (attribute; attributes)
159 		{{
160 			static if (is(attribute == constructValue!fn, alias fn))
161 				ret ~= field;
162 		}}
163 	}}
164 	return ret;
165 }();
166 
167 enum DormListFieldsForError(TModel : Model) = formatFieldList(DormFields!TModel);
168 
169 private auto findFieldIdx(ModelFormat.Field[] fields, string name)
170 {
171 	foreach (i, ref field; fields)
172 		if (field.sourceColumn == name)
173 			return i;
174 	return -1;
175 }
176 
177 private auto findPrimaryFieldIdx(ModelFormat.Field[] fields)
178 {
179 	foreach (i, ref field; fields)
180 		if (field.isPrimaryKey)
181 			return i;
182 	return -1;
183 }
184 
185 private auto formatFieldList(ModelFormat.Field[] fields)
186 {
187 	string ret;
188 	foreach (field; fields)
189 		ret ~= "\n- " ~ field.toPrettySourceString;
190 	return ret;
191 }
192 
193 /// Returns the name of all public member fields, through all base classes (i.e.
194 /// returns identifiers of public .tupleof members)
195 template LogicalFields(TModel, int cacheHack = 0)
196 {
197 	// cacheHack exists because DormLayout needs a different cache due to cyclic things
198 	static if (is(TModel : Model))
199 		alias Classes = AliasSeq!(TModel, BaseClassesTuple!TModel);
200 	else
201 		alias Classes = AliasSeq!(TModel);
202 	alias LogicalFields = AliasSeq!();
203 
204 	static foreach_reverse (Class; Classes)
205 	{
206 		static foreach (field; __traits(derivedMembers, Class))
207 		{
208 			static if (
209 				// isTemplate does not compile for circular references, but those are assumed to be fields
210 				!__traits(compiles, __traits(isTemplate, __traits(getMember, Class, field)))
211 				|| (is(typeof(__traits(getMember, Class, field)))
212 				&& !is(typeof(&__traits(getMember, Class, field)))
213 				&& !__traits(isTemplate, __traits(getMember, Class, field))
214 				&& __traits(getProtection, __traits(getMember, Class, field)) == "public"))
215 				LogicalFields = AliasSeq!(LogicalFields, field);
216 		}
217 	}
218 }
219 
220 private struct ProcessFieldResult
221 {
222 	bool include, checkForOtherPrimaryKey;
223 	ModelFormat.Field field;
224 }
225 
226 private void processField(TModel, string fieldName, string directFieldName)(ref ModelFormat serialized)
227 {
228 	alias fieldAlias = __traits(getMember, TModel, directFieldName);
229 
230 	ProcessFieldResult result = processFieldImpl!(TModel, fieldName, directFieldName)();
231 
232 	static if (hasUDA!(fieldAlias, embedded))
233 	{
234 		serialized.embeddedStructs ~= fieldName;
235 		alias TSubModel = typeof(fieldAlias);
236 		static foreach (subfield; LogicalFields!(TSubModel, 2))
237 		{
238 			processField!(TSubModel, fieldName ~ "." ~ subfield, subfield)(serialized);
239 		}
240 	}
241 
242 	with (result) if (include)
243 	{
244 		if (checkForOtherPrimaryKey)
245 		{
246 			foreach (other; serialized.fields)
247 			{
248 				if (other.isPrimaryKey)
249 					throw new DormModelException("Duplicate primary key found in Model " ~ TModel.stringof
250 						~ ":\n- first defined here:\n"
251 						~ other.sourceColumn ~ " in " ~ other.definedAt.toString
252 						~ "\n- then attempted to redefine here:\n"
253 						~ typeof(fieldAlias).stringof ~ " " ~ fieldName ~ " in " ~ field.definedAt.toString
254 						~ "\nMaybe you wanted to define a `TODO: compositeKey`?");
255 			}
256 		}
257 
258 		// https://github.com/myOmikron/drorm/issues/8
259 		if (field.hasFlag(AnnotationFlag.autoIncrement) && !field.hasFlag(AnnotationFlag.primaryKey))
260 				throw new DormModelException(field.sourceReferenceName(TModel.stringof)
261 					~ " has @autoIncrement annotation, but is missing required @primaryKey annotation.");
262 
263 		if (field.type == InvalidDBType)
264 			throw new DormModelException(SourceLocation(__traits(getLocation, fieldAlias)).toErrorString
265 				~ "Cannot resolve DORM Model DBType from " ~ typeof(fieldAlias).stringof
266 				~ " `" ~ directFieldName ~ "` in " ~ TModel.stringof);
267 
268 		foreach (ai, lhs; field.annotations)
269 		{
270 			foreach (bi, rhs; field.annotations)
271 			{
272 				if (ai == bi) continue;
273 				if (!lhs.isCompatibleWith(rhs))
274 					throw new DormModelException("Incompatible annotation: "
275 						~ lhs.to!string ~ " conflicts with " ~ rhs.to!string
276 						~ " on " ~ field.sourceReferenceName(TModel.stringof));
277 			}
278 		}
279 
280 		serialized.fields ~= field;
281 	}
282 }
283 
284 private ProcessFieldResult processFieldImpl(TModel, string fieldName, string directFieldName)()
285 {
286 	import uda = dorm.annotations;
287 
288 	alias fieldAlias = __traits(getMember, TModel, directFieldName);
289 
290 	alias attributes = __traits(getAttributes, fieldAlias);
291 
292 	bool include = true;
293 	ModelFormat.Field field;
294 
295 	field.definedAt = SourceLocation(__traits(getLocation, fieldAlias));
296 	field.columnName = directFieldName.toSnakeCase;
297 	field.sourceType = typeof(fieldAlias).stringof;
298 	field.sourceColumn = fieldName;
299 	field.type = guessDBType!(typeof(fieldAlias));
300 
301 	static if (is(typeof(fieldAlias) == Nullable!T, T))
302 	{
303 		alias UnnullType = T;
304 		enum isNullable = true;
305 	}
306 	else
307 	{
308 		alias UnnullType = typeof(fieldAlias);
309 		enum isNullable = false;
310 	}
311 
312 	bool checkForOtherPrimaryKey = false;
313 
314 	bool explicitNotNull = false;
315 	bool nullable = false;
316 	bool mustBeNullable = false;
317 	bool hasNonNullDefaultValue = false;
318 	static if (isNullable || is(typeof(fieldAlias) : Model))
319 		nullable = true;
320 
321 	static if (is(UnnullType == enum))
322 		field.annotations ~= DBAnnotation(Choices([
323 				__traits(allMembers, UnnullType)
324 			]));
325 	else static if (is(UnnullType : ModelRefImpl!(_id, _TModel, _TSelect),
326 		alias _id, _TModel, _TSelect))
327 	{
328 		ForeignKeyImpl foreignKeyImpl;
329 		auto foreignKeyInfo = processFieldImpl!(_TModel, UnnullType.primaryKeySourceName, UnnullType.primaryKeySourceName);
330 		assert(foreignKeyInfo.include, "Refering to a foreign key that's not included in the result!");
331 		field.type = foreignKeyInfo.field.type;
332 		field.annotations ~= foreignKeyInfo.field.foreignKeyAnnotations;
333 		foreignKeyImpl.column = foreignKeyInfo.field.columnName;
334 		foreignKeyImpl.table = DormModelTableName!_TModel;
335 	}
336 
337 	enum bool isForeignKey = is(typeof(foreignKeyImpl));
338 
339 	void setDefaultValue(T)(T value)
340 	{
341 		static if (__traits(compiles, value !is null))
342 		{
343 			if (value !is null)
344 				hasNonNullDefaultValue = true;
345 		}
346 		else
347 			hasNonNullDefaultValue = true;
348 
349 		field.annotations ~= DBAnnotation(defaultValue(value));
350 	}
351 
352 	static foreach (attribute; attributes)
353 	{{
354 		static if (__traits(isSame, attribute, uda.autoCreateTime))
355 		{
356 			field.type = ModelFormat.Field.DBType.datetime;
357 			field.annotations ~= DBAnnotation(AnnotationFlag.autoCreateTime);
358 			hasNonNullDefaultValue = true;
359 		}
360 		else static if (__traits(isSame, attribute, uda.autoUpdateTime))
361 		{
362 			field.type = ModelFormat.Field.DBType.datetime;
363 			field.annotations ~= DBAnnotation(AnnotationFlag.autoUpdateTime);
364 			mustBeNullable = true;
365 		}
366 		else static if (__traits(isSame, attribute, uda.timestamp))
367 		{
368 			field.type = ModelFormat.Field.DBType.datetime;
369 			mustBeNullable = true;
370 		}
371 		else static if (__traits(isSame, attribute, uda.primaryKey))
372 		{
373 			nullable = true;
374 			field.annotations ~= DBAnnotation(AnnotationFlag.primaryKey);
375 			checkForOtherPrimaryKey = true;
376 		}
377 		else static if (__traits(isSame, attribute, uda.autoIncrement))
378 		{
379 			field.annotations ~= DBAnnotation(AnnotationFlag.autoIncrement);
380 			hasNonNullDefaultValue = true;
381 		}
382 		else static if (__traits(isSame, attribute, uda.unique))
383 		{
384 			field.annotations ~= DBAnnotation(AnnotationFlag.unique);
385 		}
386 		else static if (__traits(isSame, attribute, uda.embedded))
387 		{
388 			static if (!is(typeof(fieldAlias) == struct))
389 				static assert(false, "@embedded is only supported on structs");
390 			include = false;
391 		}
392 		else static if (__traits(isSame, attribute, uda.ignored))
393 		{
394 			include = false;
395 		}
396 		else static if (__traits(isSame, attribute, uda.notNull))
397 		{
398 			explicitNotNull = true;
399 		}
400 		else static if (is(attribute == constructValue!fn, alias fn))
401 		{
402 			field.internalAnnotations ~= InternalAnnotation(ConstructValueRef(fieldName));
403 		}
404 		else static if (is(attribute == validator!fn, alias fn))
405 		{
406 			field.internalAnnotations ~= InternalAnnotation(ValidatorRef(fieldName));
407 		}
408 		else static if (__traits(isSame, attribute, defaultFromInit))
409 		{
410 			static if (is(TModel == struct))
411 			{
412 				setDefaultValue(__traits(getMember, TModel.init, directFieldName));
413 			}
414 			else static if (__traits(compiles, __traits(getMember, new TModel(), directFieldName)))
415 			{
416 				setDefaultValue(__traits(getMember, new TModel(), directFieldName));
417 			}
418 			else
419 			{
420 				if (include)
421 				{
422 					throw new DormModelException(field.sourceReferenceName(TModel.stringof)
423 						~ " is annotated with `@defaultFromInit`, but failed construction. "
424 						~ "Consider moving this field into a struct and using `@embedded` to use it "
425 						~ "or remove incompatible attributes. (Implicit Patches work as well)");
426 				}
427 			}
428 		}
429 		else static if (is(typeof(attribute) == maxLength)
430 			|| is(typeof(attribute) == DefaultValue!T, T)
431 			|| is(typeof(attribute) == index))
432 		{
433 			static if (is(typeof(attribute) == DefaultValue!U, U)
434 				&& !is(U == typeof(null)))
435 			{
436 				hasNonNullDefaultValue = true;
437 			}
438 			field.annotations ~= DBAnnotation(attribute);
439 		}
440 		else static if (is(typeof(attribute) == Choices))
441 		{
442 			field.type = ModelFormat.Field.DBType.choices;
443 			field.annotations ~= DBAnnotation(attribute);
444 		}
445 		else static if (is(typeof(attribute) == columnName))
446 		{
447 			field.columnName = attribute.name;
448 		}
449 		else static if (is(typeof(attribute) == uda.onUpdate)
450 			&& isForeignKey)
451 		{
452 			foreignKeyImpl.onUpdate = attribute.type;
453 		}
454 		else static if (is(typeof(attribute) == uda.onDelete)
455 			&& isForeignKey)
456 		{
457 			foreignKeyImpl.onDelete = attribute.type;
458 		}
459 		else static if (isDormFieldAttribute!attribute)
460 		{
461 			static assert(false, "Unsupported attribute " ~ attribute.stringof ~ " on " ~ TModel.stringof ~ "." ~ fieldName);
462 		}
463 	}}
464 
465 	static if (isForeignKey)
466 		field.annotations ~= DBAnnotation(foreignKeyImpl);
467 
468 	if (include)
469 	{
470 		if (!nullable || explicitNotNull)
471 		{
472 			if (mustBeNullable && !hasNonNullDefaultValue)
473 			{
474 				throw new DormModelException(field.sourceReferenceName(TModel.stringof)
475 					~ " may be null. Change it to Nullable!(" ~ typeof(fieldAlias).stringof
476 					~ ") or annotate with defaultValue, autoIncrement or autoCreateTime");
477 			}
478 			field.annotations = DBAnnotation(AnnotationFlag.notNull) ~ field.annotations;
479 		}
480 	}
481 
482 	return ProcessFieldResult(
483 		include,
484 		checkForOtherPrimaryKey,
485 		field
486 	);
487 }
488 
489 private string toSnakeCase(string s)
490 {
491 	import std.array;
492 	import std.ascii;
493 
494 	auto ret = appender!(char[]);
495 	int upperCount;
496 	char lastChar;
497 	foreach (char c; s)
498 	{
499 		scope (exit)
500 			lastChar = c;
501 		if (upperCount)
502 		{
503 			if (isUpper(c))
504 			{
505 				ret ~= toLower(c);
506 				upperCount++;
507 			}
508 			else
509 			{
510 				if (isDigit(c))
511 				{
512 					ret ~= '_';
513 					ret ~= c;
514 				}
515 				else if (isLower(c) && upperCount > 1 && ret.data.length)
516 				{
517 					auto last = ret.data[$ - 1];
518 					ret.shrinkTo(ret.data.length - 1);
519 					ret ~= '_';
520 					ret ~= last;
521 					ret ~= c;
522 				}
523 				else
524 				{
525 					ret ~= toLower(c);
526 				}
527 				upperCount = 0;
528 			}
529 		}
530 		else if (isUpper(c))
531 		{
532 			if (ret.data.length
533 				&& ret.data[$ - 1] != '_'
534 				&& !lastChar.isUpper)
535 			{
536 				ret ~= '_';
537 				ret ~= toLower(c);
538 				upperCount++;
539 			}
540 			else
541 			{
542 				if (ret.data.length)
543 					upperCount++;
544 				ret ~= toLower(c);
545 			}
546 		}
547 		else if (c == '_')
548 		{
549 			if (ret.data.length)
550 				ret ~= '_';
551 		}
552 		else if (isDigit(c))
553 		{
554 			if (ret.data.length && !ret.data[$ - 1].isDigit && ret.data[$ - 1] != '_')
555 				ret ~= '_';
556 			ret ~= c;
557 		}
558 		else
559 		{
560 			if (ret.data.length && ret.data[$ - 1].isDigit && ret.data[$ - 1] != '_')
561 				ret ~= '_';
562 			ret ~= c;
563 		}
564 	}
565 
566 	auto slice = ret.data;
567 	while (slice.length && slice[$ - 1] == '_')
568 		slice = slice[0 .. $ - 1];
569 	return slice.idup;
570 }
571 
572 unittest
573 {
574 	assert("".toSnakeCase == "");
575 	assert("a".toSnakeCase == "a");
576 	assert("A".toSnakeCase == "a");
577 	assert("AB".toSnakeCase == "ab");
578 	assert("JsonValue".toSnakeCase == "json_value");
579 	assert("HTTPHandler".toSnakeCase == "http_handler");
580 	assert("Test123".toSnakeCase == "test_123");
581 	assert("foo_bar".toSnakeCase == "foo_bar");
582 	assert("foo_Bar".toSnakeCase == "foo_bar");
583 	assert("Foo_Bar".toSnakeCase == "foo_bar");
584 	assert("FOO_bar".toSnakeCase == "foo_bar");
585 	assert("FOO__bar".toSnakeCase == "foo__bar");
586 	assert("do_".toSnakeCase == "do");
587 	assert("fooBar_".toSnakeCase == "foo_bar");
588 	assert("_do".toSnakeCase == "do");
589 	assert("_fooBar".toSnakeCase == "foo_bar");
590 	assert("_FooBar".toSnakeCase == "foo_bar");
591 	assert("HTTP2".toSnakeCase == "http_2");
592 	assert("HTTP2Foo".toSnakeCase == "http_2_foo");
593 	assert("HTTP2foo".toSnakeCase == "http_2_foo");
594 }
595 
596 private enum InvalidDBType = cast(ModelFormat.Field.DBType)int.max;
597 
598 private template guessDBType(T)
599 {
600 	static if (is(T == enum))
601 		enum guessDBType = ModelFormat.Field.DBType.choices;
602 	else static if (is(T == BitFlags!U, U))
603 		enum guessDBType = ModelFormat.Field.DBType.set;
604 	else static if (is(T == Nullable!U, U))
605 	{
606 		static if (__traits(compiles, guessDBType!U))
607 			enum guessDBType = guessDBType!U;
608 		else
609 			static assert(false, "cannot resolve DBType from nullable " ~ U.stringof);
610 	}
611 	else static if (__traits(compiles, guessDBTypeBase!T))
612 		enum guessDBType = guessDBTypeBase!T;
613 	else
614 		enum guessDBType = InvalidDBType;
615 }
616 
617 private enum guessDBTypeBase(T : const(char)[]) = ModelFormat.Field.DBType.varchar;
618 private enum guessDBTypeBase(T : const(ubyte)[]) = ModelFormat.Field.DBType.varbinary;
619 private enum guessDBTypeBase(T : byte) = ModelFormat.Field.DBType.int8;
620 private enum guessDBTypeBase(T : short) = ModelFormat.Field.DBType.int16;
621 private enum guessDBTypeBase(T : int) = ModelFormat.Field.DBType.int32;
622 private enum guessDBTypeBase(T : long) = ModelFormat.Field.DBType.int64;
623 private enum guessDBTypeBase(T : ubyte) = ModelFormat.Field.DBType.int16;
624 private enum guessDBTypeBase(T : ushort) = ModelFormat.Field.DBType.int32;
625 private enum guessDBTypeBase(T : uint) = ModelFormat.Field.DBType.int64;
626 private enum guessDBTypeBase(T : float) = ModelFormat.Field.DBType.floatNumber;
627 private enum guessDBTypeBase(T : double) = ModelFormat.Field.DBType.doubleNumber;
628 private enum guessDBTypeBase(T : bool) = ModelFormat.Field.DBType.boolean;
629 private enum guessDBTypeBase(T : Date) = ModelFormat.Field.DBType.date;
630 private enum guessDBTypeBase(T : DateTime) = ModelFormat.Field.DBType.datetime;
631 private enum guessDBTypeBase(T : SysTime) = ModelFormat.Field.DBType.datetime;
632 private enum guessDBTypeBase(T : TimeOfDay) = ModelFormat.Field.DBType.time;
633 
634 unittest
635 {
636 	import std.sumtype;
637 
638 	struct Mod
639 	{
640 		import std.datetime;
641 		import std.typecons;
642 
643 		enum State : string
644 		{
645 			ok = "ok",
646 			warn = "warn",
647 			critical = "critical",
648 			unknown = "unknown"
649 		}
650 
651 		class User : Model
652 		{
653 			@maxLength(255)
654 			string username;
655 
656 			@maxLength(255)
657 			string password;
658 
659 			@maxLength(255)
660 			Nullable!string email;
661 
662 			ubyte age;
663 
664 			Nullable!DateTime birthday;
665 
666 			@autoCreateTime
667 			SysTime createdAt;
668 
669 			@autoUpdateTime
670 			Nullable!SysTime updatedAt;
671 
672 			@autoCreateTime
673 			ulong createdAt2;
674 
675 			@autoUpdateTime
676 			Nullable!ulong updatedAt2;
677 
678 			State state;
679 
680 			@choices("ok", "warn", "critical", "unknown")
681 			string state2;
682 
683 			@columnName("admin")
684 			bool isAdmin;
685 
686 			@constructValue!(() => Clock.currTime + 4.hours)
687 			SysTime validUntil;
688 
689 			@maxLength(255)
690 			@defaultValue("")
691 			string comment;
692 
693 			@defaultValue(1337)
694 			int counter;
695 
696 			@primaryKey
697 			long ownPrimaryKey;
698 
699 			@timestamp
700 			Nullable!ulong someTimestamp;
701 
702 			@unique
703 			int uuid;
704 
705 			@validator!(x => x >= 18)
706 			int someInt;
707 
708 			@ignored
709 			int imNotIncluded;
710 		}
711 	}
712 
713 	auto mod = processModelsToDeclarations!Mod;
714 	assert(mod.models.length == 1);
715 
716 	auto m = mod.models[0];
717 
718 	// Length is always len(m.fields + 1) as dorm.model.Model adds the id field,
719 	// unless you define your own primary key field.
720 	assert(m.fields.length == 19);
721 
722 	int i = 0;
723 
724 	assert(m.fields[i].columnName == "username");
725 	assert(m.fields[i].type == ModelFormat.Field.DBType.varchar);
726 	assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull), DBAnnotation(maxLength(255))]);
727 
728 	assert(m.fields[++i].columnName == "password");
729 	assert(m.fields[i].type == ModelFormat.Field.DBType.varchar);
730 	assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull), DBAnnotation(maxLength(255))]);
731 
732 	assert(m.fields[++i].columnName == "email");
733 	assert(m.fields[i].type == ModelFormat.Field.DBType.varchar);
734 	assert(m.fields[i].annotations == [DBAnnotation(maxLength(255))]);
735 
736 	assert(m.fields[++i].columnName == "age");
737 	assert(m.fields[i].type == ModelFormat.Field.DBType.int16);
738 	assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull)]);
739 
740 	assert(m.fields[++i].columnName == "birthday");
741 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
742 	assert(m.fields[i].annotations == []);
743 
744 	assert(m.fields[++i].columnName == "created_at");
745 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
746 	assert(m.fields[i].annotations == [
747 			DBAnnotation(AnnotationFlag.notNull),
748 			DBAnnotation(AnnotationFlag.autoCreateTime),
749 		]);
750 
751 	assert(m.fields[++i].columnName == "updated_at");
752 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
753 	assert(m.fields[i].annotations == [
754 			DBAnnotation(AnnotationFlag.autoUpdateTime)
755 		]);
756 
757 	assert(m.fields[++i].columnName == "created_at_2");
758 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
759 	assert(m.fields[i].annotations == [
760 			DBAnnotation(AnnotationFlag.notNull),
761 			DBAnnotation(AnnotationFlag.autoCreateTime),
762 		]);
763 
764 	assert(m.fields[++i].columnName == "updated_at_2");
765 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
766 	assert(m.fields[i].annotations == [
767 			DBAnnotation(AnnotationFlag.autoUpdateTime)
768 		]);
769 
770 	assert(m.fields[++i].columnName == "state");
771 	assert(m.fields[i].type == ModelFormat.Field.DBType.choices);
772 	assert(m.fields[i].annotations == [
773 			DBAnnotation(AnnotationFlag.notNull),
774 			DBAnnotation(Choices(["ok", "warn", "critical", "unknown"])),
775 		]);
776 
777 	assert(m.fields[++i].columnName == "state_2");
778 	assert(m.fields[i].type == ModelFormat.Field.DBType.choices);
779 	assert(m.fields[i].annotations == [
780 			DBAnnotation(AnnotationFlag.notNull),
781 			DBAnnotation(Choices(["ok", "warn", "critical", "unknown"])),
782 		]);
783 
784 	assert(m.fields[++i].columnName == "admin");
785 	assert(m.fields[i].type == ModelFormat.Field.DBType.boolean);
786 	assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull)]);
787 
788 	assert(m.fields[++i].columnName == "valid_until");
789 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
790 	assert(m.fields[i].annotations == [
791 			DBAnnotation(AnnotationFlag.notNull)
792 		]);
793 	assert(m.fields[i].internalAnnotations.length == 1);
794 	assert(m.fields[i].internalAnnotations[0].match!((ConstructValueRef r) => true, _ => false));
795 
796 	assert(m.fields[++i].columnName == "comment");
797 	assert(m.fields[i].type == ModelFormat.Field.DBType.varchar);
798 	assert(m.fields[i].annotations == [
799 			DBAnnotation(AnnotationFlag.notNull),
800 			DBAnnotation(maxLength(255)),
801 			DBAnnotation(defaultValue("")),
802 		]);
803 
804 	assert(m.fields[++i].columnName == "counter");
805 	assert(m.fields[i].type == ModelFormat.Field.DBType.int32);
806 	assert(m.fields[i].annotations == [
807 			DBAnnotation(AnnotationFlag.notNull),
808 			DBAnnotation(defaultValue(1337)),
809 		]);
810 
811 	assert(m.fields[++i].columnName == "own_primary_key");
812 	assert(m.fields[i].type == ModelFormat.Field.DBType.int64);
813 	assert(m.fields[i].annotations == [
814 			DBAnnotation(AnnotationFlag.primaryKey),
815 		]);
816 
817 	assert(m.fields[++i].columnName == "some_timestamp");
818 	assert(m.fields[i].type == ModelFormat.Field.DBType.datetime);
819 	assert(m.fields[i].annotations == []);
820 
821 	assert(m.fields[++i].columnName == "uuid");
822 	assert(m.fields[i].type == ModelFormat.Field.DBType.int32);
823 	assert(m.fields[i].annotations == [
824 			DBAnnotation(AnnotationFlag.notNull),
825 			DBAnnotation(AnnotationFlag.unique),
826 		]);
827 
828 	assert(m.fields[++i].columnName == "some_int");
829 	assert(m.fields[i].type == ModelFormat.Field.DBType.int32);
830 	assert(m.fields[i].annotations == [
831 			DBAnnotation(AnnotationFlag.notNull)
832 		]);
833 	assert(m.fields[i].internalAnnotations.length == 1);
834 	assert(m.fields[i].internalAnnotations[0].match!((ValidatorRef r) => true, _ => false));
835 }
836 
837 unittest
838 {
839 	struct Mod
840 	{
841 		abstract class NamedThing : Model
842 		{
843 			@Id long id;
844 
845 			@maxLength(255)
846 			string name;
847 		}
848 
849 		class User : NamedThing
850 		{
851 			int age;
852 		}
853 	}
854 
855 	auto mod = processModelsToDeclarations!Mod;
856 	assert(mod.models.length == 1);
857 
858 	auto m = mod.models[0];
859 	assert(m.tableName == "user");
860 	assert(m.fields.length == 3);
861 	assert(m.fields[0].columnName == "id");
862 	assert(m.fields[1].columnName == "name");
863 	assert(m.fields[2].columnName == "age");
864 }
865 
866 unittest
867 {
868 	struct Mod
869 	{
870 		struct SuperCommon
871 		{
872 			int superCommonField;
873 		}
874 
875 		struct Common
876 		{
877 			string commonName;
878 			@embedded
879 			SuperCommon superCommon;
880 		}
881 
882 		class NamedThing : Model
883 		{
884 			@Id long id;
885 
886 			@embedded
887 			Common common;
888 
889 			@maxLength(255)
890 			string name;
891 		}
892 	}
893 
894 	auto mod = processModelsToDeclarations!Mod;
895 	assert(mod.models.length == 1);
896 	auto m = mod.models[0];
897 	assert(m.tableName == "named_thing");
898 	assert(m.fields.length == 4);
899 	assert(m.fields[1].columnName == "common_name");
900 	assert(m.fields[1].sourceColumn == "common.commonName");
901 	assert(m.fields[2].columnName == "super_common_field");
902 	assert(m.fields[2].sourceColumn == "common.superCommon.superCommonField");
903 	assert(m.fields[3].columnName == "name");
904 	assert(m.fields[3].sourceColumn == "name");
905 	assert(m.embeddedStructs == ["common", "common.superCommon"]);
906 }
907 
908 // https://github.com/myOmikron/drorm/issues/6
909 unittest
910 {
911 	struct Mod
912 	{
913 		class NamedThing : Model
914 		{
915 			@Id long id;
916 
917 			@timestamp
918 			Nullable!long timestamp1;
919 
920 			@autoUpdateTime
921 			Nullable!long timestamp2;
922 
923 			@autoCreateTime
924 			long timestamp3;
925 
926 			@autoUpdateTime @autoCreateTime
927 			long timestamp4;
928 		}
929 	}
930 
931 	auto mod = processModelsToDeclarations!Mod;
932 	assert(mod.models.length == 1);
933 	auto m = mod.models[0];
934 	assert(m.tableName == "named_thing");
935 	assert(m.fields.length == 5);
936 
937 	assert(m.fields[1].columnName == "timestamp_1");
938 	assert(m.fields[1].isNullable);
939 	assert(m.fields[2].isNullable);
940 	assert(!m.fields[3].isNullable);
941 	assert(!m.fields[4].isNullable);
942 }
943 
944 unittest
945 {
946 	struct Mod
947 	{
948 		class DefaultValues : Model
949 		{
950 			@Id long id;
951 
952 			@defaultValue(10)
953 			int f1;
954 
955 			@defaultValue(2)
956 			int f2 = 1337;
957 
958 			@defaultFromInit
959 			int f3 = 1337;
960 		}
961 	}
962 
963 	auto mod = processModelsToDeclarations!Mod;
964 	assert(mod.models.length == 1);
965 	auto m = mod.models[0];
966 	assert(m.fields.length == 4);
967 
968 	assert(m.fields[1].columnName == "f_1");
969 	assert(m.fields[1].annotations == [
970 		DBAnnotation(AnnotationFlag.notNull),
971 		DBAnnotation(defaultValue(10))
972 	]);
973 
974 	assert(m.fields[2].columnName == "f_2");
975 	assert(m.fields[2].annotations == [
976 		DBAnnotation(AnnotationFlag.notNull),
977 		DBAnnotation(defaultValue(2))
978 	]);
979 
980 	assert(m.fields[3].columnName == "f_3");
981 	assert(m.fields[3].annotations == [
982 		DBAnnotation(AnnotationFlag.notNull),
983 		DBAnnotation(defaultValue(1337))
984 	]);
985 }
986 
987 unittest
988 {
989 	struct Mod
990 	{
991 		class User : Model
992 		{
993 			@maxLength(255) @primaryKey
994 			string username;
995 		}
996 
997 		class Toot : Model
998 		{
999 			@Id long id;
1000 
1001 			ModelRef!(User.username) author;
1002 		}
1003 	}
1004 
1005 	static assert(DormForeignKeys!(Mod.Toot).length == 1);
1006 	static assert(DormForeignKeys!(Mod.Toot)[0].columnName == "author");
1007 
1008 	auto mod = processModelsToDeclarations!Mod;
1009 	assert(mod.models.length == 2);
1010 	auto m = mod.models[0];
1011 	assert(m.fields.length == 1);
1012 
1013 	assert(m.fields[0].columnName == "username");
1014 	assert(m.fields[0].annotations == [
1015 		DBAnnotation(maxLength(255)),
1016 		DBAnnotation(AnnotationFlag.primaryKey)
1017 	]);
1018 
1019 	m = mod.models[1];
1020 	assert(m.fields.length == 2);
1021 
1022 	assert(m.fields[1].columnName == "author");
1023 	assert(m.fields[1].annotations == [
1024 		DBAnnotation(AnnotationFlag.notNull),
1025 		DBAnnotation(maxLength(255)),
1026 		DBAnnotation(ForeignKeyImpl(
1027 			"user", "username",
1028 			restrict, restrict
1029 		))
1030 	]);
1031 }
1032 
1033 unittest
1034 {
1035 	// cyclic ref
1036 	struct Mod
1037 	{
1038 		class User : Model
1039 		{
1040 			@Id long id;
1041 
1042 			ModelRef!(Project.otherId) favoriteProject;
1043 		}
1044 
1045 		class Project : Model
1046 		{
1047 			@primaryKey @maxLength(123) string otherId;
1048 
1049 			ModelRef!(User.id) author;
1050 		}
1051 	}
1052 
1053 	auto mod = processModelsToDeclarations!Mod;
1054 	assert(mod.models.length == 2);
1055 	auto m = mod.models[0];
1056 	assert(m.fields.length == 2);
1057 
1058 	assert(m.fields[0].columnName == "id");
1059 	assert(m.fields[0].annotations == [
1060 		DBAnnotation(AnnotationFlag.primaryKey),
1061 		DBAnnotation(AnnotationFlag.autoIncrement),
1062 	]);
1063 
1064 	assert(m.fields[1].columnName == "favorite_project");
1065 	assert(m.fields[1].annotations == [
1066 		DBAnnotation(AnnotationFlag.notNull),
1067 		DBAnnotation(maxLength(123)),
1068 		DBAnnotation(ForeignKeyImpl(
1069 			"project", "other_id",
1070 			restrict, restrict
1071 		))
1072 	]);
1073 
1074 	m = mod.models[1];
1075 	assert(m.fields.length == 2);
1076 
1077 	assert(m.fields[0].columnName == "other_id");
1078 	assert(m.fields[0].annotations == [
1079 		DBAnnotation(AnnotationFlag.primaryKey),
1080 		DBAnnotation(maxLength(123)),
1081 	]);
1082 
1083 	assert(m.fields[1].columnName == "author");
1084 	assert(m.fields[1].annotations == [
1085 		DBAnnotation(AnnotationFlag.notNull),
1086 		DBAnnotation(ForeignKeyImpl(
1087 			"user", "id",
1088 			restrict, restrict
1089 		))
1090 	]);
1091 }