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