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