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 }