1 /** 2 * This whole package is used for the declarative model descriptions. The 3 * declarative descriptions are automatically generated from the D source code 4 * and are used for the diff process for the migrations generator. 5 * 6 * The conversion from D classes/structs + UDAs into the declarative format 7 * described in this module is done inside the $(REF conversion, dorm,declarative) 8 * module. 9 */ 10 module dorm.declarative; 11 12 import dorm.annotations; 13 import dorm.model; 14 15 import std.algorithm; 16 import std.array; 17 import std.sumtype; 18 import std.typecons : tuple; 19 20 import mir.serde; 21 import mir.algebraic_alias.json; 22 23 /** 24 * This is the root of a described models module. It contains a list of models 25 * as defined in the D source file. 26 * 27 * The `validators` and `valueConstructors` maps contain the global functions 28 * defined in the $(REF defaultValue, dorm,annotations) and $(REF validator, 29 * dorm,annotations) UDAs. 30 */ 31 struct SerializedModels 32 { 33 /// List of all the models defined in the full module file. 34 @serdeKeys("Models") 35 ModelFormat[] models; 36 } 37 38 /** 39 * Describes a single Model class (Database Table) in a generic format that is 40 * only later used by the drivers to actually convert to SQL statements. 41 */ 42 struct ModelFormat 43 { 44 /** 45 * Describes a field inside the Model class, which corresponds to a column 46 * inside the actual database table later. It's using a generic format that 47 * is only later used by the drivers to actually convert to SQL statements. 48 */ 49 struct Field 50 { 51 @safe: 52 /// List of different (generic) database column types. 53 @serdeProxy!string 54 enum DBType 55 { 56 varchar, /// inferred from `string` 57 varbinary, /// inferred from `ubyte[]` 58 int8, /// inferred from `byte` 59 int16, /// inferred from `short` 60 int32, /// inferred from `int` 61 int64, /// inferred from `long` 62 floatNumber, /// inferred from `float` 63 doubleNumber, /// inferred from `double` 64 boolean, /// inferred from `bool` 65 date, /// inferred from `std.datetime : Date` 66 datetime, /// inferred from `std.datetime : DateTime`, `std.datetime : SysTime`, `@AutoCreateTime ulong`, `@AutoUpdateTime ulong`, `@timestamp ulong` (always saved UTC) 67 time, /// inferred from `std.datetime : TimeOfDay` 68 choices, /// inferred from `@choices string`, `enum T` 69 set, /// inferred from `BitFlags!enum` 70 } 71 72 /// The exact name of the column later used in the DB, not neccessarily 73 /// corresponding to the D field name anymore. 74 @serdeKeys("Name") 75 string columnName; 76 /// Name of the field inside the D source code. 77 @serdeIgnore 78 string sourceColumn; 79 /// D type stringof. 80 @serdeIgnore 81 string sourceType; 82 /// The generic column type that is later translated to a concrete SQL 83 /// type by a driver. 84 @serdeKeys("Type") 85 DBType type; 86 /// List of different annotations defined in the source code, converted 87 /// to a serializable format and also all implicit annotations such as 88 /// `Choices` for enums. 89 @serdeKeys("Annotations") 90 DBAnnotation[] annotations; 91 /// List of annotations only relevant for internal use. 92 @serdeIgnore 93 InternalAnnotation[] internalAnnotations; 94 /// For debugging purposes this is the D source code location where this 95 /// field is defined from. This can be used in error messages. 96 @serdeKeys("SourceDefinedAt") 97 SourceLocation definedAt; 98 99 @serdeIgnore 100 string selectorColumnName(string tableName) const @property 101 { 102 return tableName ~ "." ~ columnName ~ " AS __" ~ columnName; 103 } 104 105 /// Returns true if this field does not have the `notNull` AnnotationFlag 106 /// assigned, otherwise false. 107 @serdeIgnore 108 bool isNullable() const @property 109 { 110 return !hasFlag(AnnotationFlag.notNull) 111 && !hasFlag(AnnotationFlag.primaryKey); 112 } 113 114 /// Returns true iff this field has the `primaryKey` AnnotationFlag. 115 @serdeIgnore 116 bool isPrimaryKey() const @property 117 { 118 return hasFlag(AnnotationFlag.primaryKey); 119 } 120 121 /// Returns true iff this field has the `ForeignKeyImpl` annotation. 122 @serdeIgnore 123 bool isForeignKey() const @property 124 { 125 foreach (annotation; annotations) 126 { 127 if (annotation.value.match!( 128 (ForeignKeyImpl f) => true, 129 _ => false 130 )) 131 return true; 132 } 133 return false; 134 } 135 136 /// Returns true iff this field has the given AnnotationFlag assigned. 137 @serdeIgnore 138 bool hasFlag(AnnotationFlag q) const @property 139 { 140 foreach (annotation; annotations) 141 { 142 if (annotation.value.match!( 143 (AnnotationFlag f) => f == q, 144 _ => false 145 )) 146 return true; 147 } 148 return false; 149 } 150 151 @serdeIgnore 152 bool hasDefaultValue() const @property 153 { 154 import std.datetime; 155 156 foreach (annotation; annotations) 157 { 158 if (annotation.value.match!( 159 (d) { 160 static assert(is(typeof(d) : DefaultValue!T, T)); 161 return true; 162 }, 163 _ => false 164 )) 165 return true; 166 } 167 return false; 168 } 169 170 /// Human-readable description how fields with auto-generated values 171 /// (non-required values) can be specified. 172 static immutable string humanReadableGeneratedDefaultValueTypes = 173 `Annotations for automatic value generation: @defaultValue(v), ` 174 ~ `@defaultFromInit, @constructValue(() => v), @autoCreateTime, ` 175 ~ `@autoIncrement or change type to Nullable!T for default null.`; 176 177 /** 178 * Returns true if: 179 * - this field has some annotation that auto-generates a value if it's 180 * not provided in an insert statement, 181 * (@defaultValue, @autoCreateTime, @autoIncrement) 182 * - has a `@constructValue` annotation (which is handled in db.d) 183 * - is nullable (e.g. of type `Nullable!T`), which implies that `null` 184 * is the default value. 185 */ 186 @serdeIgnore 187 bool hasGeneratedDefaultValue() const @property 188 { 189 return hasDefaultValue 190 || hasConstructValue 191 || isNullable 192 || hasFlag(AnnotationFlag.autoCreateTime) 193 || hasFlag(AnnotationFlag.autoIncrement); 194 } 195 196 /** 197 * Returns true if this field has a $(REF constructValue, dorm.annotations) 198 * annotation. 199 */ 200 @serdeIgnore 201 bool hasConstructValue() const @property 202 { 203 import std.datetime; 204 205 foreach (annotation; internalAnnotations) 206 { 207 if (annotation.match!( 208 (const ConstructValueRef c) => true, 209 _ => false 210 )) 211 return true; 212 } 213 return false; 214 } 215 216 /** 217 * Returns true if this field is the default `id` field defined in the 218 * $(REF Model, dorm.model) super-class. 219 */ 220 @serdeIgnore 221 bool isBuiltinId() const @property 222 { 223 return sourceColumn == "_fallbackId"; 224 } 225 226 @serdeIgnore 227 string sourceReferenceName(string modelName = null) const @property 228 { 229 if (modelName.length) 230 return sourceType ~ " " ~ sourceColumn 231 ~ " in " ~ modelName ~ " (from " 232 ~ definedAt.toString ~ ")"; 233 else 234 return sourceType ~ " " ~ sourceColumn 235 ~ " (from " ~ definedAt.toString ~ ")"; 236 } 237 238 @serdeIgnore 239 DBAnnotation[] foreignKeyAnnotations() const @property 240 { 241 DBAnnotation[] ret; 242 foreach (annotation; annotations) 243 if (annotation.isForeignKeyInheritable) 244 ret ~= annotation; 245 return ret; 246 } 247 248 @serdeIgnore 249 string toPrettySourceString() const 250 { 251 string ret = sourceColumn ~ " : " ~ sourceType; 252 if (isBuiltinId) 253 ret ~= " [builtin ID]"; 254 else if (isPrimaryKey) 255 ret ~= " [primary]"; 256 257 if (isNullable) 258 ret ~= " [nullable]"; 259 if (hasGeneratedDefaultValue) 260 ret ~= " [has default]"; 261 return ret; 262 } 263 } 264 265 /// The exact name of the table later used in the DB, not neccessarily 266 /// corresponding to the D class name anymore. 267 @serdeKeys("Name") 268 string tableName; 269 /// For debugging purposes this is the D source code location where this 270 /// field is defined from. This can be used in error messages. 271 @serdeKeys("SourceDefinedAt") 272 SourceLocation definedAt; 273 /// List of fields, such as defined in the D source code, recursively 274 /// including all fields from all inherited classes. This maps to the actual 275 /// SQL columns later when it is generated into an SQL create statement by 276 /// the actual driver implementation. 277 @serdeKeys("Fields") 278 Field[] fields; 279 /// Lists the source field names for embedded structs, recursively. 280 @serdeIgnore 281 string[] embeddedStructs; 282 283 /// Perform checks if the model description seems valid (does not validate 284 /// fields, only general model things) 285 package string lint(string errorPrefix) 286 { 287 string errors; 288 289 bool hasPrimary; 290 // https://github.com/myOmikron/drorm/issues/7 291 Field[] autoIncrementFields; 292 foreach (field; fields) 293 { 294 if (field.hasFlag(AnnotationFlag.autoIncrement)) 295 autoIncrementFields ~= field; 296 if (field.hasFlag(AnnotationFlag.primaryKey)) 297 hasPrimary = true; 298 } 299 300 if (autoIncrementFields.length > 1) 301 { 302 errors ~= errorPrefix ~ "Multiple fields with @autoIncrement defined, only one is allowed:"; 303 foreach (field; autoIncrementFields) 304 errors ~= errorPrefix ~ "\t" ~ field.sourceReferenceName(tableName); 305 } 306 307 if (!hasPrimary) 308 errors ~= errorPrefix ~ "No primary key defined on this model. Consider adding a simple auto-increment integer:" 309 ~ errorPrefix ~ "\t`@Id long id;`"; 310 311 return errors; 312 } 313 } 314 315 /** 316 * The source location where something is defined in D code. 317 * 318 * The implementation uses [__traits(getLocation)](https://dlang.org/spec/traits.html#getLocation) 319 */ 320 struct SourceLocation 321 { 322 /// The D filename, assumed to be of the same format as [__FILE__](https://dlang.org/spec/expression.html#specialkeywords). 323 @serdeKeys("File") 324 string sourceFile; 325 /// The 1-based line number and column number where the symbol is defined. 326 @serdeKeys("Line") 327 int sourceLine; 328 /// ditto 329 @serdeKeys("Column") 330 int sourceColumn; 331 332 string toString() const @safe 333 { 334 import std.conv : text; 335 336 string ret = text(sourceFile, "(", sourceLine, ",", sourceColumn, ")"); 337 if (__ctfe) 338 return ret.idup; 339 else 340 return ret; 341 } 342 343 /// Same as toString, but bolds the string using ANSI escape codes 344 string toErrorString() const @safe 345 { 346 return "\x1B[1m" ~ toString ~ ": \x1B[1;31mError: (DORM)\x1B[m"; 347 } 348 } 349 350 /** 351 * This enum contains all no-argument flags that can be added as annotation to 352 * the fields. It's part of the $(LREF DBAnnotation) SumType. 353 */ 354 enum AnnotationFlag 355 { 356 /// corresponds to the $(REF autoCreateTime, dorm,annotations) UDA. 357 autoCreateTime, 358 /// corresponds to the $(REF autoUpdateTime, dorm,annotations) UDA. 359 autoUpdateTime, 360 /// corresponds to the $(REF autoIncrement, dorm,annotations) UDA. 361 autoIncrement, 362 /// corresponds to the $(REF primaryKey, dorm,annotations) UDA. 363 primaryKey, 364 /// corresponds to the $(REF unique, dorm,annotations) UDA. 365 unique, 366 /// corresponds to the $(REF notNull, dorm,annotations) UDA. Implicit for all types except Nullable!T and Model. 367 notNull 368 } 369 370 private bool isCompatibleFlags(AnnotationFlag a, AnnotationFlag b) @safe 371 { 372 final switch (a) with (AnnotationFlag) 373 { 374 case autoCreateTime: return !b.among!( 375 autoIncrement, 376 primaryKey, 377 unique, 378 ); 379 case autoUpdateTime: return !b.among!( 380 autoIncrement, 381 primaryKey, 382 unique, 383 ); 384 case autoIncrement: return !b.among!( 385 autoCreateTime, 386 autoUpdateTime, 387 ); 388 case primaryKey: return !b.among!( 389 autoCreateTime, 390 autoUpdateTime, 391 notNull, 392 ); 393 case unique: return !b.among!( 394 autoCreateTime, 395 autoUpdateTime, 396 ); 397 case notNull: return !b.among!( 398 primaryKey 399 ); 400 } 401 } 402 403 /** 404 * SumType combining all the different annotations (UDAs) that can be added to 405 * a model field, in a serializable format. (e.g. the lambdas are moved into a 406 * helper field in the model description and these annotations only contain an 407 * integer to reference it) 408 */ 409 @serdeProxy!IonDBAnnotation 410 struct DBAnnotation 411 { 412 @safe: 413 SumType!( 414 AnnotationFlag, 415 maxLength, 416 PossibleDefaultValueTs, 417 Choices, 418 index, 419 ForeignKeyImpl 420 ) value; 421 alias value this; 422 423 this(T)(T v) 424 { 425 value = v; 426 } 427 428 auto opAssign(T)(T v) 429 { 430 value = v; 431 return this; 432 } 433 434 /// Returns true if the other annotation can be used together with this one. 435 /// Must not call on itself, only on other instances. (which may be the same 436 /// attribute however) 437 bool isCompatibleWith(DBAnnotation other, bool firstTry = true) 438 { 439 return match!( 440 (AnnotationFlag lhs, AnnotationFlag rhs) => isCompatibleFlags(lhs, rhs), 441 (maxLength lhs, AnnotationFlag rhs) => !rhs.among!( 442 AnnotationFlag.autoCreateTime, 443 AnnotationFlag.autoUpdateTime, 444 AnnotationFlag.autoIncrement, 445 ), 446 (maxLength lhs, Choices rhs) => false, 447 (Choices lhs, AnnotationFlag rhs) => !rhs.among!( 448 AnnotationFlag.autoCreateTime, 449 AnnotationFlag.autoUpdateTime, 450 AnnotationFlag.autoIncrement, 451 AnnotationFlag.primaryKey, 452 AnnotationFlag.unique, 453 ), 454 (index lhs, AnnotationFlag rhs) => rhs != AnnotationFlag.primaryKey, 455 (lhs, AnnotationFlag rhs) { 456 static assert(is(typeof(lhs) : DefaultValue!T, T)); 457 return !rhs.among!( 458 AnnotationFlag.autoCreateTime, 459 AnnotationFlag.autoUpdateTime, 460 AnnotationFlag.autoIncrement, 461 AnnotationFlag.primaryKey, 462 AnnotationFlag.unique, 463 ); 464 }, 465 (lhs, _) { 466 static assert(is(typeof(lhs) : DefaultValue!T, T)); 467 return true; 468 }, 469 (index lhs, index rhs) => true, 470 (ForeignKeyImpl lhs, AnnotationFlag rhs) => !rhs.among!( 471 AnnotationFlag.autoCreateTime, 472 AnnotationFlag.autoUpdateTime, 473 AnnotationFlag.autoIncrement, 474 AnnotationFlag.primaryKey, 475 AnnotationFlag.unique, 476 ), 477 (ForeignKeyImpl lhs, ForeignKeyImpl rhs) => false, 478 (ForeignKeyImpl lhs, Choices rhs) => false, 479 (ForeignKeyImpl lhs, _) => true, 480 (a, b) => firstTry ? other.isCompatibleWith(this, false) : false 481 )(value, other.value); 482 } 483 484 bool isForeignKeyInheritable() const 485 { 486 return value.match!( 487 (const AnnotationFlag _) => false, 488 (const maxLength _) => true, 489 (someDefaultValue) { 490 static assert(is(typeof(someDefaultValue) : DefaultValue!T, T)); 491 return false; 492 }, 493 (const Choices _) => false, 494 (const index _) => false, 495 (const ForeignKeyImpl _) => false 496 ); 497 } 498 } 499 500 alias InternalAnnotation = SumType!( 501 ConstructValueRef, 502 ValidatorRef, 503 ); 504 505 private struct IonDBAnnotation 506 { 507 JsonAlgebraic data; 508 509 this(DBAnnotation a) @safe 510 { 511 a.match!( 512 (AnnotationFlag f) { 513 string typeStr; 514 final switch (f) 515 { 516 case AnnotationFlag.autoCreateTime: 517 typeStr = "auto_create_time"; 518 break; 519 case AnnotationFlag.autoUpdateTime: 520 typeStr = "auto_update_time"; 521 break; 522 case AnnotationFlag.notNull: 523 typeStr = "not_null"; 524 break; 525 case AnnotationFlag.autoIncrement: 526 typeStr = "auto_increment"; 527 break; 528 case AnnotationFlag.primaryKey: 529 typeStr = "primary_key"; 530 break; 531 case AnnotationFlag.unique: 532 typeStr = "unique"; 533 break; 534 } 535 data = JsonAlgebraic([ 536 "Type": JsonAlgebraic(typeStr) 537 ]); 538 }, 539 (maxLength l) { 540 data = JsonAlgebraic([ 541 "Type": JsonAlgebraic("max_length"), 542 "Value": JsonAlgebraic(l.maxLength) 543 ]); 544 }, 545 (Choices c) { 546 data = JsonAlgebraic([ 547 "Type": JsonAlgebraic("choices"), 548 "Value": JsonAlgebraic(c.choices.map!(v => JsonAlgebraic(v)).array) 549 ]); 550 }, 551 (index i) { 552 JsonAlgebraic[string] args; 553 if (i._composite !is i.composite.init) 554 args["Name"] = i._composite.name; 555 if (i._priority !is i.priority.init) 556 args["Priority"] = i._priority.priority; 557 558 if (args.empty) 559 data = JsonAlgebraic(["Type": JsonAlgebraic("index")]); 560 else 561 data = JsonAlgebraic([ 562 "Type": JsonAlgebraic("index"), 563 "Value": JsonAlgebraic(args) 564 ]); 565 }, 566 (DefaultValue!(ubyte[]) binary) { 567 import std.digest : toHexString; 568 569 data = JsonAlgebraic([ 570 "Type": JsonAlgebraic("default_value"), 571 "Value": JsonAlgebraic(binary.value.toHexString) 572 ]); 573 }, 574 (ForeignKeyImpl foreignKey) { 575 import std.digest : toHexString; 576 577 data = JsonAlgebraic([ 578 "Type": JsonAlgebraic("foreign_key"), 579 "Value": JsonAlgebraic([ 580 "TableName": JsonAlgebraic(foreignKey.table), 581 "ColumnName": JsonAlgebraic(foreignKey.column), 582 "OnUpdate": JsonAlgebraic(foreignKey.onUpdate.toPascalCase), 583 "OnDelete": JsonAlgebraic(foreignKey.onDelete.toPascalCase) 584 ]) 585 ]); 586 }, 587 (rest) { 588 static assert(is(typeof(rest) == DefaultValue!U, U)); 589 static if (__traits(hasMember, rest.value, "toISOExtString")) 590 { 591 data = JsonAlgebraic([ 592 "Type": JsonAlgebraic("default_value"), 593 "Value": JsonAlgebraic(rest.value.toISOExtString) 594 ]); 595 } 596 else 597 { 598 data = JsonAlgebraic([ 599 "Type": JsonAlgebraic("default_value"), 600 "Value": JsonAlgebraic(rest.value) 601 ]); 602 } 603 } 604 ); 605 } 606 607 void serialize(S)(scope ref S serializer) const 608 { 609 import mir.ser : serializeValue; 610 611 serializeValue(serializer, data); 612 } 613 } 614 615 private string toPascalCase(ReferentialAction type) @safe nothrow @nogc pure 616 { 617 final switch (type) 618 { 619 case restrict: return "Restrict"; 620 case cascade: return "Cascade"; 621 case setNull: return "SetNull"; 622 case setDefault: return "SetDefault"; 623 } 624 } 625 626 /** 627 * Corresponds to the $(REF constructValue, dorm,annotations) and $(REF 628 * constructValue, dorm,annotations) UDAs. 629 * 630 * A global function that is compiled into the executable through the call of 631 * $(REF processModelsToDeclarations, dorm,declarative) generating the 632 * `InternalAnnotation` values. Manually constructing this function is not 633 * required, use the $(REF RegisterModels, dorm,declarative,entrypoint) mixin 634 * instead. 635 * 636 * The functions take in a Model (class) instance and assert it is the correct 637 * model class type that it was registered with. 638 */ 639 struct ConstructValueRef 640 { 641 /** 642 * This function calls the UDA specified lambda without argument and 643 * sets the annotated field value inside the containing Model instance to 644 * its return value, with the code assuming it can simply assign it. 645 * (a compiler error will occur if it cannot implicitly convert to the 646 * annotated property type) 647 * 648 * The value itself is meaningless, it's only for later executing the actual 649 * callback through runValueConstructor. 650 */ 651 string rid; 652 } 653 654 /// ditto 655 struct ValidatorRef 656 { 657 /** 658 * This function calls the UDA specified lambda with the field as argument 659 * and returns its return value, with the code assuming it is a boolean. 660 * (a compiler error will occur if it cannot implicitly convert to `bool`) 661 * 662 * The value itself is meaningless, it's only for later executing the actual 663 * callback through runValueConstructor. 664 */ 665 string rid; 666 } 667 668 unittest 669 { 670 import mir.ser.json; 671 672 SerializedModels models; 673 ModelFormat m; 674 m.tableName = "foo"; 675 m.definedAt = SourceLocation("file.d", 140, 10); 676 ModelFormat.Field f; 677 f.columnName = "foo"; 678 f.type = ModelFormat.Field.DBType.varchar; 679 f.definedAt = SourceLocation("file.d", 142, 12); 680 f.annotations = [ 681 DBAnnotation(AnnotationFlag.notNull), 682 DBAnnotation(AnnotationFlag.primaryKey), 683 DBAnnotation(index()), 684 DBAnnotation(maxLength(255)) 685 ]; 686 f.internalAnnotations = [ 687 InternalAnnotation(ValidatorRef("NONE")) 688 ]; 689 m.fields = [f]; 690 691 models.models = [m]; 692 string json = serializeJsonPretty(models); 693 assert(json == `{ 694 "Models": [ 695 { 696 "Name": "foo", 697 "SourceDefinedAt": { 698 "File": "file.d", 699 "Line": 140, 700 "Column": 10 701 }, 702 "Fields": [ 703 { 704 "Name": "foo", 705 "Type": "varchar", 706 "Annotations": [ 707 { 708 "Type": "not_null" 709 }, 710 { 711 "Type": "primary_key" 712 }, 713 { 714 "Type": "index" 715 }, 716 { 717 "Type": "max_length", 718 "Value": 255 719 } 720 ], 721 "SourceDefinedAt": { 722 "File": "file.d", 723 "Line": 142, 724 "Column": 12 725 } 726 } 727 ] 728 } 729 ] 730 }`, json); 731 }