1 module dorm.api.db; 2 3 import dorm.declarative; 4 import dorm.declarative.conversion; 5 import dorm.lib.util; 6 import dorm.types; 7 import dorm.model : Model; 8 import ffi = dorm.lib.ffi; 9 10 import std.algorithm : any, move; 11 import std.range : chain; 12 import std.conv : text, to; 13 import std.datetime : Clock, Date, DateTime, DateTimeException, SysTime, TimeOfDay, UTC; 14 import std.meta; 15 import std.range.primitives; 16 import std.traits; 17 import std.typecons : Nullable; 18 19 import core.attribute; 20 import core.time; 21 22 public import dorm.types : DormPatch; 23 public import dorm.lib.ffi : DBBackend; 24 25 public import dorm.api.condition; 26 27 static if (!is(typeof(mustuse))) 28 private enum mustuse; // @suppress(dscanner.style.phobos_naming_convention) 29 30 /// Currently only a limited number of joins is supported per query, this could 31 /// configure it when it becomes a problem. This is due to a maximum number of 32 /// join aliases being available right now. 33 private enum maxJoins = 256; 34 35 /** 36 * Configuration operation to connect to a database. 37 */ 38 struct DBConnectOptions 39 { 40 @safe: 41 /// Specifies the driver that will be used. 42 DBBackend backend; 43 /// Name of the database, in case of `DatabaseBackend.SQLite` name of the file. 44 string name; 45 /// Host to connect to. Not used in case of `DatabaseBackend.SQLite`. 46 string host; 47 /// Port to connect to. Not used in case of `DatabaseBackend.SQLite`. 48 ushort port; 49 /// Username to authenticate with. Not used in case of `DatabaseBackend.SQLite`. 50 string user; 51 /// Password to authenticate with. Not used in case of `DatabaseBackend.SQLite`. 52 string password; 53 /// Minimal connections to initialize upfront. Must not be 0. 54 uint minConnections = ffi.DBConnectOptions.init.minConnections; 55 /// Maximum connections that allowed to be created. Must not be 0. 56 uint maxConnections = ffi.DBConnectOptions.init.maxConnections; 57 } 58 59 /** 60 * High-level wrapper around a database. Through the driver implementation layer 61 * this handles connection pooling and distributes work across a thread pool 62 * automatically. 63 * 64 * Use the (UFCS) methods 65 * 66 * - $(LREF select) 67 * - $(LREF update) 68 * - $(LREF insert) 69 * 70 * to access the database. 71 * 72 * This struct cannot be copied, to pass it around, use `ref` or `move`. Once 73 * the struct goes out of scope or gets unset, the connection to the database 74 * will be freed. 75 */ 76 struct DormDB 77 { 78 @safe: 79 private ffi.DBHandle handle; 80 81 @disable this(); 82 83 /** 84 * Performs a Database connection (possibly in another thread) and returns 85 * the constructed DormDB handle once connected. 86 */ 87 this(DBConnectOptions options) @trusted 88 { 89 auto ffiOptions = options.ffiInto!(ffi.DBConnectOptions); 90 91 scope dbHandleAsync = FreeableAsyncResult!(ffi.DBHandle).make; 92 ffi.rorm_db_connect(ffiOptions, dbHandleAsync.callback.expand); 93 handle = dbHandleAsync.result; 94 } 95 96 ~this() @trusted 97 { 98 if (handle) 99 { 100 ffi.rorm_db_free(handle); 101 handle = null; 102 } 103 } 104 105 @disable this(this); 106 107 /// Starts a database transaction, on which most operations can be called. 108 /// 109 /// Gets automatically rolled back if commit isn't called and the 110 /// transaction goes out of scope, but it's recommended to explicitly 111 /// call `rollback` to clarify the intent. 112 DormTransaction startTransaction() return 113 { 114 ffi.DBTransactionHandle txHandle; 115 (() @trusted { 116 auto ctx = FreeableAsyncResult!(ffi.DBTransactionHandle).make; 117 ffi.rorm_db_start_transaction(this.handle, ctx.callback.expand); 118 txHandle = ctx.result(); 119 })(); 120 return DormTransaction(&this, txHandle); 121 } 122 123 /// Database operation to INSERT a single value or multiple values when a 124 /// slice is passed into `insert`. 125 /// 126 /// It's possible to insert full Model instances, in which case every field 127 /// of the model is used for the insertion. (also the primary key) 128 /// 129 /// It's also possible to insert DormPatch instances to only pass the 130 /// available fields into the SQL insert statement. This means default 131 /// values will be auto-generated if possible. 132 /// (see $(REF hasGeneratedDefaultValue, dorm,declarative,ModelFormat,Field)) 133 /// 134 /// This is the place where `@constructValue` constructors are called. 135 /// 136 /// This method can also be used on transactions. 137 void insert(T)(T value) 138 if (!is(T == U[], U)) 139 { 140 return (() @trusted => insertImpl!true(handle, (&value)[0 .. 1], null))(); 141 } 142 143 /// ditto 144 void insert(T)(scope T[] value) 145 { 146 return insertImpl!false(handle, value, null); 147 } 148 149 /** 150 * Returns a builder struct that can be used to perform an UPDATE statement 151 * in the SQL database on the provided Model table. 152 * 153 * See_Also: `DormTransaction.update` 154 */ 155 UpdateOperation!T update(T : Model)() return pure 156 { 157 return UpdateOperation!T(&this, null); 158 } 159 160 /** 161 * Returns a builder struct that can be used to perform a DELETE statement 162 * in the SQL database on the provided Model table. 163 * 164 * See_Also: `DormTransaction.remove` 165 */ 166 RemoveOperation!T remove(T : Model)() return pure 167 { 168 return RemoveOperation!T(&this, null); 169 } 170 171 /** 172 * Deletes the given model instance from the database. 173 * 174 * Equivalent to calling `db.remove!T.single(instance)`. 175 * 176 * See_Also: `RemoveOperation.single` 177 * 178 * Returns: true if anything was deleted, false otherwise. 179 */ 180 bool remove(T : Model)(T instance) return 181 { 182 return remove!T.single(instance); 183 } 184 185 /// ditto 186 bool remove(TPatch)(TPatch instance) return 187 if (!is(TPatch : Model) && isSomePatch!TPatch) 188 { 189 alias T = DBType!TPatch; 190 return remove!T.single(instance); 191 } 192 193 /** 194 * This function executes a raw SQL statement. 195 * 196 * Iterate over the result using `foreach`. 197 * 198 * Statements are executed as prepared statements, if possible. 199 * 200 * To define placeholders, use `?` in SQLite and MySQL and $1, $n in Postgres. 201 * The corresponding parameters are bound in order to the query. 202 * 203 * The number of placeholder must match with the number of provided bind 204 * parameters. 205 * 206 * Params: 207 * queryString = SQL statement to execute. 208 * bindParams = Parameters to fill into placeholders of `queryString`. 209 * 210 * See_Also: `DormTransaction.rawSQL` 211 */ 212 RawSQLIterator rawSQL( 213 scope return const(char)[] queryString, 214 scope return ffi.FFIValue[] bindParams = null 215 ) return pure 216 { 217 return RawSQLIterator(&this, null, queryString, bindParams); 218 } 219 } 220 221 // defined this as global so that we can pass `Foo.fieldName` as alias argument, 222 // to have it be selected. 223 /** 224 * Starts a builder struct that can be used to SELECT (query) data from the 225 * database. 226 * 227 * It's possible to query full Model instances (get all fields), which are 228 * allocated by the GC. It's also possible to only query parts of a Model, for 229 * which DormPatch types are used, which is useful for improved query 230 * performance when only using parts of a Model as well as reusing the data in 231 * later update calls. (if the primary key is included in the patch) 232 * 233 * See `SelectOperation` for possible conditions and how to extract data. 234 * 235 * This method can also be used on transactions. 236 */ 237 static SelectOperation!(DBType!(Selection), SelectType!(Selection)) select( 238 Selection... 239 )( 240 return ref const DormDB db 241 ) @trusted 242 { 243 return typeof(return)(&db, null); 244 } 245 246 /// ditto 247 static SelectOperation!(DBType!(Selection), SelectType!(Selection)) select( 248 Selection... 249 )( 250 return ref const DormTransaction tx 251 ) @trusted 252 { 253 return typeof(return)(tx.db, tx.txHandle); 254 } 255 256 /// Helper struct that makes it possible to `foreach` over the `rawSQL` result. 257 @mustuse struct RawSQLIterator 258 { 259 private DormDB* db; 260 private ffi.DBTransactionHandle tx; 261 private const(char)[] queryString; 262 private ffi.FFIValue[] bindParams; 263 private size_t rowCountImpl = -1; 264 265 /// Returns the number of rows, only valid inside the foreach. 266 size_t rowCount() 267 { 268 assert(rowCountImpl != -1, "Calling rowCount is only valid inside the foreach / opApply"); 269 return rowCountImpl; 270 } 271 272 // TODO: delegate with @safe / @system differences + index overloads + don't mark whole thing as @trusted 273 /// Starts a new query and iterates all the results on each foreach call. 274 int opApply(scope int delegate(scope RawRow row) dg) @trusted 275 { 276 scope (exit) 277 rowCountImpl = -1; 278 assert(rowCountImpl == -1, "Don't iterate over the same RawSQLIterator on multiple threads!"); 279 280 int result = 0; 281 auto ctx = FreeableAsyncResult!(void delegate(scope ffi.FFIArray!(ffi.DBRowHandle))).make; 282 ctx.forward_callback = (scope rows) { 283 rowCountImpl = rows.size; 284 foreach (row; rows[]) 285 { 286 result = dg(RawRow(row)); 287 if (result) 288 break; 289 } 290 }; 291 ffi.rorm_db_raw_sql(db.handle, 292 tx, 293 ffi.ffi(queryString), 294 ffi.ffi(bindParams), 295 ctx.callback.expand); 296 ctx.result(); 297 return result; 298 } 299 300 /// Runs the raw SQL query, discarding results (throwing on error) 301 void exec() 302 { 303 assert(rowCountImpl == -1, "Don't iterate over the same RawSQLIterator on multiple threads!"); 304 305 auto ctx = FreeableAsyncResult!(void delegate(scope ffi.FFIArray!(ffi.DBRowHandle))).make; 306 ctx.forward_callback = (scope rows) {}; 307 ffi.rorm_db_raw_sql(db.handle, 308 tx, 309 ffi.ffi(queryString), 310 ffi.ffi(bindParams), 311 ctx.callback.expand); 312 ctx.result(); 313 } 314 } 315 316 /// Allows column access on a raw DB row as returned by `db.rawSQL`. 317 struct RawRow 318 { 319 private ffi.DBRowHandle row; 320 321 @disable this(this); 322 323 private static template ffiConvPrimitive(T) 324 { 325 static if (is(T == short)) 326 alias ffiConvPrimitive = ffi.rorm_row_get_i16; 327 else static if (is(T == int)) 328 alias ffiConvPrimitive = ffi.rorm_row_get_i32; 329 else static if (is(T == long)) 330 alias ffiConvPrimitive = ffi.rorm_row_get_i64; 331 else static if (is(T == float)) 332 alias ffiConvPrimitive = ffi.rorm_row_get_f32; 333 else static if (is(T == double)) 334 alias ffiConvPrimitive = ffi.rorm_row_get_f64; 335 else static if (is(T == bool)) 336 alias ffiConvPrimitive = ffi.rorm_row_get_bool; 337 else 338 static assert(false, "Unsupported column type: " ~ T.stringof); 339 } 340 341 /// Gets the value of the column at the given column name assuming it is of 342 /// the given type. If the value is not of the given type, an exception will 343 /// be thrown. 344 /// 345 /// Supported types: 346 /// - any string type (auto-converted from strings / varchar) 347 /// - `ubyte[]` for binary data 348 /// - `short`, `int`, `long`, `float`, `double`, `bool` 349 /// 350 /// For nullable values, use $(LREF opt) instead. 351 T get(T)(scope const(char)[] column) 352 { 353 auto ffiColumn = ffi.ffi(column); 354 ffi.RormError error; 355 T result; 356 357 static if (isSomeString!T) 358 { 359 auto slice = ffi.rorm_row_get_str(row, ffiColumn, error); 360 if (!error) 361 { 362 static if (is(T : char[])) 363 result = cast(T)slice[].dup; 364 else 365 result = slice[].to!T; 366 } 367 } 368 else static if (is(T : ubyte[])) 369 { 370 auto slice = ffi.rorm_row_get_binary(row, ffiColumn, error); 371 if (!error) 372 result = cast(T)slice[].dup; 373 } 374 else 375 { 376 alias fn = ffiConvPrimitive!T; 377 result = fn(row, ffiColumn, error); 378 } 379 380 if (error) 381 throw error.makeException(" (in column '" ~ column.idup ~ "')"); 382 return result; 383 } 384 385 private static template ffiConvOptionalPrimitive(T) 386 { 387 static if (is(T == short)) 388 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_i16; 389 else static if (is(T == int)) 390 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_i32; 391 else static if (is(T == long)) 392 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_i64; 393 else static if (is(T == float)) 394 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_f32; 395 else static if (is(T == double)) 396 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_f64; 397 else static if (is(T == bool)) 398 alias ffiConvOptionalPrimitive = ffi.rorm_row_get_null_bool; 399 else 400 static assert(false, "Unsupported column type: " ~ T.stringof); 401 } 402 403 /// Same as get, wraps primitives inside Nullable!T. Strings and ubyte[] 404 /// binary arrays will return `null` (checkable with `is null`), but 405 /// otherwise simply be embedded. 406 auto opt(T)(scope const(char)[] column) 407 { 408 auto ffiColumn = ffi.ffi(column); 409 ffi.RormError error; 410 411 static if (isSomeString!T) 412 { 413 auto slice = ffi.rorm_row_get_null_str(row, ffiColumn, error); 414 if (!error) 415 { 416 if (slice.isNull) 417 return null; 418 static if (is(T : char[])) 419 return cast(T)slice.raw_value[].dup; 420 else 421 return slice.raw_value[].to!T; 422 } 423 else 424 throw error.makeException(" (in column '" ~ column.idup ~ "')"); 425 } 426 else static if (is(T : ubyte[])) 427 { 428 auto slice = ffi.rorm_row_get_null_binary(row, ffiColumn, error); 429 if (slice.isNull) 430 return null; 431 if (!error) 432 return cast(T)slice.raw_value[].dup; 433 else 434 throw error.makeException(" (in column '" ~ column.idup ~ "')"); 435 } 436 else 437 { 438 Nullable!T result; 439 alias fn = ffiConvOptionalPrimitive!T; 440 auto opt = fn(row, ffiColumn, error); 441 if (error) 442 throw error.makeException(" (in column '" ~ column.idup ~ "')"); 443 if (!opt.isNull) 444 result = opt.raw_value; 445 return result; 446 } 447 } 448 } 449 450 /** 451 * Wrapper around a Database transaction. Most methods that can be used on a 452 * DormDB can also be used on a transaction. 453 * 454 * Performs a rollback when going out of scope and wasn't committed or rolled 455 * back explicitly. 456 */ 457 struct DormTransaction 458 { 459 @safe: 460 private DormDB* db; 461 private ffi.DBTransactionHandle txHandle; 462 463 @disable this(this); 464 465 ~this() 466 { 467 if (txHandle) 468 { 469 rollback(); 470 } 471 } 472 473 /// Commits this transaction, so the changes are recorded to the current 474 /// database state. 475 void commit() 476 { 477 scope (exit) txHandle = null; 478 (() @trusted { 479 auto ctx = FreeableAsyncResult!void.make; 480 ffi.rorm_transaction_commit(txHandle, ctx.callback.expand); 481 ctx.result(); 482 })(); 483 } 484 485 /// Rolls back this transaction, so the DB changes are reverted to before 486 /// the transaction was started. 487 void rollback() 488 { 489 scope (exit) txHandle = null; 490 (() @trusted { 491 auto ctx = FreeableAsyncResult!void.make; 492 ffi.rorm_transaction_rollback(txHandle, ctx.callback.expand); 493 ctx.result(); 494 })(); 495 } 496 497 /// Transacted variant of $(LREF DormDB.insert). Can insert a single value 498 /// or multiple values at once. 499 void insert(T)(T value) 500 { 501 return (() @trusted => insertImpl!true(db.handle, (&value)[0 .. 1], txHandle))(); 502 } 503 504 /// ditto 505 void insert(T)(scope T[] value) 506 { 507 return insertImpl!false(db.handle, value, txHandle); 508 } 509 510 /** 511 * This function executes a raw SQL statement. 512 * 513 * Iterate over the result using `foreach`. 514 * 515 * Statements are executed as prepared statements, if possible. 516 * 517 * To define placeholders, use `?` in SQLite and MySQL and $1, $n in Postgres. 518 * The corresponding parameters are bound in order to the query. 519 * 520 * The number of placeholder must match with the number of provided bind 521 * parameters. 522 * 523 * Params: 524 * queryString = SQL statement to execute. 525 * bindParams = Parameters to fill into placeholders of `queryString`. 526 * 527 * See_Also: `DormDB.rawSQL` 528 */ 529 RawSQLIterator rawSQL( 530 scope return const(char)[] queryString, 531 scope return ffi.FFIValue[] bindParams = null 532 ) return pure 533 { 534 return RawSQLIterator(db, txHandle, queryString, bindParams); 535 } 536 537 /** 538 * Returns a builder struct that can be used to perform an update statement 539 * in the SQL database on the provided Model table. 540 * 541 * See_Also: `DormDB.update` 542 */ 543 UpdateOperation!T update(T : Model)() return pure 544 { 545 return UpdateOperation!T(db, txHandle); 546 } 547 548 /** 549 * Returns a builder struct that can be used to perform a DELETE statement 550 * in the SQL database on the provided Model table. 551 * 552 * See_Also: `DormDB.remove` 553 */ 554 RemoveOperation!T remove(T : Model)() return pure 555 { 556 return RemoveOperation!T(db, txHandle); 557 } 558 559 /** 560 * Deletes the given model instance from the database inside the transaction. 561 * 562 * Equivalent to calling `tx.remove!T.single(instance)`. 563 * 564 * See_Also: `RemoveOperation.single` 565 * 566 * Returns: true if anything was deleted, false otherwise. 567 */ 568 bool remove(T : Model)(T instance) return 569 { 570 return remove!T.single(instance); 571 } 572 573 /// ditto 574 bool remove(TPatch)(TPatch instance) return 575 if (!is(TPatch : Model) && isSomePatch!TPatch) 576 { 577 alias T = DBType!TPatch; 578 return remove!T.single(instance); 579 } 580 } 581 582 private string makePatchAccessPrefix(Patch, DB)() 583 { 584 string ret; 585 static if (!is(Patch == DB) 586 && is(__traits(parent, Patch) == DB)) 587 { 588 static foreach (i, field; DB.tupleof) 589 { 590 static if (is(typeof(field) == Patch)) 591 { 592 static foreach_reverse (j, field; DB.tupleof) 593 static if (is(typeof(field) == Patch)) 594 static assert(i == j, "Multiple implicit " 595 ~ Patch.stringof ~ " patches on the same " 596 ~ DB.stringof ~ " Model class!"); 597 598 ret = DB.tupleof[i].stringof ~ "."; 599 } 600 } 601 } 602 return ret; 603 } 604 605 private void insertImpl(bool single, T)( 606 scope ffi.DBHandle handle, 607 scope T[] value, 608 ffi.DBTransactionHandle transaction) 609 @safe 610 { 611 import core.lifetime; 612 alias DB = DBType!T; 613 614 enum patchAccessPrefix = makePatchAccessPrefix!(T, DB); 615 616 static stripPrefix(string s) 617 { 618 return patchAccessPrefix.length && s.length > patchAccessPrefix.length 619 && s[0 .. patchAccessPrefix.length] == patchAccessPrefix 620 ? s[patchAccessPrefix.length .. $] : s; 621 } 622 623 enum NumColumns = { 624 int used; 625 static foreach (field; DormFields!DB) 626 static if (is(typeof(mixin("value[0]." ~ stripPrefix(field.sourceColumn)))) || field.hasConstructValue) 627 used++; 628 return used; 629 }(); 630 631 ffi.FFIString[NumColumns] columns; 632 static if (single) 633 { 634 ffi.FFIValue[NumColumns][1] values; 635 } 636 else 637 { 638 ffi.FFIValue[NumColumns][] values; 639 values.length = value.length; 640 641 if (!values.length) 642 return; 643 } 644 645 int used; 646 647 static if (!is(T == DB)) 648 { 649 auto validatorObject = new DB(); 650 static if (!single) 651 { 652 DB validatorCopy; 653 if (values.length > 1) 654 (() @trusted => copyEmplace(validatorObject, validatorCopy))(); 655 } 656 } 657 658 static foreach (field; DormFields!DB) 659 {{ 660 static if (is(typeof(mixin("value[0]." ~ stripPrefix(field.sourceColumn))))) 661 { 662 columns[used] = ffi.ffi(field.columnName); 663 foreach (i; 0 .. values.length) 664 values[i][used] = conditionValue!field(mixin("value[i]." ~ stripPrefix(field.sourceColumn))); 665 used++; 666 } 667 else static if (field.hasConstructValue) 668 { 669 // filled in by constructor 670 columns[used] = ffi.ffi(field.columnName); 671 foreach (i; 0 .. values.length) 672 { 673 static if (is(T == DB)) 674 values[i][used] = conditionValue!field(mixin("value[i]." ~ field.sourceColumn)); 675 else 676 values[i][used] = conditionValue!field(mixin("validatorObject." ~ stripPrefix(field.sourceColumn))); 677 } 678 used++; 679 } 680 else static if (field.hasGeneratedDefaultValue) 681 { 682 // OK 683 } 684 else static if (!is(T == DB)) 685 static assert(false, "Trying to insert a patch " ~ T.stringof 686 ~ " into " ~ DB.stringof ~ ", but it is missing the required field " 687 ~ stripPrefix(field.sourceReferenceName) ~ "! " 688 ~ "Fields with auto-generated values may be omitted in patch types. " 689 ~ ModelFormat.Field.humanReadableGeneratedDefaultValueTypes); 690 else 691 static assert(false, "wat? (defined DormField not found inside the Model class that defined it)"); 692 }} 693 694 assert(used == NumColumns); 695 696 static if (is(T == DB)) 697 { 698 foreach (i; 0 .. values.length) 699 { 700 auto brokenFields = value[i].runValidators(); 701 702 string error; 703 foreach (field; brokenFields) 704 { 705 static if (single) 706 error ~= "Field `" ~ field.sourceColumn ~ "` defined in " 707 ~ field.definedAt.toString ~ " failed user validation."; 708 else 709 error ~= "row[" ~ i.to!string 710 ~ "] field `" ~ field.sourceColumn ~ "` defined in " 711 ~ field.definedAt.toString ~ " failed user validation."; 712 } 713 if (error.length) 714 throw new Exception(error); 715 } 716 } 717 else 718 { 719 foreach (i; 0 .. values.length) 720 { 721 static if (!single) 722 if (i != 0) 723 (() @trusted => copyEmplace(validatorCopy, validatorObject))(); 724 725 validatorObject.applyPatch(value[i]); 726 auto brokenFields = validatorObject.runValidators(); 727 728 string error; 729 foreach (field; brokenFields) 730 { 731 switch (field.columnName) 732 { 733 static foreach (sourceField; DormFields!DB) 734 { 735 static if (is(typeof(mixin("value[i]." ~ stripPrefix(sourceField.sourceColumn))))) 736 { 737 case sourceField.columnName: 738 } 739 } 740 static if (single) 741 error ~= "Field `" ~ field.sourceColumn ~ "` defined in " 742 ~ field.definedAt.toString ~ " failed user validation."; 743 else 744 error ~= "row[" ~ i.to!string 745 ~ "] field `" ~ field.sourceColumn ~ "` defined in " 746 ~ field.definedAt.toString ~ " failed user validation."; 747 break; 748 default: 749 break; 750 } 751 } 752 753 if (error.length) 754 throw new Exception(error); 755 } 756 } 757 758 759 (() @trusted { 760 auto ctx = FreeableAsyncResult!void.make; 761 static if (single) 762 { 763 ffi.rorm_db_insert(handle, 764 transaction, 765 ffi.ffi(DormLayout!DB.tableName), 766 ffi.ffi(columns), 767 ffi.ffi(values[0]), ctx.callback.expand); 768 } 769 else 770 { 771 auto rows = new ffi.FFIArray!(ffi.FFIValue)[values.length]; 772 foreach (i; 0 .. values.length) 773 rows[i] = ffi.ffi(values[i]); 774 775 ffi.rorm_db_insert_bulk(handle, 776 transaction, 777 ffi.ffi(DormLayout!DB.tableName), 778 ffi.ffi(columns), 779 ffi.ffi(rows), ctx.callback.expand); 780 } 781 ctx.result(); 782 })(); 783 } 784 785 private struct ConditionBuilderData 786 { 787 @disable this(this); 788 789 JoinInformation joinInformation; 790 } 791 792 /// This is the type of the variable that is passed into the condition callback 793 /// at runtime on the `SelectOperation` struct. It automatically mirrors all 794 /// DORM fields that are defined on the passed-in `T` Model class. 795 /// 796 /// Fields can be accessed with the same name they were defined in the Model 797 /// class. Embedded structs will only use the deepest variable name, e.g. a 798 /// nested field of name `userCommon.username` will only need to be accessed 799 /// using `username`. Duplicate / shadowing members is not implemented and will 800 /// be unable to use the condition builder on them. 801 /// 802 /// If any boolean types are defined in the model, they can be quickly checked 803 /// to be false using the `not.booleanFieldName` helper. 804 /// See `NotConditionBuilder` for this. 805 /// 806 /// When mistyping names, an expressive error message is printed as compile 807 /// time output, showing all possible members for convenience. 808 struct ConditionBuilder(T) 809 { 810 private ConditionBuilderData* builderData; 811 812 static foreach (field; DormFields!T) 813 { 814 static if (field.isForeignKey) 815 { 816 mixin("ForeignModelConditionBuilderField!(typeof(T.", field.sourceColumn, "), field) ", 817 field.sourceColumn.lastIdentifier, 818 "() @property return { return typeof(return)(DormLayout!T.tableName, builderData); }"); 819 } 820 else 821 { 822 mixin("ConditionBuilderField!(typeof(T.", field.sourceColumn, "), field) ", 823 field.sourceColumn.lastIdentifier, 824 " = ConditionBuilderField!(typeof(T.", field.sourceColumn, "), field)(`", 825 DormLayout!T.tableName, "`, `", field.columnName, 826 "`);"); 827 } 828 } 829 830 static if (__traits(allMembers, NotConditionBuilder!T).length > 1) 831 { 832 /// Helper to quickly create `field == false` conditions for boolean fields. 833 NotConditionBuilder!T not; 834 } 835 else 836 { 837 /// Helper to quickly create `field == false` conditions for boolean fields. 838 void not()() { static assert(false, "Model " ~ T.stringof 839 ~ " has no fields that can be used with .not"); } 840 } 841 842 mixin DynamicMissingMemberErrorHelper!"condition field"; 843 } 844 845 /// This is the type of the variable that is passed into the `orderBy` callback 846 /// at runtime on the `SelectOperation` struct. It automatically mirrors all 847 /// DORM fields that are defined on the passed-in `T` Model class. 848 /// 849 /// Fields can be accessed with the same name they were defined in the Model 850 /// class. Embedded structs will only use the deepest variable name, e.g. a 851 /// nested field of name `userCommon.username` will only need to be accessed 852 /// using `username`. Duplicate / shadowing members is not implemented and will 853 /// be unable to use the builder on them. 854 /// 855 /// On the columns you can either use `.asc` to sort ascending or `.desc` to 856 /// sort descending by the column. 857 /// 858 /// When mistyping names, an expressive error message is printed as compile 859 /// time output, showing all possible members for convenience. 860 struct OrderBuilder(T) 861 { 862 private ConditionBuilderData* builderData; 863 864 static foreach (field; DormFields!T) 865 { 866 static if (field.isForeignKey) 867 { 868 mixin("ForeignModelOrderBuilderField!(typeof(T.", field.sourceColumn, "), field) ", 869 field.sourceColumn.lastIdentifier, 870 "() @property return { return typeof(return)(DormLayout!T.tableName, builderData); }"); 871 } 872 else 873 { 874 mixin("OrderBuilderField!(typeof(T.", field.sourceColumn, "), field) ", 875 field.sourceColumn.lastIdentifier, 876 " = OrderBuilderField!(typeof(T.", field.sourceColumn, "), field)(`", 877 DormLayout!T.tableName, "`, `", field.columnName, 878 "`);"); 879 } 880 } 881 882 /// Only useful at runtime: when it's decided that no ordering needs to be 883 /// done after all, simply return this method to do nothing. 884 ffi.FFIOrderByEntry none() const @safe @property 885 { 886 return ffi.FFIOrderByEntry.init; 887 } 888 889 mixin DynamicMissingMemberErrorHelper!"order field"; 890 } 891 892 /// This is the type of the variable that is passed into the `populate` callback 893 /// at runtime on the `SelectOperation` struct. It automatically mirrors all 894 /// DORM fields that are defined on the passed-in `T` Model class. 895 /// 896 /// Fields can be accessed with the same name they were defined in the Model 897 /// class. Embedded structs will only use the deepest variable name, e.g. a 898 /// nested field of name `userCommon.username` will only need to be accessed 899 /// using `username`. Duplicate / shadowing members is not implemented and will 900 /// be unable to use the builder on them. 901 /// 902 /// On the column you currently just need to write `.yes` after the column to 903 /// actually include it. This is a limitation because otherwise it wouldn't be 904 /// possible to populate both reference columns directly or references of 905 /// references. e.g. populating both `model.author` and `model.author.friends` 906 /// can be done by doing `model.author.yes` and `model.author.friends.yes` 907 /// 908 /// When mistyping names, an expressive error message is printed as compile 909 /// time output, showing all possible members for convenience. 910 struct PopulateBuilder(T) 911 { 912 private ConditionBuilderData* builderData; 913 914 static foreach (field; DormFields!T) 915 { 916 static if (field.isForeignKey) 917 { 918 mixin("PopulateBuilderField!(typeof(T.", field.sourceColumn, "), field) ", 919 field.sourceColumn.lastIdentifier, 920 "() @property return { return typeof(return)(DormLayout!T.tableName, builderData); }"); 921 } 922 } 923 924 mixin DynamicMissingMemberErrorHelper!"populate builder"; 925 } 926 927 /// This MUST be mixed in at the end to show proper members 928 private mixin template DynamicMissingMemberErrorHelper(string fieldName, string simplifyName = "") 929 { 930 auto opDispatch(string member, string file = __FILE__, size_t line = __LINE__)() 931 { 932 import std.string : join; 933 934 enum available = [__traits(allMembers, typeof(this))][0 .. $ - 1].filterBuiltins; 935 936 enum suggestion = findSuggestion(available, member); 937 enum suggestionMsg = suggestion.length ? "\n\n\t\tDid you mean " ~ suggestion ~ "?" : ""; 938 939 pragma(msg, errorBoldPrefix ~ file ~ "(" ~ line.to!string ~ "): " ~ supplErrorWithFilePrefix 940 ~ fieldName ~ " `" ~ member ~ "` does not exist on " 941 ~ (simplifyName.length ? simplifyName : typeof(this).stringof) ~ ". Available members are: " 942 ~ available.join(", ") ~ suggestionMsg); 943 static assert(false, "See DORM error above."); 944 } 945 } 946 947 private mixin template DisallowOperators(string typeName) 948 { 949 auto opBinary(string op, R, string file = __FILE__, size_t line = __LINE__)(const R rhs) 950 const @safe pure nothrow @nogc 951 { 952 pragma(msg, errorBoldPrefix ~ file ~ "(" ~ line.to!string ~ "): " ~ supplErrorWithFilePrefix 953 ~ "You are not supposed to use operators like '" ~ .op ~ "' on " 954 ~ typeName ~ "! Use the operation fields on this instead."); 955 static assert(false, "See DORM error above."); 956 } 957 958 auto opBinaryRight(string op, L, string file = __FILE__, size_t line = __LINE__)(const L lhs) 959 const @safe pure nothrow @nogc 960 { 961 pragma(msg, errorBoldPrefix ~ file ~ "(" ~ line.to!string ~ "): " ~ supplErrorWithFilePrefix 962 ~ "You are not supposed to use operators like '" ~ .op ~ "' on " 963 ~ typeName ~ "! Use the operation fields on this instead."); 964 static assert(false, "See DORM error above."); 965 } 966 967 bool opEquals(R, string file = __FILE__, size_t line = __LINE__)(const R other) 968 const @safe pure nothrow @nogc 969 if (!is(immutable R == immutable typeof(this))) 970 { 971 pragma(msg, errorBoldPrefix ~ file ~ "(" ~ line.to!string ~ "): " ~ supplErrorWithFilePrefix 972 ~ "You are not supposed to use operators like '==' on " 973 ~ typeName ~ "! Use the operation fields on this instead."); 974 static assert(false, "See DORM error above."); 975 } 976 } 977 978 private string[] filterBuiltins(string[] members) 979 { 980 import std.algorithm : among, remove; 981 982 foreach_reverse (i, member; members) 983 if (member.among("__ctor", "__dtor")) 984 members = members.remove(i); 985 return members; 986 } 987 988 private string findSuggestion(string[] available, string member) 989 { 990 // TODO: levenshteinDistance doesn't work at CTFE 991 // import std.algorithm : levenshteinDistance; 992 993 // size_t minDistance = size_t.max; 994 // string suggestion; 995 996 // foreach (a; available) 997 // { 998 // auto dist = levenshteinDistance(a, member); 999 // if (dist < minDistance) 1000 // { 1001 // suggestion = a; 1002 // minDistance = dist; 1003 // } 1004 // } 1005 // return minDistance < 3 ? suggestion : null; 1006 1007 import std.string : soundex; 1008 1009 char[4] q, test; 1010 if (!soundex(member, q[])) 1011 return null; 1012 foreach (a; available) 1013 { 1014 auto t = soundex(a, test[]); 1015 if (t == q) 1016 return a; 1017 } 1018 return null; 1019 } 1020 1021 private enum errorBoldPrefix = "\x1B[1m"; 1022 private enum supplErrorWithFilePrefix = "\x1B[1;31mDORM Error:\x1B[m "; 1023 private enum supplErrorPrefix = " " ~ supplErrorWithFilePrefix; 1024 1025 /// Helper type to quickly create `field == false` conditions for boolean fields. 1026 /// 1027 /// See `ConditionBuilder` 1028 struct NotConditionBuilder(T) 1029 { 1030 static foreach (field; DormFields!T) 1031 { 1032 static if (is(typeof(mixin("T.", field.sourceColumn)) : bool)) 1033 { 1034 mixin("Condition ", 1035 field.sourceColumn.lastIdentifier, 1036 "() @property { return Condition(UnaryCondition(UnaryConditionType.Not, 1037 makeColumnReference(`", 1038 DormLayout!T.tableName, "`, `", field.columnName, 1039 "`))); }"); 1040 } 1041 } 1042 1043 mixin DynamicMissingMemberErrorHelper!"negated condition field"; 1044 } 1045 1046 private Condition* makeColumnReference(string tableName, string columnName) @safe 1047 { 1048 // TODO: think of how we can abstract memory allocation here 1049 return new Condition(columnValue(tableName, columnName)); 1050 } 1051 1052 private Condition* makeConditionConstant(ModelFormat.Field fieldInfo, T)(T value) @safe 1053 { 1054 // TODO: think of how we can abstract memory allocation here 1055 return new Condition(conditionValue!fieldInfo(value)); 1056 } 1057 1058 private mixin template ForeignJoinHelper() 1059 { 1060 private string srcTableName; 1061 private ConditionBuilderData* builderData; 1062 1063 /// Constructs this ForeignModelConditionBuilderField, operating on the given data pointer during its lifetime 1064 this(string srcTableName, ConditionBuilderData* builderData) @safe 1065 { 1066 this.srcTableName = srcTableName; 1067 this.builderData = builderData; 1068 } 1069 1070 private string ensureJoined() @safe 1071 { 1072 return builderData.joinInformation.joinSuppl[ensureJoinedIdx].placeholder; 1073 } 1074 1075 private size_t ensureJoinedIdx() @trusted 1076 { 1077 auto ji = &builderData.joinInformation; 1078 string fkName = field.columnName; 1079 auto exist = fkName in ji.joinedTables; 1080 if (exist) 1081 { 1082 return *exist; 1083 } 1084 else 1085 { 1086 size_t index = ji.joins.length; 1087 assert(ji.joinSuppl.length == index); 1088 string placeholder = JoinInformation.joinAliasList[ji.joinedTables.length]; 1089 ffi.FFICondition* condition = new ffi.FFICondition(); 1090 condition.type = ffi.FFICondition.Type.BinaryCondition; 1091 condition.binaryCondition.type = ffi.FFIBinaryCondition.Type.Equals; 1092 auto lhs = new ffi.FFICondition(); 1093 auto rhs = new ffi.FFICondition(); 1094 lhs.type = ffi.FFICondition.Type.Value; 1095 lhs.value = columnValue(placeholder, ModelRef.primaryKeyField.columnName); 1096 rhs.type = ffi.FFICondition.Type.Value; 1097 rhs.value = columnValue(srcTableName, field.columnName); 1098 condition.binaryCondition.lhs = lhs; 1099 condition.binaryCondition.rhs = rhs; 1100 1101 assert(ji.joins.length == index, 1102 "this method must absolutely never be called in parallel on the same object"); 1103 ji.joinedTables[fkName] = index; 1104 ji.joins ~= ffi.FFIJoin( 1105 ffi.FFIJoinType.join, 1106 ffi.ffi(DormLayout!RefDB.tableName), 1107 ffi.ffi(placeholder), 1108 condition 1109 ); 1110 ji.joinSuppl ~= JoinInformation.JoinSuppl( 1111 placeholder, 1112 false 1113 ); 1114 return index; 1115 } 1116 } 1117 } 1118 1119 /// Helper type to access sub-fields through `ModelRef` foreign key fields. Will 1120 /// join the foreign model table automatically if using any fields on there, 1121 /// other than the primary key, which can be read directly from the source. 1122 /// 1123 /// Just like `ConditionBuilder` this automatically mirrors all DORM fields of 1124 /// the _foreign_ table, i.e. the referenced model type. 1125 /// 1126 /// This type is returned by the `ConditionBuilder`. It does not define any 1127 /// members itself, it only defines all members of the referenced Model to be 1128 /// accessible. When operating on the primary key that is referenced to from the 1129 /// ModelRef foreign key, no join operation will be enforced, as the data is 1130 /// stored entirely in the table with the foreign key. 1131 struct ForeignModelConditionBuilderField(ModelRef, ModelFormat.Field field) 1132 { 1133 alias RefDB = ModelRef.TModel; 1134 1135 mixin ForeignJoinHelper; 1136 1137 static foreach (field; DormFields!RefDB) 1138 { 1139 static if (__traits(isSame, ModelRef.primaryKeyAlias, mixin("RefDB.", field.sourceColumn))) 1140 { 1141 mixin("ConditionBuilderField!(ModelRef.PrimaryKeyType, field) ", 1142 field.sourceColumn.lastIdentifier, 1143 "() @property @safe return { return ConditionBuilderField!(ModelRef.PrimaryKeyType, field)(srcTableName, `", 1144 field.columnName, "`); }"); 1145 } 1146 else static if (field.isForeignKey) 1147 { 1148 mixin("ForeignModelConditionBuilderField!(typeof(RefDB.", field.sourceColumn, "), field) ", 1149 field.sourceColumn.lastIdentifier, 1150 "() @property return { string placeholder = ensureJoined(); return typeof(return)(placeholder, builderData); }"); 1151 } 1152 else 1153 { 1154 mixin("ConditionBuilderField!(typeof(RefDB.", field.sourceColumn, "), field) ", 1155 field.sourceColumn.lastIdentifier, 1156 "() @property @safe return { string placeholder = ensureJoined(); return typeof(return)(placeholder, `", 1157 field.columnName, 1158 "`); }"); 1159 } 1160 } 1161 1162 mixin DynamicMissingMemberErrorHelper!( 1163 "foreign condition field", 1164 "`ForeignModelConditionBuilderField` on " ~ RefDB.stringof ~ "." ~ field.sourceColumn 1165 ); 1166 } 1167 1168 /// Helper type to access sub-fields through `ModelRef` foreign key fields. Will 1169 /// join the foreign model table automatically if using any fields on there, 1170 /// other than the primary key, which can be read directly from the source. 1171 /// 1172 /// Just like `OrderBuilder` this automatically mirrors all DORM fields of 1173 /// the _foreign_ table, i.e. the referenced model type. 1174 /// 1175 /// This type is returned by the `OrderBuilder`. It does not define any members 1176 /// itself, it only defines all members of the referenced Model to be 1177 /// accessible. When operating on the primary key that is referenced to from the 1178 /// ModelRef foreign key, no join operation will be enforced, as the data is 1179 /// stored entirely in the table with the foreign key. 1180 struct ForeignModelOrderBuilderField(ModelRef, ModelFormat.Field field) 1181 { 1182 alias RefDB = ModelRef.TModel; 1183 1184 mixin ForeignJoinHelper; 1185 1186 static foreach (field; DormFields!RefDB) 1187 { 1188 static if (__traits(isSame, ModelRef.primaryKeyAlias, mixin("RefDB.", field.sourceColumn))) 1189 { 1190 mixin("OrderBuilderField!(ModelRef.PrimaryKeyType, field) ", 1191 field.sourceColumn.lastIdentifier, 1192 "() @property @safe return { return OrderBuilderField!(ModelRef.PrimaryKeyType, field)(srcTableName, `", 1193 field.columnName, "`); }"); 1194 } 1195 else static if (field.isForeignKey) 1196 { 1197 mixin("ForeignModelOrderBuilderField!(typeof(RefDB.", field.sourceColumn, "), field) ", 1198 field.sourceColumn.lastIdentifier, 1199 "() @property return { string placeholder = ensureJoined(); return typeof(return)(placeholder, builderData); }"); 1200 } 1201 else 1202 { 1203 mixin("OrderBuilderField!(typeof(RefDB.", field.sourceColumn, "), field) ", 1204 field.sourceColumn.lastIdentifier, 1205 "() @property @safe return { string placeholder = ensureJoined(); return typeof(return)(placeholder, `", 1206 field.columnName, 1207 "`); }"); 1208 } 1209 } 1210 1211 mixin DynamicMissingMemberErrorHelper!( 1212 "foreign condition field", 1213 "`ForeignModelOrderBuilderField` on " ~ RefDB.stringof ~ "." ~ field.sourceColumn 1214 ); 1215 } 1216 1217 /// Internal structure returned by the `PopulateBuilder`, which is passed to 1218 /// user code from the `populate` method on a `SelectOperation`. Internally this 1219 /// works by setting the `include` flag on the internal join info structure that 1220 /// either already exists because of previous condition or ordering operations 1221 /// or generates the join info structure on-demand. 1222 /// 1223 /// Do not create this struct manually, only use the `PopulateBuilderField` that 1224 /// is passed to you as parameter through the `populate` function on the 1225 /// `SelectOperation` struct, which is returned by `db.select` or `tx.select`. 1226 struct PopulateRef 1227 { 1228 /// Internal index inside the JoinInfo array that is stored on the 1229 /// `SelectBuilder`. Do not modify manually, you should only use the 1230 /// `populate` function on `SelectOperation` to generate this. 1231 size_t idx; 1232 } 1233 1234 /// Helper struct 1235 struct PopulateBuilderField(ModelRef, ModelFormat.Field field) 1236 { 1237 alias RefDB = ModelRef.TModel; 1238 1239 mixin ForeignJoinHelper; 1240 1241 /// Explicitly say this field is used 1242 PopulateRef[] yes() 1243 { 1244 return [PopulateRef(ensureJoinedIdx)]; 1245 } 1246 1247 static foreach (field; DormFields!RefDB) 1248 { 1249 static if (field.isForeignKey) 1250 { 1251 mixin("PopulateBuilderField!(typeof(RefDB.", field.sourceColumn, "), field) ", 1252 field.sourceColumn.lastIdentifier, 1253 "() @property return { string placeholder = ensureJoined(); return typeof(return)(placeholder, builderData); }"); 1254 } 1255 } 1256 1257 mixin DisallowOperators!( 1258 "`PopulateBuilderField` on " ~ RefDB.stringof ~ "." ~ field.sourceColumn 1259 ); 1260 1261 mixin DynamicMissingMemberErrorHelper!( 1262 "populate field", 1263 "`PopulateBuilderField` on " ~ RefDB.stringof ~ "." ~ field.sourceColumn 1264 ); 1265 } 1266 1267 /// Returns `"baz"` from `"foo.bar.baz"` (identifier after last .) 1268 /// Returns `s` as-is if it doesn't contain any dots. 1269 private string lastIdentifier(string s) 1270 { 1271 foreach_reverse (i, c; s) 1272 if (c == '.') 1273 return s[i + 1 .. $]; 1274 return s; 1275 } 1276 1277 /// Type that actually implements the condition building on a 1278 /// `ConditionBuilder`. 1279 /// 1280 /// Implements building simple unary, binary and ternary operators: 1281 /// - `equals` 1282 /// - `notEquals` 1283 /// - `isTrue` (only defined on boolean types) 1284 /// - `lessThan` 1285 /// - `lessThanOrEqual` 1286 /// - `greaterThan` 1287 /// - `greaterThanOrEqual` 1288 /// - `like` 1289 /// - `notLike` 1290 /// - `regexp` 1291 /// - `notRegexp` 1292 /// - `in_` 1293 /// - `notIn` 1294 /// - `isNull` 1295 /// - `isNotNull` 1296 /// - `exists` 1297 /// - `notExists` 1298 /// - `between` 1299 /// - `notBetween` 1300 struct ConditionBuilderField(T, ModelFormat.Field field) 1301 { 1302 // TODO: all the type specific field to Condition thingies 1303 1304 private string tableName; 1305 private string columnName; 1306 1307 /// Constructs this ConditionBuilderField with the given columnName for generated conditions. 1308 this(string tableName, string columnName) @safe 1309 { 1310 this.tableName = tableName; 1311 this.columnName = columnName; 1312 } 1313 1314 private Condition* lhs() @safe 1315 { 1316 return makeColumnReference(tableName, columnName); 1317 } 1318 1319 /// Returns: SQL condition `field == value` 1320 Condition equals(V)(V value) @safe 1321 { 1322 return Condition(BinaryCondition(BinaryConditionType.Equals, lhs, makeConditionConstant!field(value))); 1323 } 1324 1325 /// Returns: SQL condition `field != value` 1326 Condition notEquals(V)(V value) @safe 1327 { 1328 return Condition(BinaryCondition(BinaryConditionType.NotEquals, lhs, makeConditionConstant!field(value))); 1329 } 1330 1331 static if (field.type == ModelFormat.Field.DBType.boolean) 1332 { 1333 /// Returns: SQL condition `field == true` 1334 Condition isTrue() @safe 1335 { 1336 return equals(true); 1337 } 1338 } 1339 1340 /// Returns: SQL condition `field < value` 1341 Condition lessThan(V)(V value) @safe 1342 { 1343 return Condition(BinaryCondition(BinaryConditionType.Less, lhs, makeConditionConstant!field(value))); 1344 } 1345 1346 /// Returns: SQL condition `field <= value` 1347 Condition lessThanOrEqual(V)(V value) @safe 1348 { 1349 return Condition(BinaryCondition(BinaryConditionType.LessOrEquals, lhs, makeConditionConstant!field(value))); 1350 } 1351 1352 /// Returns: SQL condition `field > value` 1353 Condition greaterThan(V)(V value) @safe 1354 { 1355 return Condition(BinaryCondition(BinaryConditionType.Greater, lhs, makeConditionConstant!field(value))); 1356 } 1357 1358 /// Returns: SQL condition `field >= value` 1359 Condition greaterThanOrEqual(V)(V value) @safe 1360 { 1361 return Condition(BinaryCondition(BinaryConditionType.GreaterOrEquals, lhs, makeConditionConstant!field(value))); 1362 } 1363 1364 /// Returns: SQL condition `field LIKE value` 1365 Condition like(V)(V value) @safe 1366 { 1367 return Condition(BinaryCondition(BinaryConditionType.Like, lhs, makeConditionConstant!field(value))); 1368 } 1369 1370 /// Returns: SQL condition `field NOT LIKE value` 1371 Condition notLike(V)(V value) @safe 1372 { 1373 return Condition(BinaryCondition(BinaryConditionType.NotLike, lhs, makeConditionConstant!field(value))); 1374 } 1375 1376 /// Returns: SQL condition `field REGEXP value` 1377 Condition regexp(V)(V value) @safe 1378 { 1379 return Condition(BinaryCondition(BinaryConditionType.Regexp, lhs, makeConditionConstant!field(value))); 1380 } 1381 1382 /// Returns: SQL condition `field NOT REGEXP value` 1383 Condition notRegexp(V)(V value) @safe 1384 { 1385 return Condition(BinaryCondition(BinaryConditionType.NotRegexp, lhs, makeConditionConstant!field(value))); 1386 } 1387 1388 /// Returns: SQL condition `field IN value` 1389 Condition in_(V)(V value) @safe 1390 { 1391 return Condition(BinaryCondition(BinaryConditionType.In, lhs, makeConditionConstant!field(value))); 1392 } 1393 1394 /// Returns: SQL condition `field NOT IN value` 1395 Condition notIn(V)(V value) @safe 1396 { 1397 return Condition(BinaryCondition(BinaryConditionType.NotIn, lhs, makeConditionConstant!field(value))); 1398 } 1399 1400 /// Returns: SQL condition `field IS NULL` 1401 Condition isNull() @safe 1402 { 1403 return Condition(UnaryCondition(UnaryConditionType.IsNull, lhs)); 1404 } 1405 1406 alias equalsNull = isNull; 1407 1408 /// Returns: SQL condition `field IS NOT NULL` 1409 Condition isNotNull() @safe 1410 { 1411 return Condition(UnaryCondition(UnaryConditionType.IsNotNull, lhs)); 1412 } 1413 1414 alias notEqualsNull = isNotNull; 1415 1416 /// Returns: SQL condition `field EXISTS` 1417 Condition exists() @safe 1418 { 1419 return Condition(UnaryCondition(UnaryConditionType.Exists, lhs)); 1420 } 1421 1422 /// Returns: SQL condition `field NOT EXISTS` 1423 Condition notExists() @safe 1424 { 1425 return Condition(UnaryCondition(UnaryConditionType.NotExists, lhs)); 1426 } 1427 1428 /// Returns: SQL condition `field BETWEEN min AND max` 1429 Condition between(L, R)(L min, R max) @safe 1430 { 1431 return Condition(TernaryCondition( 1432 TernaryConditionType.Between, 1433 lhs, 1434 makeConditionConstant!field(min), 1435 makeConditionConstant!field(max) 1436 )); 1437 } 1438 1439 /// Returns: SQL condition `field NOT BETWEEN min AND max` 1440 Condition notBetween(L, R)(L min, R max) @safe 1441 { 1442 return Condition(TernaryCondition( 1443 TernaryConditionType.NotBetween, 1444 lhs, 1445 makeConditionConstant!field(min), 1446 makeConditionConstant!field(max) 1447 )); 1448 } 1449 1450 mixin DisallowOperators!( 1451 "`ConditionBuilderField!(" ~ T.stringof ~ ")` on " ~ field.sourceColumn 1452 ); 1453 1454 mixin DynamicMissingMemberErrorHelper!( 1455 "field comparision operator", 1456 "`ConditionBuilderField!(" ~ T.stringof ~ ")` on " ~ field.sourceColumn 1457 ); 1458 } 1459 1460 /// Type that actually implements the asc/desc methods inside the orderBy 1461 /// callback. (`OrderBuilder`) Defaults to ascending. 1462 struct OrderBuilderField(T, ModelFormat.Field field) 1463 { 1464 private string tableName; 1465 private string columnName; 1466 1467 /// Constructs this OrderBuilderField with the given columnName for generated orders. 1468 this(string tableName, string columnName) @safe 1469 { 1470 this.tableName = tableName; 1471 this.columnName = columnName; 1472 } 1473 1474 /// Ascending ordering. 1475 ffi.FFIOrderByEntry asc() @safe 1476 { 1477 return ffi.FFIOrderByEntry(ffi.FFIOrdering.asc, ffi.ffi(tableName), ffi.ffi(columnName)); 1478 } 1479 1480 /// Descending ordering. 1481 ffi.FFIOrderByEntry desc() @safe 1482 { 1483 return ffi.FFIOrderByEntry(ffi.FFIOrdering.desc, ffi.ffi(tableName), ffi.ffi(columnName)); 1484 } 1485 1486 mixin DisallowOperators!( 1487 "`OrderBuilderField!(" ~ T.stringof ~ ")` on " ~ field.sourceColumn 1488 ); 1489 1490 mixin DynamicMissingMemberErrorHelper!( 1491 "field ordering", 1492 "`OrderBuilderField!(" ~ T.stringof ~ ")` on " ~ field.sourceColumn 1493 ); 1494 } 1495 1496 private struct JoinInformation 1497 { 1498 private static immutable joinAliasList = { 1499 // list of _0, _1, _2, _3, ... embedded into the executable 1500 string[] aliasList; 1501 foreach (i; 0 .. maxJoins) 1502 aliasList ~= ("_" ~ i.to!string); 1503 return aliasList; 1504 }(); 1505 1506 static struct JoinSuppl 1507 { 1508 string placeholder; 1509 bool include; 1510 } 1511 1512 private ffi.FFIJoin[] joins; 1513 /// Supplemental information for joins, same length and order as in joins. 1514 private JoinSuppl[] joinSuppl; 1515 /// Lookup foreign key name -> array index 1516 private size_t[string] joinedTables; 1517 } 1518 1519 /** 1520 * This is the builder struct that's used for update operations. 1521 * 1522 * Don't construct this struct manually, use the db.update or tx.update method 1523 * to create this struct. 1524 * 1525 * Methods you can call on this builder to manipulate the result: 1526 * - `condition` to limit which rows to update. (can only be called once) 1527 * - `set!("sourceColumnName")(value)` to update a single column to the given 1528 * value 1529 * - `set(patchValue)`, where patchValue is a patch for this UpdateOperation, to 1530 * set multiple fields at once. 1531 * 1532 * Finishing methods you can call on this builder: 1533 * - `await` to send the prepared update operation 1534 */ 1535 struct UpdateOperation( 1536 T : Model, 1537 bool hasWhere = false, 1538 ) 1539 { 1540 @safe: 1541 private const(DormDB)* db; 1542 private ffi.DBTransactionHandle tx; 1543 private ffi.FFICondition[] conditionTree; 1544 private JoinInformation joinInformation; 1545 private ffi.FFIUpdate[] updates; 1546 1547 // TODO: might be copyable 1548 @disable this(this); 1549 1550 static if (!hasWhere) 1551 { 1552 /// Argument to `condition`. Callback that takes in a 1553 /// `ConditionBuilder!T` and returns a `Condition` that can easily be 1554 /// created using that builder. 1555 alias ConditionBuilderCallback = Condition delegate(ConditionBuilder!T); 1556 1557 /// Limits the update to only rows matching this condition. Maps to the 1558 /// `WHERE` clause in an SQL statement. 1559 /// 1560 /// This method may only be called once on each query. 1561 /// 1562 /// See `ConditionBuilder` to see how the callback-based overload is 1563 /// implemented. Basically the argument that is passed to the callback 1564 /// is a virtual type that mirrors all the DB-related types from the 1565 /// Model class, on which operations such as `.equals` or `.like` can 1566 /// be called to generate conditions. 1567 /// 1568 /// Use the `Condition.and(...)`, `Condition.or(...)` or `Condition.not(...)` 1569 /// methods to combine conditions into more complex ones. You can also 1570 /// choose to not use the builder object at all and integrate manually 1571 /// constructed 1572 UpdateOperation!(T, true) condition( 1573 ConditionBuilderCallback callback 1574 ) return @trusted 1575 { 1576 scope ConditionBuilderData data; 1577 scope ConditionBuilder!T builder; 1578 builder.builderData = &data; 1579 data.joinInformation = move(joinInformation); 1580 conditionTree = callback(builder).makeTree; 1581 joinInformation = move(data.joinInformation); 1582 return cast(typeof(return))move(this); 1583 } 1584 } 1585 1586 /// Method to set one field or multiple via a patch. Update will be 1587 /// performed when `await` is called. 1588 template set(FieldOrPatch...) 1589 { 1590 static if (FieldOrPatch.length == 0) 1591 { 1592 typeof(this) set(P)(P patch) return 1593 { 1594 setPatch(patch); 1595 return move(this); 1596 } 1597 } 1598 else 1599 { 1600 static assert(FieldOrPatch.length == 1, 1601 "Allowed template types on `update.set!(...)` are:\n" 1602 ~ "\t- `set!(\"fieldName\")(value)`\n" 1603 ~ "\t- `set(SomePatch(...))`"); 1604 1605 static if (is(FieldOrPatch[0] == struct)) 1606 { 1607 typeof(this) set(FieldOrPatch[0] patch) return 1608 { 1609 setPatch(patch); 1610 return move(this); 1611 } 1612 } 1613 else 1614 { 1615 static assert(hasDormField!(T, FieldOrPatch[0]), 1616 "Called update.set with field `" ~ FieldOrPatch[0] 1617 ~ "`, but it doesn't exist on Model `" 1618 ~ T.stringof ~ "`\n\tAvailable fields:" ~ DormListFieldsForError!T); 1619 1620 typeof(this) set(typeof(mixin("T.", FieldOrPatch[0])) value) return 1621 { 1622 enum field = DormField!(T, FieldOrPatch[0]); 1623 static immutable columnName = field.columnName; 1624 updates ~= ffi.FFIUpdate( 1625 ffi.ffi(columnName), 1626 conditionValue!field(value) 1627 ); 1628 return move(this); 1629 } 1630 } 1631 } 1632 } 1633 1634 private void setPatch(TPatch)(TPatch patch) 1635 if (isSomePatch!TPatch) 1636 { 1637 mixin ValidatePatch!(TPatch, T); 1638 1639 import std.array; 1640 1641 enum fields = FilterLayoutFields!(T, TPatch); 1642 1643 static assert(fields.length > 0, "Could not find any fields to set in patch! " 1644 ~ "Model: " ~ T.stringof ~ ", Patch: " ~ TPatch.stringof); 1645 1646 static foreach (i, field; fields) 1647 {{ 1648 updates ~= ffi.FFIUpdate( 1649 ffi.ffi(field.columnName), 1650 conditionValue!field( 1651 mixin("patch.", field.sourceColumn) 1652 ) 1653 ); 1654 }} 1655 } 1656 1657 /** 1658 * Starts the update procedure and waits for the result. Throws in case of 1659 * an error. Returns the number of rows affected. 1660 * 1661 * Uses the state modified by previous calls to the builder methods like 1662 * `set` and `condition` on this builder object. 1663 * 1664 * Bugs: currently does not support joins because the underlying library 1665 * doesn't expose them yet. 1666 */ 1667 ulong await() 1668 { 1669 // TODO: use join information 1670 1671 return (() @trusted { 1672 auto ctx = FreeableAsyncResult!ulong.make; 1673 ffi.rorm_db_update( 1674 db.handle, 1675 tx, 1676 ffi.ffi(DormLayout!T.tableName), 1677 ffi.ffi(updates), 1678 conditionTree.length ? &conditionTree[0] : null, 1679 ctx.callback.expand 1680 ); 1681 return ctx.result; 1682 })(); 1683 } 1684 } 1685 1686 /** 1687 * This is the builder struct that's used for delete operations. 1688 * 1689 * Don't construct this struct manually, use the db.remove or tx.remove method 1690 * to create this struct. 1691 * 1692 * Finishing methods you can call on this builder: 1693 * - `byCondition` to delete all rows matching the condition. 1694 * - `single` to delete a single instance, matched by primary key. 1695 * - `all` to delete all rows in the table. 1696 */ 1697 struct RemoveOperation(T : Model) 1698 { 1699 @safe: 1700 private const(DormDB)* db; 1701 private ffi.DBTransactionHandle tx; 1702 1703 // TODO: might be copyable 1704 @disable this(this); 1705 1706 /// Argument to `condition`. Callback that takes in a 1707 /// `ConditionBuilder!T` and returns a `Condition` that can easily be 1708 /// created using that builder. 1709 alias ConditionBuilderCallback = Condition delegate(ConditionBuilder!T); 1710 1711 /** 1712 * Deletes the rows matching this condition. Maps to the `WHERE` clause in 1713 * an SQL statement. 1714 * 1715 * See `ConditionBuilder` to see how the callback-based overload is 1716 * implemented. Basically the argument that is passed to the callback 1717 * is a virtual type that mirrors all the DB-related types from the 1718 * Model class, on which operations such as `.equals` or `.like` can 1719 * be called to generate conditions. 1720 * 1721 * Use the `Condition.and(...)`, `Condition.or(...)` or `Condition.not(...)` 1722 * methods to combine conditions into more complex ones. You can also 1723 * choose to not use the builder object at all and integrate manually 1724 * constructed. 1725 * 1726 * Returns: DB-returned number of how many rows have been touched. May also 1727 * include foreign rows deleted by referential actions and other things. 1728 * 1729 * Bugs: currently does not support joins because the underlying library 1730 * doesn't expose them yet. 1731 */ 1732 ulong byCondition( 1733 ConditionBuilderCallback callback 1734 ) return @trusted 1735 { 1736 scope ConditionBuilderData data; 1737 scope ConditionBuilder!T builder; 1738 builder.builderData = &data; 1739 auto conditionTree = callback(builder).makeTree; 1740 auto joinInformation = move(data.joinInformation); 1741 1742 // TODO: use join information 1743 1744 return (() @trusted { 1745 auto ctx = FreeableAsyncResult!ulong.make; 1746 ffi.rorm_db_delete( 1747 db.handle, 1748 tx, 1749 ffi.ffi(DormLayout!T.tableName), 1750 &conditionTree[0], 1751 ctx.callback.expand 1752 ); 1753 return ctx.result; 1754 })(); 1755 } 1756 1757 /** 1758 * Deletes the passed-in value by limiting the delete operation to the 1759 * primary key of this instance. 1760 * 1761 * Returns: true if anything was deleted, false otherwise. 1762 */ 1763 bool single(T value) @safe 1764 { 1765 return singleImpl(conditionValue!(DormPrimaryKey!T)( 1766 mixin("value.", DormPrimaryKey!T.sourceColumn))); 1767 } 1768 1769 /// ditto 1770 bool single(P)(P patch) @safe 1771 if (!is(P == T) && isSomePatch!P) 1772 { 1773 mixin ValidatePatch!(P, T); 1774 1775 static assert(is(typeof(mixin("patch.", DormPrimaryKey!T.sourceColumn))), 1776 "Primary key '" ~ DormPrimaryKey!T.sourceColumn 1777 ~ "' must be included in patch type " 1778 ~ P.stringof ~ " in order to be a valid argument to remove!"); 1779 1780 return singleImpl(conditionValue!(DormPrimaryKey!T)( 1781 mixin("patch.", DormPrimaryKey!T.sourceColumn))); 1782 } 1783 1784 private bool singleImpl(ffi.FFIValue primaryKey) @trusted 1785 { 1786 ffi.FFICondition condition, lhs, rhs; 1787 condition.type = ffi.FFICondition.Type.BinaryCondition; 1788 condition.binaryCondition.type = ffi.FFIBinaryCondition.Type.Equals; 1789 condition.binaryCondition.lhs = &lhs; 1790 condition.binaryCondition.rhs = &rhs; 1791 1792 lhs.type = ffi.FFICondition.Type.Value; 1793 rhs.type = ffi.FFICondition.Type.Value; 1794 lhs.value = columnValue(DormLayout!T.tableName, DormPrimaryKey!T.columnName); 1795 rhs.value = primaryKey; 1796 1797 auto ctx = FreeableAsyncResult!ulong.make; 1798 ffi.rorm_db_delete( 1799 db.handle, 1800 tx, 1801 ffi.ffi(DormLayout!T.tableName), 1802 &condition, 1803 ctx.callback.expand 1804 ); 1805 return ctx.result != 0; 1806 } 1807 1808 /** 1809 * Deletes the passed-in values by limiting the delete operation to the 1810 * primary key of this instance. 1811 * 1812 * Returns: DB-returned number of how many rows have been touched. May also 1813 * include foreign rows deleted by referential actions and other things. 1814 */ 1815 ulong bulk(T[] values...) @trusted 1816 { 1817 ffi.FFICondition[] condition, rhs; 1818 condition.length = values.length; 1819 rhs.length = values.length; 1820 ffi.FFICondition lhs; 1821 lhs.type = ffi.FFICondition.Type.Value; 1822 lhs.value = columnValue(DormLayout!T.tableName, DormPrimaryKey!T.columnName); 1823 1824 foreach (i, value; values) 1825 { 1826 condition[i].type = ffi.FFICondition.Type.BinaryCondition; 1827 condition[i].binaryCondition.type = ffi.FFIBinaryCondition.Type.Equals; 1828 condition[i].binaryCondition.lhs = &lhs; 1829 condition[i].binaryCondition.rhs = &rhs[i]; 1830 1831 rhs[i].type = ffi.FFICondition.Type.Value; 1832 rhs[i].value = conditionValue!(DormPrimaryKey!T)( 1833 mixin("value.", DormPrimaryKey!T.sourceColumn)); 1834 } 1835 1836 ffi.FFICondition finalCondition; 1837 finalCondition.type = ffi.FFICondition.Type.Disjunction; 1838 finalCondition.disjunction = ffi.ffi(condition); 1839 1840 auto ctx = FreeableAsyncResult!ulong.make; 1841 ffi.rorm_db_delete( 1842 db.handle, 1843 tx, 1844 ffi.ffi(DormLayout!T.tableName), 1845 &finalCondition, 1846 ctx.callback.expand 1847 ); 1848 return ctx.result; 1849 } 1850 1851 /** 1852 * Deletes all entries in this model. 1853 * 1854 * Returns: DB-returned number of how many rows have been touched. May also 1855 * include foreign rows deleted by referential actions and other things. 1856 */ 1857 ulong all() @trusted 1858 { 1859 auto ctx = FreeableAsyncResult!ulong.make; 1860 ffi.rorm_db_delete( 1861 db.handle, 1862 tx, 1863 ffi.ffi(DormLayout!T.tableName), 1864 null, 1865 ctx.callback.expand 1866 ); 1867 return ctx.result; 1868 } 1869 } 1870 1871 /** 1872 * This is the builder struct that's used for select operations (queries) 1873 * 1874 * Don't construct this struct manually, use the db.select or tx.select method 1875 * (UFCS method defined globally) to create this struct. 1876 * 1877 * Methods you can call on this builder to manipulate the result: 1878 * 1879 * The following methods are implemented for restricting queries: (most can 1880 * only be called once, which is enforced through the template parameters) 1881 * - `condition` is used to set the "WHERE" clause in SQL. It can only be 1882 * called once on any query operation. 1883 * - `limit` can be used to set a maximum number of rows to return. When this 1884 * restriction is called, `findOne` and `findOptional` can no longer be used. 1885 * - `offset` can be used to offset after how many rows to start returning. 1886 * - `orderBy` can be used to order how the results are to be returned by the 1887 * database. 1888 * 1889 * The following methods are important when working with `ModelRef` / foreign 1890 * keys: 1891 * - `populate` eagerly loads data from a foreign model, (re)using a join 1892 * 1893 * Finishing methods you can call on this builder: 1894 * 1895 * The following methods can be used to extract the data: 1896 * - `stream` to asynchronously stream data. (can be used as iterator / range) 1897 * - `array` to eagerly fetch all data and do a big memory allocation to store 1898 * all the values into. 1899 * - `findOne` to find the first matching item or throw for no data. 1900 * - `findOptional` to find the first matching item or return Nullable!T.init 1901 * for no data. 1902 * 1903 * There are restrictions when `stream`/`array` as well as when 1904 * `findOne`/`findOptional` can be used: 1905 * 1906 * `stream`/`array` are usable when: 1907 * - neither `limit` and `offset` are set 1908 * - both `limit` and `offset` are set 1909 * - only `limit` is set and `offset` is not set 1910 * 1911 * `findOne`/`findOptional` are only usable when no `limit` is set. 1912 */ 1913 struct SelectOperation( 1914 T, 1915 TSelect, 1916 bool hasWhere = false, 1917 bool hasOffset = false, 1918 bool hasLimit = false, 1919 ) 1920 { 1921 @safe: 1922 private const(DormDB)* db; 1923 private ffi.DBTransactionHandle tx; 1924 private ffi.FFICondition[] conditionTree; 1925 private ffi.FFIOrderByEntry[] ordering; 1926 private JoinInformation joinInformation; 1927 private ulong _offset, _limit; 1928 1929 // TODO: might be copyable 1930 @disable this(this); 1931 1932 static if (!hasWhere) 1933 { 1934 /// Argument to `condition`. Callback that takes in a 1935 /// `ConditionBuilder!T` and returns a `Condition` that can easily be 1936 /// created using that builder. 1937 alias ConditionBuilderCallback = Condition delegate(ConditionBuilder!T); 1938 1939 /// Limits the query to only rows matching this condition. Maps to the 1940 /// `WHERE` clause in an SQL statement. 1941 /// 1942 /// This method may only be called once on each query. 1943 /// 1944 /// See `ConditionBuilder` to see how the callback-based overload is 1945 /// implemented. Basically the argument that is passed to the callback 1946 /// is a virtual type that mirrors all the DB-related types from the 1947 /// Model class, on which operations such as `.equals` or `.like` can 1948 /// be called to generate conditions. 1949 /// 1950 /// Use the `Condition.and(...)`, `Condition.or(...)` or `Condition.not(...)` 1951 /// methods to combine conditions into more complex ones. You can also 1952 /// choose to not use the builder object at all and integrate manually 1953 /// constructed 1954 SelectOperation!(T, TSelect, true, hasOffset, hasLimit) condition( 1955 ConditionBuilderCallback callback 1956 ) return @trusted 1957 { 1958 scope ConditionBuilderData data; 1959 scope ConditionBuilder!T builder; 1960 builder.builderData = &data; 1961 data.joinInformation = move(joinInformation); 1962 conditionTree = callback(builder).makeTree; 1963 joinInformation = move(data.joinInformation); 1964 return cast(typeof(return))move(this); 1965 } 1966 } 1967 1968 /// Argument to `orderBy`. Callback that takes in an `OrderBuilder!T` and 1969 /// returns the ffi ordering value that can be easily created using the 1970 /// builder. 1971 alias OrderBuilderCallback = ffi.FFIOrderByEntry delegate(OrderBuilder!T); 1972 1973 /// Allows ordering by the specified field with the specified direction. 1974 /// (defaults to ascending) 1975 /// 1976 /// Returning `u => u.none` means no ordering will be added. (Useful only 1977 /// at runtime) 1978 /// 1979 /// Multiple `orderBy` can be added to the same query object. Ordering is 1980 /// important - the first order orders all the rows, the second order only 1981 /// orders each group of rows where the previous order had the same values, 1982 /// etc. 1983 typeof(this) orderBy(OrderBuilderCallback callback) return @trusted 1984 { 1985 scope ConditionBuilderData data; 1986 scope OrderBuilder!T builder; 1987 builder.builderData = &data; 1988 data.joinInformation = move(joinInformation); 1989 auto order = callback(builder); 1990 if (order !is typeof(order).init) 1991 ordering ~= order; 1992 joinInformation = move(data.joinInformation); 1993 return move(this); 1994 } 1995 1996 /// Argument to `populate`. Callback that takes in an `OrderBuilder!T` and 1997 /// returns the ffi ordering value that can be easily created using the 1998 /// builder. 1999 alias PopulateBuilderCallback = PopulateRef[] delegate(PopulateBuilder!T); 2000 2001 /// Eagerly loads the data for the specified foreign key ModelRef fields 2002 /// when executing the query. 2003 /// 2004 /// Returning `u => null` means no further populate will be added. (Useful 2005 /// only at runtime) 2006 typeof(this) populate(PopulateBuilderCallback callback) return @trusted 2007 { 2008 scope ConditionBuilderData data; 2009 scope PopulateBuilder!T builder; 2010 builder.builderData = &data; 2011 data.joinInformation = move(joinInformation); 2012 foreach (populates; callback(builder)) 2013 joinInformation.joinSuppl[populates.idx].include = true; 2014 joinInformation = move(data.joinInformation); 2015 return move(this); 2016 } 2017 2018 static if (!hasOffset) 2019 { 2020 /// Sets the offset. (number of rows after which to return from the database) 2021 SelectOperation!(T, TSelect, hasWhere, true, hasLimit) offset(ulong offset) return @trusted 2022 { 2023 _offset = offset; 2024 return cast(typeof(return))move(this); 2025 } 2026 } 2027 2028 static if (!hasLimit) 2029 { 2030 /// Sets the maximum number of rows to return. Using this method 2031 /// disables the `findOne` and `findOptional` methods. 2032 SelectOperation!(T, TSelect, hasWhere, hasOffset, true) limit(ulong limit) return @trusted 2033 { 2034 _limit = limit; 2035 return cast(typeof(return))move(this); 2036 } 2037 } 2038 2039 static if (!hasOffset && !hasLimit) 2040 { 2041 /// Implementation detail, makes it possible to use `[start .. end]` on 2042 /// the select struct to set both offset and limit at the same time. 2043 /// 2044 /// Start is inclusive, end is exclusive - mimicking how array slicing 2045 /// works. 2046 ulong[2] opSlice(size_t dim)(ulong start, ulong end) 2047 { 2048 return [start, end]; 2049 } 2050 2051 /// ditto 2052 SelectOperation!(T, TSelect, hasWhere, true, true) opIndex(ulong[2] slice) return @trusted 2053 { 2054 this._offset = slice[0]; 2055 this._limit = cast(long)slice[1] - cast(long)slice[0]; 2056 return cast(typeof(return))move(this); 2057 } 2058 2059 /// ditto 2060 SelectOperation!(T, TSelect, hasWhere, true, true) range(ulong start, ulong endExclusive) return @safe 2061 { 2062 return this[start .. endExclusive]; 2063 } 2064 } 2065 2066 private ffi.FFIOption!(ffi.FFILimitClause) ffiLimit() const @property @safe 2067 { 2068 ffi.FFIOption!(ffi.FFILimitClause) ret; 2069 static if (hasLimit) 2070 { 2071 ret.state = ret.State.some; 2072 ret.raw_value.limit = _limit; 2073 static if (hasOffset) 2074 ret.raw_value.offset = ffi.FFIOption!ulong(_offset); 2075 } 2076 return ret; 2077 } 2078 2079 static if (hasLimit || !hasOffset) 2080 { 2081 /// Fetches all result data into one array. Uses the GC to allocate the 2082 /// data, so it's not needed to keep track of how long objects live by the 2083 /// user. 2084 TSelect[] array() @trusted 2085 { 2086 enum fields = FilterLayoutFields!(T, TSelect); 2087 2088 ffi.FFIColumnSelector[fields.length] columns; 2089 static foreach (i, field; fields) 2090 {{ 2091 enum aliasedName = "__" ~ field.columnName; 2092 2093 columns[i] = ffi.FFIColumnSelector( 2094 ffi.ffi(DormLayout!T.tableName), 2095 ffi.ffi(field.columnName), 2096 ffi.ffi(aliasedName) 2097 ); 2098 }} 2099 2100 mixin(makeRtColumns); 2101 2102 TSelect[] ret; 2103 auto ctx = FreeableAsyncResult!(void delegate(scope ffi.FFIArray!(ffi.DBRowHandle))).make; 2104 ctx.forward_callback = (scope rows) { 2105 ret.length = rows.size; 2106 foreach (i; 0 .. rows.size) 2107 ret[i] = unwrapRowResult!(T, TSelect)(rows.data[i], joinInformation); 2108 }; 2109 ffi.rorm_db_query_all(db.handle, 2110 tx, 2111 ffi.ffi(DormLayout!T.tableName), 2112 ffi.ffi(rtColumns), 2113 ffi.ffi(joinInformation.joins), 2114 conditionTree.length ? &conditionTree[0] : null, 2115 ffi.ffi(ordering), 2116 ffiLimit, 2117 ctx.callback.expand); 2118 ctx.result(); 2119 return ret; 2120 } 2121 2122 /// Fetches all data into a range that can be iterated over or processed 2123 /// with regular range functions. Does not allocate an array to store the 2124 /// fetched data in, but may still use sparingly the GC in implementation. 2125 auto stream() @trusted 2126 { 2127 enum fields = FilterLayoutFields!(T, TSelect); 2128 2129 ffi.FFIColumnSelector[fields.length] columns; 2130 static foreach (i, field; fields) 2131 {{ 2132 enum aliasedName = "__" ~ field.columnName; 2133 2134 columns[i] = ffi.FFIColumnSelector( 2135 ffi.ffi(DormLayout!T.tableName), 2136 ffi.ffi(field.columnName), 2137 ffi.ffi(aliasedName) 2138 ); 2139 }} 2140 2141 mixin(makeRtColumns); 2142 2143 auto stream = sync_call!(ffi.rorm_db_query_stream)(db.handle, 2144 tx, 2145 ffi.ffi(DormLayout!T.tableName), 2146 ffi.ffi(rtColumns), 2147 ffi.ffi(joinInformation.joins), 2148 conditionTree.length ? &conditionTree[0] : null, 2149 ffi.ffi(ordering), 2150 ffiLimit); 2151 2152 return RormStream!(T, TSelect)(stream, joinInformation); 2153 } 2154 } 2155 2156 static if (!hasLimit) 2157 { 2158 /// Returns the first row of the result data or throws if no data exists. 2159 TSelect findOne() @trusted 2160 { 2161 enum fields = FilterLayoutFields!(T, TSelect); 2162 2163 ffi.FFIColumnSelector[fields.length] columns; 2164 static foreach (i, field; fields) 2165 {{ 2166 enum aliasedName = "__" ~ field.columnName; 2167 2168 columns[i] = ffi.FFIColumnSelector( 2169 ffi.ffi(DormLayout!T.tableName), 2170 ffi.ffi(field.columnName), 2171 ffi.ffi(aliasedName) 2172 ); 2173 }} 2174 2175 mixin(makeRtColumns); 2176 2177 TSelect ret; 2178 auto ctx = FreeableAsyncResult!(void delegate(scope ffi.DBRowHandle)).make; 2179 ctx.forward_callback = (scope row) { 2180 ret = unwrapRowResult!(T, TSelect)(row, joinInformation); 2181 }; 2182 ffi.rorm_db_query_one(db.handle, 2183 tx, 2184 ffi.ffi(DormLayout!T.tableName), 2185 ffi.ffi(rtColumns), 2186 ffi.ffi(joinInformation.joins), 2187 conditionTree.length ? &conditionTree[0] : null, 2188 ffi.ffi(ordering), 2189 ffi.FFIOption!ulong(_offset), 2190 ctx.callback.expand); 2191 ctx.result(); 2192 return ret; 2193 } 2194 2195 /// Returns the first row of the result data or throws if no data exists. 2196 Nullable!TSelect findOptional() @trusted 2197 { 2198 enum fields = FilterLayoutFields!(T, TSelect); 2199 2200 ffi.FFIColumnSelector[fields.length] columns; 2201 static foreach (i, field; fields) 2202 {{ 2203 enum aliasedName = "__" ~ field.columnName; 2204 2205 columns[i] = ffi.FFIColumnSelector( 2206 ffi.ffi(DormLayout!T.tableName), 2207 ffi.ffi(field.columnName), 2208 ffi.ffi(aliasedName) 2209 ); 2210 }} 2211 2212 mixin(makeRtColumns); 2213 2214 Nullable!TSelect ret; 2215 auto ctx = FreeableAsyncResult!(void delegate(scope ffi.DBRowHandle)).make; 2216 ctx.forward_callback = (scope row) { 2217 if (row) 2218 ret = unwrapRowResult!(T, TSelect)(row, joinInformation); 2219 }; 2220 ffi.rorm_db_query_optional(db.handle, 2221 tx, 2222 ffi.ffi(DormLayout!T.tableName), 2223 ffi.ffi(rtColumns), 2224 ffi.ffi(joinInformation.joins), 2225 conditionTree.length ? &conditionTree[0] : null, 2226 ffi.ffi(ordering), 2227 ffi.FFIOption!ulong(_offset), 2228 ctx.callback.expand); 2229 ctx.result(); 2230 return ret; 2231 } 2232 } 2233 } 2234 2235 private enum makeRtColumns = q{ 2236 // inputs: ffi.FFIColumnSelector[n] columns; 2237 // JoinInformation joinInformation; 2238 // T (template type) 2239 // output: ffi.FFIColumnSelector[] rtColumns; 2240 2241 ffi.FFIColumnSelector[] rtColumns = columns[]; 2242 if (joinInformation.joinSuppl.any!"a.include") 2243 { 2244 static foreach (fk; DormForeignKeys!T) 2245 { 2246 if (auto joinId = fk.columnName in joinInformation.joinedTables) 2247 { 2248 auto suppl = joinInformation.joinSuppl[*joinId]; 2249 if (suppl.include) 2250 { 2251 auto ffiPlaceholder = ffi.ffi(suppl.placeholder); 2252 alias RefField = typeof(mixin("T.", fk.sourceColumn)); 2253 enum filteredFields = FilterLayoutFields!(RefField.TModel, RefField.TSelect); 2254 size_t start = rtColumns.length; 2255 size_t i = 0; 2256 rtColumns.length += filteredFields.length; 2257 static foreach (field; filteredFields) 2258 {{ 2259 auto ffiColumnName = ffi.ffi(field.columnName); 2260 auto aliasCol = text(suppl.placeholder, ("_" ~ field.columnName)); 2261 rtColumns[start + i].tableName = ffiPlaceholder; 2262 rtColumns[start + i].columnName = ffiColumnName; 2263 rtColumns[start + i].selectAlias = ffi.ffi(aliasCol); 2264 i++; 2265 }} 2266 } 2267 } 2268 } 2269 } 2270 }; 2271 2272 /// Row streaming range implementation. (query_stream) 2273 private struct RormStream(T, TSelect) 2274 { 2275 import dorm.lib.util; 2276 2277 private static struct RowHandleState 2278 { 2279 FreeableAsyncResult!(ffi.DBRowHandle) impl; 2280 alias impl this; 2281 bool done; 2282 2283 void reset() @safe 2284 { 2285 impl.reset(); 2286 done = false; 2287 } 2288 } 2289 2290 extern(C) private static void rowCallback( 2291 void* data, 2292 ffi.DBRowHandle result, 2293 scope ffi.RormError error 2294 ) nothrow @trusted 2295 { 2296 auto res = cast(RowHandleState*)data; 2297 if (error.tag == ffi.RormError.Tag.NoRowsLeftInStream) 2298 res.done = true; 2299 else if (error) 2300 res.error = error.makeException; 2301 else 2302 res.raw_result = result; 2303 res.awaiter.set(); 2304 } 2305 2306 private ffi.DBStreamHandle handle; 2307 private RowHandleState currentHandle; 2308 private JoinInformation joinInformation; 2309 private bool started; 2310 2311 this(ffi.DBStreamHandle handle, JoinInformation joinInformation = JoinInformation.init) @trusted 2312 { 2313 this.handle = handle; 2314 this.joinInformation = joinInformation; 2315 currentHandle = RowHandleState(FreeableAsyncResult!(ffi.DBRowHandle).make); 2316 } 2317 2318 ~this() @trusted 2319 { 2320 if (started) 2321 { 2322 currentHandle.impl.waitAndThrow(); 2323 if (currentHandle.impl.raw_result !is null) 2324 ffi.rorm_row_free(currentHandle.impl.raw_result); 2325 ffi.rorm_stream_free(handle); 2326 } 2327 } 2328 2329 @disable this(this); 2330 2331 /// Helper to `foreach` over this entire stream using the row mapped to 2332 /// `TSelect`. 2333 int opApply(scope int delegate(TSelect) @system dg) @system 2334 { 2335 return opApplyImpl(cast(int delegate(TSelect) @safe) dg); 2336 } 2337 /// ditto 2338 int opApply(scope int delegate(TSelect) @safe dg) @safe 2339 { 2340 return opApplyImpl(dg); 2341 } 2342 /// ditto 2343 int opApplyImpl(scope int delegate(TSelect) @safe dg) @safe 2344 { 2345 int result = 0; 2346 for (; !this.empty; this.popFront()) 2347 { 2348 result = dg(this.front); 2349 if (result) 2350 break; 2351 } 2352 return result; 2353 } 2354 2355 /// Helper to `foreach` over this entire stream using an index (simply 2356 /// counting up from 0 in D code) and the row mapped to `TSelect`. 2357 int opApply(scope int delegate(size_t i, TSelect) @system dg) @system 2358 { 2359 return opApplyImpl(cast(int delegate(size_t i, TSelect) @safe) dg); 2360 } 2361 /// ditto 2362 int opApply(scope int delegate(size_t i, TSelect) @safe dg) @safe 2363 { 2364 return opApplyImpl(dg); 2365 } 2366 /// ditto 2367 int opApplyImpl(scope int delegate(size_t i, TSelect) @safe dg) @safe 2368 { 2369 int result = 0; 2370 size_t i; 2371 for (; !this.empty; this.popFront()) 2372 { 2373 result = dg(i++, this.front); 2374 if (result) 2375 break; 2376 } 2377 return result; 2378 } 2379 2380 /// Starts the iteration if it hasn't already, waits until data is there 2381 /// and returns the current row. 2382 /// 2383 /// Implements the standard D range interface. 2384 auto front() @trusted 2385 { 2386 if (!started) nextIteration(); 2387 return unwrapRowResult!(T, TSelect)(currentHandle.result(), joinInformation); 2388 } 2389 2390 /// Starts the iteration if it hasn't already, waits until data is there 2391 /// and returns if there is any data left to be read using `front`. 2392 bool empty() @trusted 2393 { 2394 if (!started) nextIteration(); 2395 currentHandle.impl.waitAndThrow(); 2396 return currentHandle.done; 2397 } 2398 2399 /// Starts the iteration if it hasn't already, waits until the current 2400 /// request is finished and skips the current row, so empty and front can 2401 /// be called next. 2402 void popFront() @trusted 2403 { 2404 if (!started) nextIteration(); 2405 currentHandle.impl.waitAndThrow(); 2406 if (currentHandle.done) 2407 throw new Exception("attempted to run popFront on ended stream"); 2408 else if (currentHandle.impl.error) 2409 throw currentHandle.impl.error; 2410 else 2411 { 2412 ffi.rorm_row_free(currentHandle.impl.raw_result); 2413 currentHandle.reset(); 2414 nextIteration(); 2415 } 2416 } 2417 2418 private void nextIteration() @trusted 2419 { 2420 started = true; 2421 ffi.rorm_stream_get_row(handle, &rowCallback, cast(void*)¤tHandle); 2422 } 2423 2424 static assert(isInputRange!RormStream, "implementation error: did not become an input range"); 2425 } 2426 2427 /// Extracts the DBRowHandle, optionally using JoinInformation when joins were 2428 /// used, into the TSelect datatype. TSelect may be a DormPatch or the model T 2429 /// directly. This is mostly used internally. Expect changes to this API until 2430 /// there is a stable API. 2431 TSelect unwrapRowResult(T, TSelect)(ffi.DBRowHandle row, JoinInformation ji) @safe 2432 { 2433 auto base = unwrapRowResultImpl!(T, TSelect)(row, "__"); 2434 if (ji.joins.length) 2435 { 2436 static foreach (fk; DormForeignKeys!T) 2437 {{ 2438 if (auto idx = fk.columnName in ji.joinedTables) 2439 { 2440 auto suppl = ji.joinSuppl[*idx]; 2441 if (suppl.include) 2442 { 2443 auto prefix = suppl.placeholder; 2444 alias ModelRef = typeof(mixin("T.", fk.sourceColumn)); 2445 mixin("base.", fk.sourceColumn) = 2446 unwrapRowResult!(ModelRef.TModel, ModelRef.TSelect)(row, prefix); 2447 } 2448 } 2449 }} 2450 } 2451 return base; 2452 } 2453 2454 /// ditto 2455 TSelect unwrapRowResult(T, TSelect)(ffi.DBRowHandle row) @safe 2456 { 2457 return unwrapRowResultImpl!(T, TSelect, false)(row, null); 2458 } 2459 2460 /// Unwraps the row like the other unwrap methods, but prefixes all fields with 2461 /// `<placeholder>_`, so for example placeholder `foo` and field `user` would 2462 /// result in `foo_user`. 2463 TSelect unwrapRowResult(T, TSelect)(ffi.DBRowHandle row, string placeholder) @safe 2464 { 2465 scope placeholderDot = new char[placeholder.length + 1]; 2466 placeholderDot[0 .. placeholder.length] = placeholder; 2467 placeholderDot[$ - 1] = '_'; // was dot before, but that's not valid SQL - we use _ to separate names in aliases! 2468 return unwrapRowResultImpl!(T, TSelect)(row, (() @trusted => cast(string)placeholderDot)()); 2469 } 2470 2471 private TSelect unwrapRowResultImpl(T, TSelect)(ffi.DBRowHandle row, string columnPrefix) @safe 2472 { 2473 TSelect res; 2474 static if (is(TSelect == class)) 2475 res = new TSelect(); 2476 ffi.RormError rowError; 2477 enum fields = FilterLayoutFields!(T, TSelect); 2478 static foreach (field; fields) 2479 { 2480 mixin("res." ~ field.sourceColumn) = extractField!(field, typeof(mixin("res." ~ field.sourceColumn)), 2481 text(" from model ", T.stringof, 2482 " in column ", field.sourceColumn, 2483 " in file ", field.definedAt).idup 2484 )(row, rowError, columnPrefix); 2485 if (rowError) 2486 throw rowError.makeException(" (in column '" ~ columnPrefix ~ field.columnName ~ "')"); 2487 } 2488 return res; 2489 } 2490 2491 private T extractField(alias field, T, string errInfo)( 2492 ffi.DBRowHandle row, 2493 ref ffi.RormError error, 2494 string columnPrefix 2495 ) @trusted 2496 { 2497 import std.conv; 2498 import dorm.declarative; 2499 2500 auto columnName = ffi.ffi(columnPrefix.length 2501 ? columnPrefix ~ field.columnName 2502 : field.columnName); 2503 2504 enum pre = field.isNullable() ? "ffi.rorm_row_get_null_" : "ffi.rorm_row_get_"; 2505 enum suf = "(row, columnName, error)"; 2506 2507 final switch (field.type) with (ModelFormat.Field.DBType) 2508 { 2509 case varchar: 2510 static if (field.type == varchar) return fieldInto!(T, errInfo)(mixin(pre, "str", suf), error); 2511 else assert(false); 2512 case varbinary: 2513 static if (field.type == varbinary) return fieldInto!(T, errInfo)(mixin(pre, "binary", suf), error); 2514 else assert(false); 2515 case int8: 2516 static if (field.type == int8) return fieldInto!(T, errInfo)(mixin(pre, "i16", suf), error); 2517 else assert(false); 2518 case int16: 2519 static if (field.type == int16) return fieldInto!(T, errInfo)(mixin(pre, "i16", suf), error); 2520 else assert(false); 2521 case int32: 2522 static if (field.type == int32) return fieldInto!(T, errInfo)(mixin(pre, "i32", suf), error); 2523 else assert(false); 2524 case int64: 2525 static if (field.type == int64) return fieldInto!(T, errInfo)(mixin(pre, "i64", suf), error); 2526 else assert(false); 2527 case floatNumber: 2528 static if (field.type == floatNumber) return fieldInto!(T, errInfo)(mixin(pre, "f32", suf), error); 2529 else assert(false); 2530 case doubleNumber: 2531 static if (field.type == doubleNumber) return fieldInto!(T, errInfo)(mixin(pre, "f64", suf), error); 2532 else assert(false); 2533 case boolean: 2534 static if (field.type == boolean) return fieldInto!(T, errInfo)(mixin(pre, "bool", suf), error); 2535 else assert(false); 2536 case date: 2537 static if (field.type == date) return fieldInto!(T, errInfo)(mixin(pre, "date", suf), error); 2538 else assert(false); 2539 case time: 2540 static if (field.type == time) return fieldInto!(T, errInfo)(mixin(pre, "time", suf), error); 2541 else assert(false); 2542 case datetime: 2543 static if (field.type == datetime) return fieldInto!(T, errInfo)(mixin(pre, "datetime", suf), error); 2544 else assert(false); 2545 2546 static assert( 2547 field.type != set, 2548 "field type " ~ field.type.to!string ~ " not yet implemented for reading"); 2549 2550 case choices: 2551 static if (field.type == choices) return fieldInto!(T, errInfo)(mixin(pre, "str", suf), error); 2552 else assert(false); 2553 case set: assert(false); 2554 } 2555 } 2556 2557 private T fieldInto(T, string errInfo, From)(scope From v, ref ffi.RormError error) @safe 2558 { 2559 import dorm.lib.ffi : FFIArray, FFIOption; 2560 import std.typecons : Nullable; 2561 2562 static if (is(T == From)) 2563 return v; 2564 else static if (is(T == enum)) 2565 { 2566 auto s = fieldInto!(string, errInfo, From)(v, error); 2567 static if (is(OriginalType!T == string)) 2568 return cast(T)s; 2569 else 2570 { 2571 switch (s) 2572 { 2573 static foreach (f; __traits(allMembers, T)) 2574 { 2575 case f: 2576 return __traits(getMember, T, f); 2577 } 2578 default: 2579 error = ffi.RormError(ffi.RormError.Tag.ColumnDecodeError); 2580 return T.init; 2581 } 2582 } 2583 } 2584 else static if (is(T == ModelRefImpl!(id, _TModel, _TSelect), alias id, _TModel, _TSelect)) 2585 { 2586 T ret; 2587 ret.foreignKey = fieldInto!(typeof(id), errInfo, From)(v, error); 2588 return ret; 2589 } 2590 else static if (is(From == FFIArray!U, U)) 2591 { 2592 static if (is(T == Res[], Res)) 2593 { 2594 static if (is(immutable Res == immutable U)) 2595 return (() @trusted => cast(T)v.data.dup)(); 2596 else 2597 static assert(false, "can't auto-wrap array element type " ~ Res.stringof ~ " into " ~ U.stringof ~ errInfo); 2598 } 2599 else static if (is(T == Nullable!V, V)) 2600 { 2601 return T(fieldInto!(V, errInfo, From)(v, error)); 2602 } 2603 else 2604 static assert(false, "can't auto-wrap " ~ U.stringof ~ "[] into " ~ T.stringof ~ errInfo); 2605 } 2606 else static if (is(From == FFIOption!U, U)) 2607 { 2608 static if (is(T == Nullable!V, V)) 2609 { 2610 if (v.isNull) 2611 return T.init; 2612 else 2613 return T(fieldInto!(V, errInfo)(v.raw_value, error)); 2614 } 2615 else static if (__traits(compiles, T(null))) 2616 { 2617 if (v.isNull) 2618 return T(null); 2619 else 2620 return fieldInto!(T, errInfo)(v.raw_value, error); 2621 } 2622 else 2623 { 2624 if (v.isNull) 2625 { 2626 error = ffi.RormError(ffi.RormError.Tag.ColumnDecodeError); 2627 return T.init; 2628 } 2629 else 2630 { 2631 return fieldInto!(T, errInfo)(v.raw_value, error); 2632 } 2633 } 2634 } 2635 else static if (is(T == Nullable!U, U)) 2636 { 2637 return T(fieldInto!(U, errInfo, From)(v, error)); 2638 } 2639 else static if (isIntegral!From) 2640 { 2641 static if (isIntegral!T && From.sizeof >= T.sizeof) 2642 { 2643 if (v < cast(From)T.min || v > cast(From)T.max) 2644 { 2645 error = ffi.RormError(ffi.RormError.Tag.ColumnDecodeError); 2646 return T.init; 2647 } 2648 else 2649 { 2650 return cast(T)v; 2651 } 2652 } 2653 else static if (isFloatingPoint!T) 2654 { 2655 return cast(T)v; 2656 } 2657 else 2658 static assert(false, "can't put " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2659 } 2660 else static if (isFloatingPoint!From) 2661 { 2662 static if (isFloatingPoint!T) 2663 return cast(T)v; 2664 else 2665 static assert(false, "can't put " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2666 } 2667 else static if (is(From : ffi.FFITime)) 2668 { 2669 static if (is(T == TimeOfDay)) 2670 { 2671 try 2672 { 2673 return TimeOfDay(cast(int)v.hour, cast(int)v.min, cast(int)v.sec); 2674 } 2675 catch (DateTimeException) 2676 { 2677 error = ffi.RormError(ffi.RormError.Tag.InvalidTimeError); 2678 return T.init; 2679 } 2680 } 2681 else 2682 static assert(false, "can't put " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2683 } 2684 else static if (is(From : ffi.FFIDate)) 2685 { 2686 static if (is(T == Date)) 2687 { 2688 try 2689 { 2690 return Date(cast(int)v.year, cast(int)v.month, cast(int)v.day); 2691 } 2692 catch (DateTimeException) 2693 { 2694 error = ffi.RormError(ffi.RormError.Tag.InvalidDateError); 2695 return T.init; 2696 } 2697 } 2698 else 2699 static assert(false, "can't put " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2700 } 2701 else static if (is(From : ffi.FFIDateTime)) 2702 { 2703 try 2704 { 2705 static if (is(T == DateTime)) 2706 { 2707 return DateTime(cast(int)v.year, cast(int)v.month, cast(int)v.day, 2708 cast(int)v.hour, cast(int)v.min, cast(int)v.sec); 2709 } 2710 else static if (is(T == SysTime)) 2711 { 2712 return SysTime(DateTime(cast(int)v.year, cast(int)v.month, cast(int)v.day, 2713 cast(int)v.hour, cast(int)v.min, cast(int)v.sec), UTC()); 2714 } 2715 else static if (is(T == long) || is(T == ulong)) 2716 { 2717 return cast(T)SysTime(DateTime(cast(int)v.year, cast(int)v.month, cast(int)v.day, 2718 cast(int)v.hour, cast(int)v.min, cast(int)v.sec), UTC()).stdTime; 2719 } 2720 else 2721 static assert(false, "can't put " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2722 } 2723 catch (DateTimeException) 2724 { 2725 error = ffi.RormError(ffi.RormError.Tag.InvalidDateTimeError); 2726 return T.init; 2727 } 2728 } 2729 else 2730 static assert(false, "did not implement conversion from " ~ From.stringof ~ " into " ~ T.stringof ~ errInfo); 2731 } 2732 2733 /// Sets up the DORM runtime that is required to use DORM (and its 2734 /// implementation library "RORM") 2735 /// 2736 /// You must use this mixin to use DORM. You can simply call 2737 /// ```d 2738 /// mixin SetupDormRuntime; 2739 /// ``` 2740 /// in your entrypoint file to have the runtime setup automatically. 2741 /// 2742 /// Supports passing in a timeout (Duration or integer msecs) 2743 mixin template SetupDormRuntime(alias timeout = 10.seconds) 2744 { 2745 __gshared bool _initializedDormRuntime; 2746 2747 shared static this() @trusted 2748 { 2749 import dorm.lib.util : sync_call; 2750 import dorm.lib.ffi : rorm_runtime_start; 2751 2752 sync_call!(rorm_runtime_start)(); 2753 _initializedDormRuntime = true; 2754 } 2755 2756 shared static ~this() @trusted 2757 { 2758 import core.time : Duration; 2759 import dorm.lib.util; 2760 import dorm.lib.ffi : rorm_runtime_shutdown; 2761 2762 if (_initializedDormRuntime) 2763 { 2764 static if (is(typeof(timeout) == Duration)) 2765 sync_call!(rorm_runtime_shutdown)(timeout.total!"msecs"); 2766 else 2767 sync_call!(rorm_runtime_shutdown)(timeout); 2768 } 2769 } 2770 }