1 /++ 2 SQL Query Builder 3 4 Construct SQL queries from code. 5 Compile them to any specific SQL dialect later. 6 7 Works at compile-time thanks to $(B CTFE). 8 Best used with $(B UFCS). 9 10 --- 11 table("person").qb 12 .where("age", '>') 13 .select("*") 14 ; 15 // is equivalent to writing this query by hand 16 // --> SELECT * FROM "person" WHERE "age" > ? 17 --- 18 19 [table] creates a struct instance representing a table; this is the base for query building. 20 21 [qb] instantiates a complex query builder (for SELECT, UPDATE or DELETE queries – those that can have WHERE clauses etc.) 22 23 [where] appends a new condition to a query’s WHERE clause. 24 For disjunctions (“OR”) the first template parameter is set to `or (i.e. `where!or(…)`) 25 whereas `where!and` is the default. 26 27 Parentheses `()` can be inserted by calling [whereParentheses]: 28 29 --- 30 table("person").qb 31 .whereParentheses(q => q 32 .where("age", '>') 33 .where!or("height", '>') 34 ) 35 .where("gender", '=') 36 .select("*") 37 ; 38 // --> SELECT * FROM "person" WHERE ( "age" > ? OR "height" > ? ) AND "gender" = ? 39 --- 40 41 Applying aggregate functions is super simple as well: 42 Helper functions [avg], [count], [max], [min], [sum] and [group_concat] got you covered! 43 44 `DISTINCT` can be applied by setting the first (and only) template parameter of said functions to `distinct` (or `true`), 45 i.e. `SELECT COUNT(DISTINCT "name")` becomes `.select(count!distinct("name"))`. 46 47 --- 48 table("person").qb // FROM "person" 49 .where("age", '>') // WHERE "age" > ? 50 .select( // SELECT 51 count!distinct("name") // COUNT(DISTINCT "name") 52 ) 53 ; 54 --- 55 56 Selecting multiple columns? No big deal either: 57 58 --- 59 table("person").qb.select( 60 "name", 61 "age" 62 ); 63 // --> SELECT "name", "age" FROM "person" 64 --- 65 66 Writing `.select(*)` is tedious? How about omitting the parameter? 67 `"*"` is the default:` 68 69 --- 70 table("person").qb.select(); 71 // --> SELECT * FROM "person" 72 --- 73 74 $(SIDEBAR 75 I am not sure whether it’s reasonable to have a default value here. 76 It might be better to just `static assert` instead. 77 So: Dear users, please let me know what you think! 78 ) 79 80 Pre-set values during query building. 81 Those values are not inserted into the query string (Regular dynamic parameters are used as well.) 82 but will be bound to the prepared statement later. 83 84 --- 85 table("person").qb 86 .where("age", '>', 18) 87 .select("*") 88 // --> SELECT * FROM person WHERE "age" > ? 89 --- 90 91 Talking about “later”, it’s time to build an actual query from those abstractations… 92 This is done by calling [build] on the [Query]. 93 The target dialect is specified through a template parameter ([oceandrift.db.sqlite3.SQLite3] in this example). 94 95 --- 96 BuiltQuery q = table("person").qb 97 .where("age", '>') 98 .select("*") 99 .build!SQLite3() 100 ; 101 writeln(q.sql); // prints: SELECT * FROM "person" WHERE "age" > ? 102 --- 103 104 Multiple conditions can be chained by calling [where] multiple times: 105 106 --- 107 Query q = table("person").qb 108 .where("age", '>') 109 .where("height", '>') 110 ; 111 // --> FROM "person" WHERE "age" > ? AND "height" > ? 112 --- 113 114 Note that “conjunction” (AND) is the default. 115 To create a “disjunction” (OR) set the first template parameter of [where] to [or] 116 117 --- 118 Query q = table("person").qb 119 .where ("age", '>') 120 .where!or("height", '>') 121 ; 122 // --> FROM "person" WHERE "age" > ? OR "height" > ? 123 --- 124 125 $(TIP 126 [or] is shorthand for [LogicalOperator.or]. 127 ) 128 129 $(TIP 130 Of course there is [and] (aka [LogicalOperator.and]), too. 131 132 --- 133 Query q = table("person").qb 134 .where ("age", '>') 135 .where!and("height", '>') 136 ; 137 // --> FROM "person" WHERE "age" > ? AND "height" > ? 138 --- 139 140 This results in the same query as not specifing the logical juntion in the first place. 141 ) 142 143 $(NOTE 144 The primary goal of this query builder implementation is to power an ORM. 145 Nevertheless, it’s designed to be used by actual human programmers. 146 147 It doesn’t support every possible SQL feature. 148 The goal is to support the subset needed for ORM purposes. 149 150 I’d rather have limitations that affect versatility than to give up usability. 151 Feedback is always welcome! 152 ) 153 154 Codename: v4 (“version 4”) 155 156 Special thanks to Steven “schveiguy” Schveighoffer. 157 +/ 158 module oceandrift.db.dbal.v4; 159 160 import std.conv : to; 161 import std.traits : ReturnType; 162 import std.typecons : Nullable; 163 164 import oceandrift.db.dbal.driver; 165 166 @safe: 167 168 /++ 169 Prepares a built query using the specified database connection 170 +/ 171 Statement prepareBuiltQuery(DatabaseDriver)(DatabaseDriver db, BuiltQuery builtQuery) 172 { 173 Statement stmt = db.prepare(builtQuery.sql); 174 175 foreach (index, value; builtQuery.preSets.where) 176 stmt.bindDBValue(index, value); 177 178 if (!builtQuery.preSets.limit.isNull) 179 stmt.bind(cast(int) builtQuery.placeholders.where + 0, builtQuery.preSets.limit.get); 180 181 if (!builtQuery.preSets.limitOffset.isNull) 182 stmt.bind(cast(int) builtQuery.placeholders.where + 1, builtQuery.preSets.limitOffset.get); 183 184 return stmt; 185 } 186 187 /// ditto 188 Statement prepare(DatabaseDriver)(BuiltQuery builtQuery, DatabaseDriver db) 189 { 190 return db.prepareBuiltQuery(builtQuery); 191 } 192 193 // there’s no “impure” keyword :( 194 195 pure: 196 197 /// 198 enum ComparisonOperator : wchar 199 { 200 invalid = '\0', 201 202 equals = '=', 203 notEquals = '≠', 204 lessThan = '<', 205 greaterThan = '>', 206 lessThanOrEquals = '≤', 207 greaterThanOrEquals = '≥', 208 209 in_ = '∈', 210 notIn = '∉', 211 like = '≈', 212 notLike = '≉', 213 isNull = '0', 214 isNotNull = '1', // 215 // between = '∓', 216 } 217 218 /++ 219 Logical operators 220 +/ 221 enum LogicalOperator : bool 222 { 223 /// OR (logical operator, SQL) 224 or = true, 225 226 /// AND (logical operator, SQL) 227 and = false, 228 } 229 230 enum 231 { 232 /// OR (logical operator, SQL) 233 or = LogicalOperator.or, 234 235 /// AND (logical operator, SQL) 236 and = LogicalOperator.and, 237 } 238 239 /// NOT (SQL) 240 enum not = false; 241 242 /++ 243 Abstract SQL query token 244 +/ 245 struct Token 246 { 247 /++ 248 Meaning of the token 249 +/ 250 Type type; 251 252 /++ 253 Additional data (optional) 254 +/ 255 Data data; 256 257 /++ 258 Token types 259 +/ 260 enum Type : char 261 { 262 invalid = '\xFF', /// garbage, apparently something is broken should you encounter this type in an actual token 263 264 column = 'c', 265 columnTable = 't', 266 placeholder = '?', 267 comparisonOperator = 'o', /// a [ComparisonOperator], for the actual operator see [Token.data.op] 268 269 and = '&', 270 or = '|', 271 272 not = '!', 273 274 leftParenthesis = '(', 275 rightParenthesis = ')', 276 } 277 278 /++ 279 Token data 280 +/ 281 union Data 282 { 283 string str; 284 ComparisonOperator op; 285 } 286 } 287 288 /++ 289 Database Table Representation 290 +/ 291 struct Table 292 { 293 string name; 294 } 295 296 /++ 297 Convenience function to create a table instance 298 +/ 299 inout(Table) table(inout string name) nothrow @nogc 300 { 301 return Table(name); 302 } 303 304 /++ 305 Column identifier 306 +/ 307 struct Column 308 { 309 string name; 310 Table table; 311 } 312 313 inout(Column) column(inout string name) nothrow @nogc 314 { 315 return Column(name, Table(null)); 316 } 317 318 inout(Column) column(inout string name, inout Table table) nothrow @nogc 319 { 320 return Column(name, table); 321 } 322 323 inout(Column) column(inout Table table, inout string name) nothrow @nogc 324 { 325 return Column(name, table); 326 } 327 328 alias col = column; 329 330 /++ 331 Abstract WHERE clause 332 +/ 333 struct Where 334 { 335 Token[] tokens; 336 DBValue[int] preSet; // Pre-set values provided during query building 337 private: 338 int _placeholders = 0; // number of placeholders in tokens 339 340 public @safe pure nothrow @nogc: 341 int placeholders() const 342 { 343 return _placeholders; 344 } 345 } 346 347 void updatePreSetWhereValue(ref Query q, int index, DBValue value) @trusted 348 { 349 q._where.preSet[index] = value; 350 } 351 352 /++ 353 Ascending vs. Descending 354 +/ 355 enum OrderingSequence : bool 356 { 357 asc = false, 358 desc = true, 359 } 360 361 enum 362 { 363 /++ 364 ASCending 365 +/ 366 asc = OrderingSequence.asc, 367 368 /++ 369 DESCending 370 +/ 371 desc = OrderingSequence.desc, 372 } 373 374 struct OrderingTerm 375 { 376 Column column; 377 OrderingSequence orderingSequence = OrderingSequence.asc; 378 } 379 380 struct Limit 381 { 382 bool enabled = false; 383 Nullable!ulong preSet; 384 385 bool offsetEnabled = false; 386 Nullable!ulong offsetPreSet; 387 } 388 389 /++ 390 JOIN clause abstraction 391 +/ 392 struct Join 393 { 394 /++ 395 JOIN types 396 +/ 397 enum Type 398 { 399 invalid = '\xFF', /// 400 inner = 'i', /// 401 leftOuter = 'L', /// 402 rightOuter = 'R', /// 403 fullOuter = 'F', /// 404 cross = 'C' /// 405 } 406 407 Type type; 408 Column source; 409 Column target; 410 } 411 412 enum : Join.Type 413 { 414 /// [INNER] JOIN 415 inner = Join.Type.inner, 416 417 /// LEFT [OUTER] JOIN 418 leftOuter = Join.Type.leftOuter, 419 420 /// RIGHT [OUTER] JOIN 421 rightOuter = Join.Type.rightOuter, 422 423 /// FULL [OUTER] JOIN 424 fullOuter = Join.Type.fullOuter, 425 426 /// CROSS JOIN 427 cross = Join.Type.cross, 428 } 429 430 /++ 431 SQL SELECT/UPDATE/DELETE query abstraction 432 433 This is the secondary base type for query building. 434 435 Not used with INSERT queries. 436 437 --- 438 Query myQuery = table("my_table").qb; 439 440 Select myQuerySelectAll= myQuery.where(/* … */).select("*"); 441 BuiltQuery myQuerySelectAllBuilt = myQuerySelectAll.build!Database; 442 --- 443 +/ 444 struct Query 445 { 446 Table table; 447 448 private: 449 Join[] _join; 450 Where _where; 451 OrderingTerm[] _orderBy; 452 Limit _limit; 453 } 454 455 /++ 456 Special query representation for use in query compilers 457 +/ 458 struct CompilerQuery 459 { 460 @safe pure nothrow @nogc: 461 462 this(const Query q) 463 { 464 this.table = q.table; 465 this.join = q._join; 466 this.where = q._where; 467 this.orderBy = q._orderBy; 468 this.limit = q._limit; 469 } 470 471 const 472 { 473 /++ 474 Table to query 475 +/ 476 Table table; 477 478 /++ 479 JOIN clause 480 +/ 481 Join[] join; 482 483 /++ 484 WHERE clause 485 +/ 486 Where where; 487 488 /++ 489 ORDER BY clause 490 +/ 491 OrderingTerm[] orderBy; 492 493 /++ 494 LIMIT clause 495 +/ 496 Limit limit; 497 } 498 } 499 500 /++ 501 Returns: a complex query builder for the specified table 502 +/ 503 Query complexQueryBuilder(const Table table) nothrow @nogc 504 { 505 return Query(table); 506 } 507 508 /// ditto 509 Query complexQueryBuilder(const string table) nothrow @nogc 510 { 511 return Query(Table(table)); 512 } 513 514 /// ditto 515 alias qb = complexQueryBuilder; 516 517 /++ 518 Appends a JOIN statement to a query 519 520 --- 521 // … FROM book JOIN author ON author.id = author_id … 522 Query q = table("book").qb.join( 523 table("author"), 524 "id", 525 "author_id" 526 ); 527 // or: 528 Query q = table("book").qb.join( 529 column(table("author"), "id"), 530 "author_id" 531 ); 532 533 enum book = table("book"); 534 enum author = table("author"); 535 Query q = book.qb.join( 536 column(author, "id"), 537 column(book, "author_id") 538 ); 539 // --> … FROM book JOIN author ON author.id = book.author_id … 540 --- 541 542 Params: 543 joinTarget = determines which table to join with (and which column to use in the join constraint (“ON”)) 544 +/ 545 Query join(Join.Type type = Join.Type.inner)(Query q, const Column joinTarget, const Column sourceColumn) 546 in (!((joinTarget.name is null) ^ (sourceColumn.name is null))) 547 { 548 q._join ~= Join(type, sourceColumn, joinTarget); 549 return q; 550 } 551 552 /// ditto 553 Query join(Join.Type type = Join.Type.inner)(Query q, const Column joinTarget, const string sourceColumn) 554 { 555 pragma(inline, true); 556 return join!type(q, joinTarget, col(sourceColumn)); 557 } 558 559 /// ditto 560 Query join(Join.Type type = Join.Type.inner)( 561 Query q, 562 const Table joinTarget, 563 const string joinOnTargetColumn, 564 const string onSourceColumn 565 ) 566 { 567 pragma(inline, true); 568 return join!type(q, col(joinTarget, joinOnTargetColumn), col(onSourceColumn)); 569 } 570 571 enum bool isComparisonOperator(T) = ( 572 is(T == ComparisonOperator) 573 || is(T == wchar) 574 || is(T == char) 575 ); 576 577 /++ 578 Appends a condition to the query's WHERE clause 579 580 --- 581 // …FROM mountain WHERE height > ?… 582 Query qMountainsGreaterThan = table("mountain").qb.where("height", '>'); 583 584 // …FROM mountain WHERE height > ?… 585 // Pre-sets the value `8000` for the dynamic paramter. 586 Query qMountainsGreaterThan = table("mountain").qb.where("height", '>', 8000); 587 588 // …FROM people WHERE ago > ? AND age < ?… 589 // Pre-sets the values 60 and 100 for the dynamic paramters. 590 Query qOver60butNot100yet = table("people").qb 591 .where("age", '>', 60) 592 .where("age", ComparisonOperator.lessThan, 100) 593 ; 594 595 // …FROM people WHERE name = ? OR name = ?… 596 Query qNameAorB = table("people").qb 597 .where ("name", '=') 598 .where!or("name", '=') // explicit !or as !and is the default 599 ; 600 --- 601 +/ 602 /// ditto 603 Query where(LogicalOperator logicalJunction = and, TComparisonOperator)( 604 Query q, 605 Column column, 606 TComparisonOperator op 607 ) if (isComparisonOperator!TComparisonOperator) 608 { 609 enum Token tokenLogicalJunction = 610 (logicalJunction == or) ? Token(Token.Type.or) : Token(Token.Type.and); 611 612 if ((q._where.tokens.length > 0) && (q._where.tokens[$ - 1].type != Token.Type.leftParenthesis)) 613 q._where.tokens ~= tokenLogicalJunction; 614 615 if (column.table.name !is null) 616 q._where.tokens ~= Token(Token.Type.columnTable, Token.Data(column.table.name)); 617 q._where.tokens ~= Token(Token.Type.column, Token.Data(column.name)); 618 619 auto dataOp = Token.Data(); 620 dataOp.op = cast(ComparisonOperator) op; 621 q._where.tokens ~= Token(Token.Type.comparisonOperator, dataOp); 622 623 if ((op != ComparisonOperator.isNull) && (op != ComparisonOperator.isNotNull)) 624 { 625 q._where.tokens ~= Token(Token.Type.placeholder); 626 ++q._where._placeholders; 627 } 628 629 return q; 630 } 631 632 /// ditto 633 Query where(LogicalOperator logicalJunction = and, TComparisonOperator)( 634 Query q, 635 string column, 636 TComparisonOperator op 637 ) if (isComparisonOperator!TComparisonOperator) 638 { 639 pragma(inline, true); 640 return where!logicalJunction(q, col(column), op); 641 } 642 643 /// ditto 644 Query where(LogicalOperator logicalJunction = and, TComparisonOperator, T)( 645 Query q, 646 Column column, 647 TComparisonOperator op, 648 T value 649 ) @trusted // TODO: template constraint 650 { 651 q._where.preSet[q._where.placeholders] = value; 652 653 return q.where!logicalJunction(column, op); 654 } 655 656 Query where(LogicalOperator logicalJunction = and, TComparisonOperator, T)( 657 Query q, 658 string column, 659 TComparisonOperator op, 660 T value 661 ) 662 { 663 pragma(inline, true); 664 return where!logicalJunction(q, col(column), op, value); 665 } 666 667 /++ 668 Appends checks in parentheses to the query's WHERE clause 669 670 --- 671 // …FROM mountain WHERE height > ? AND ( country = ? OR country = ? )… 672 Query qMountainsGreaterThanInUSorCA = table("mountain").qb 673 .where("height", '>') 674 .whereParentheses(q => q 675 .where ("location", '=', "US") 676 .where!or("location", '=', "CA") 677 ) 678 ; 679 --- 680 +/ 681 Query whereParentheses(LogicalOperator logicalJunction = and)(Query q, Query delegate(Query q) @safe pure conditions) 682 { 683 enum Token tokenLogicalJunction = 684 (logicalJunction == or) ? Token(Token.Type.or) : Token(Token.Type.and); 685 686 if ((q._where.tokens.length > 0) && (q._where.tokens[$ - 1].type != Token.Type.leftParenthesis)) 687 q._where.tokens ~= tokenLogicalJunction; 688 689 q._where.tokens ~= Token(Token.Type.leftParenthesis); 690 q = conditions(q); 691 q._where.tokens ~= Token(Token.Type.rightParenthesis); 692 693 return q; 694 } 695 696 /// 697 template whereNot(bool logicalJunction = or, TComparisonOperator) 698 if (isComparisonOperator!TComparisonOperator) 699 { 700 Query whereNot(Query q, Column column, TComparisonOperator op) 701 { 702 q._where ~= Token(Token.Type.not); 703 q._where ~= Token(Token.Type.leftParenthesis); 704 q.where!logicalJunction(column, op); 705 q._where ~= Token(Token.Type.rightParenthesis); 706 } 707 708 Query whereNot(Query q, Column column, TComparisonOperator op, DBValue value) 709 { 710 q._where ~= Token(Token.Type.not); 711 q._where ~= Token(Token.Type.leftParenthesis); 712 q.where!logicalJunction(column, op, value); 713 q._where ~= Token(Token.Type.rightParenthesis); 714 } 715 716 Query whereNot(Query q, string column, TComparisonOperator op) 717 { 718 pragma(inline, true); 719 return whereNot(q, col(column), op); 720 } 721 722 Query whereNot(Query q, string column, TComparisonOperator op, DBValue value) 723 { 724 pragma(inline, true); 725 return whereNot(q, col(column), op, value); 726 } 727 } 728 729 /++ 730 Appends an ORDER BY clause to a query 731 732 --- 733 q.orderBy("column"); // ASCending order is the default 734 735 q.orderBy("column", asc); // explicit ASCending order 736 q.orderBy("column", desc); // DESCending order 737 738 q.orderBy("column", OrderingSequence.asc); // ASC, long form 739 q.orderBy("column", OrderingSequence.desc); // DESC, long form 740 --- 741 +/ 742 Query orderBy(Query q, Column column, OrderingSequence orderingSequence = asc) 743 { 744 q._orderBy ~= OrderingTerm(column, orderingSequence); 745 return q; 746 } 747 748 Query orderBy(Query q, string column, OrderingSequence orderingSequence = asc) 749 { 750 pragma(inline, true); 751 return orderBy(q, col(column), orderingSequence); 752 } 753 754 /++ 755 Sets or updates the LIMIT clause of a query 756 757 --- 758 q.limit(25); // LIMIT ? – pre-set: limit=25 759 q.limit(25, 100); // LIMIT ? OFFSET ? – pre-set: limit=25,offset=100 760 761 q.limit(false); // LIMIT ? – set values later 762 q.limit(true); // LIMIT ? OFFSET ? – set values later 763 +/ 764 Query limit(bool withOffset = false)(Query q) 765 { 766 q._limit.enabled = true; 767 q._limit.preSet.nullify(); 768 769 q._limit.offsetEnabled = withOffset; 770 q._limit.offsetPreSet.nullify(); 771 772 return q; 773 } 774 775 /// ditto 776 Query limit(Query q, ulong limit) 777 { 778 q._limit.enabled = true; 779 q._limit.preSet = limit; 780 781 return q; 782 } 783 784 /// ditto 785 Query limit(Query q, ulong limit, ulong offset) 786 { 787 q._limit.enabled = true; 788 q._limit.preSet = limit; 789 q._limit.offsetEnabled = true; 790 q._limit.offsetPreSet = offset; 791 792 return q; 793 } 794 795 // -- Select 796 797 /++ 798 SQL aggregate function types 799 +/ 800 enum AggregateFunction 801 { 802 none = 0, /// 803 avg, /// 804 count, /// 805 max, /// 806 min, /// 807 sum, /// 808 group_concat, /// 809 } 810 811 /++ 812 SELECT expression abstraction 813 814 $(WARNING 815 A select expression is a unit to be selected by a query (like a column, or the aggregate (count, min, max, …) of a column). 816 817 Not to confuse with “SELECT query”. 818 See [Select] for abstraction of the latter. 819 ) 820 +/ 821 struct SelectExpression 822 { 823 /++ 824 Column to select 825 +/ 826 Column column; 827 828 /++ 829 Aggregate function to apply 830 +/ 831 AggregateFunction aggregateFunction = AggregateFunction.none; 832 833 /++ 834 Whether selection should be DISTINCT (or not) 835 +/ 836 Distinct distinct = Distinct.no; 837 } 838 839 /++ 840 SQL DISTINCT keyword abstraction 841 +/ 842 enum Distinct : bool 843 { 844 /// 845 no = false, 846 847 /// 848 yes = true, 849 } 850 851 /++ 852 DISTINCT 853 i.e. no duplicates 854 +/ 855 enum Distinct distinct = Distinct.yes; 856 857 /++ 858 Short-hand helper function for SELECT AVG(…) 859 +/ 860 SelectExpression avg(Distinct distinct = Distinct.no)(string column) 861 { 862 return SelectExpression(col(column), AggregateFunction.avg, distinct); 863 } 864 865 /++ 866 Short-hand helper function for SELECT COUNT(…) 867 +/ 868 SelectExpression count(Distinct distinct = Distinct.no)(string column = "*") 869 { 870 return SelectExpression(col(column), AggregateFunction.count, distinct); 871 } 872 873 /++ 874 Short-hand helper function for SELECT MAX(…) 875 +/ 876 SelectExpression max(Distinct distinct = Distinct.no)(string column) 877 { 878 return SelectExpression(col(column), AggregateFunction.max, distinct); 879 } 880 881 /++ 882 Short-hand helper function for SELECT MIN(…) 883 +/ 884 SelectExpression min(Distinct distinct = Distinct.no)(string column) 885 { 886 return SelectExpression(col(column), AggregateFunction.min, distinct); 887 } 888 889 /++ 890 Short-hand helper function for SELECT SUM(…) 891 +/ 892 SelectExpression sum(Distinct distinct = Distinct.no)(string column) 893 { 894 return SelectExpression(col(column), AggregateFunction.sum, distinct); 895 } 896 897 /++ 898 Short-hand helper function for SELECT GROUP_CONCAT(…) 899 +/ 900 SelectExpression group_concat(Distinct distinct = Distinct.no)(string column) 901 { 902 return SelectExpression(col(column), AggregateFunction.groupConcat, distinct); 903 } 904 905 /++ 906 SELECT Query abstraction 907 +/ 908 struct Select 909 { 910 Query query; 911 const(SelectExpression)[] columns; 912 } 913 914 /++ 915 Creates an abstracted SELECT query selecting the specified columns. 916 917 --- 918 Select mtHigherThan2k = table("mountain").qb 919 .where("height", '>', 2000) 920 .select("id", "height") 921 ; 922 923 Select knownMountains = table("mountain").qb 924 .select(count("*")) 925 ; 926 927 Select maxHeight = table("mountain").qb 928 .select(max("height")) 929 ; 930 --- 931 932 Params: 933 columns = Columns to select; either as strings or [SelectExpression]s 934 +/ 935 Select select(ColumnV...)(Query from, ColumnV columns) 936 { 937 static if (columns.length == 0) 938 return Select(from, [SelectExpression(col("*"))]); 939 else 940 { 941 auto data = new SelectExpression[](columns.length); 942 943 static foreach (idx, col; columns) 944 { 945 static if (is(typeof(col) == string)) 946 data[idx] = SelectExpression(oceandrift.db.dbal.v4.column(col)); 947 else static if (is(typeof(col) == Column)) 948 { 949 data[idx] = SelectExpression(col); 950 } 951 else static if (is(typeof(col) == SelectExpression)) 952 data[idx] = col; 953 else 954 { 955 enum colType = typeof(col).stringof; 956 static assert( 957 0, 958 "Column identifiers must be strings, but type of parameter " ~ idx.to!string ~ " is `" ~ colType ~ '`' 959 ); 960 } 961 } 962 963 return Select(from, data); 964 } 965 } 966 967 // -- Update 968 969 /++ 970 UPDATE query abstraction 971 +/ 972 struct Update 973 { 974 Query query; 975 const(string)[] columns; 976 } 977 978 /++ 979 Creates an abstracted UPDATE query for updating the specified columns 980 981 --- 982 Update updateMountainNo90 = table("mountain").qb 983 .where("id", '=', 90) 984 .update("height") 985 ; 986 987 Update updateStats = table("mountain").qb.update( 988 "visitors", 989 "times_summit_reached", 990 "updated_at" 991 ); 992 --- 993 +/ 994 Update update(Query query, const(string)[] columns) 995 { 996 return Update(query, columns); 997 } 998 999 /// ditto 1000 Update update(Columns...)(Query query, Columns columns) 1001 { 1002 auto data = new string[](columns.length); 1003 1004 static foreach (idx, col; columns) 1005 { 1006 static if (is(typeof(col) == string)) 1007 data[idx] = col; 1008 else 1009 { 1010 enum colType = typeof(col).stringof; 1011 static assert( 1012 is(typeof(col) == string) || is(typeof(col) == SelectExpression), 1013 "Column identifiers must be strings, but type of parameter " ~ idx.to!string ~ " is `" ~ colType ~ '`' 1014 ); 1015 } 1016 } 1017 1018 return query.update(data); 1019 } 1020 1021 // -- Insert 1022 1023 /++ 1024 INSERT query abstraction 1025 +/ 1026 struct Insert 1027 { 1028 Table table; 1029 const(string)[] columns; 1030 uint rowCount = 1; 1031 } 1032 1033 /++ 1034 Creates an abstracted INSERT query for inserting row(s) into the specified table 1035 filling the passed columns 1036 1037 --- 1038 Insert insertMountain = table("mountain").insert( 1039 "name", 1040 "location", 1041 "height" 1042 ); 1043 // INSERT INTO "mountain"("name", "location", "height") VALUES (?, ?, ?) 1044 1045 Insert insert3MountainsAtOnce = table("mountain") 1046 .insert( 1047 "name", 1048 "height" 1049 ) 1050 .times(3) 1051 ; 1052 // INSERT INTO "mountain"("name", "height") VALUES (?, ?), (?, ?), (?, ?) 1053 --- 1054 1055 See_Also: 1056 Use [times] to create a query for inserting multiple rows at once. 1057 +/ 1058 Insert insert(Table table, const(string)[] columns) 1059 { 1060 return Insert(table, columns); 1061 } 1062 1063 /// ditto 1064 Insert insert(Columns...)(Table table, Columns columns) 1065 { 1066 auto data = new string[](columns.length); 1067 1068 static foreach (idx, col; columns) 1069 { 1070 static if (is(typeof(col) == string)) 1071 data[idx] = col; 1072 else 1073 { 1074 enum colType = typeof(col).stringof; 1075 static assert( 1076 is(typeof(col) == string) || is(typeof(col) == SelectExpression), 1077 "Column identifiers must be strings, but type of parameter " ~ idx.to!string ~ " is `" ~ colType ~ '`' 1078 ); 1079 } 1080 } 1081 1082 return table.insert(data); 1083 } 1084 1085 /++ 1086 Specifies how many rows to INSERT a once 1087 1088 e.g. `INSERT INTO "mountain"("name", "height") VALUES (?, ?), (?, ?)` 1089 +/ 1090 Insert times(Insert insert, const uint rows) 1091 { 1092 insert.rowCount = rows; 1093 return insert; 1094 } 1095 1096 // -- Delete 1097 1098 /++ 1099 DELETE query abstraction 1100 +/ 1101 struct Delete 1102 { 1103 Query query; 1104 } 1105 1106 /++ 1107 Creates an abstracted DELETE query from the specified query 1108 1109 --- 1110 Delete deleteMountainsWithUnknownHeight = table("mountain").qb 1111 .where("height", ComparisonOperator.isNull) 1112 .delete_() 1113 ; 1114 // DELETE FROM "mountain" WHERE "height" IS NULL 1115 --- 1116 +/ 1117 Delete delete_(Query query) 1118 { 1119 return Delete(query); 1120 } 1121 1122 // -- Query building 1123 1124 private struct _PlaceholdersMeta 1125 { 1126 int where; 1127 } 1128 1129 /// 1130 public alias PlaceholdersMeta = const(_PlaceholdersMeta); 1131 1132 private struct _PreSets 1133 { 1134 DBValue[int] where; 1135 Nullable!ulong limit; 1136 Nullable!ulong limitOffset; 1137 } 1138 1139 /// 1140 public alias PreSets = const(_PreSets); 1141 1142 private struct _BuiltQuery 1143 { 1144 string sql; 1145 PlaceholdersMeta placeholders; 1146 PreSets preSets; 1147 } 1148 1149 /++ 1150 Built query as generated by a QueryBuilder 1151 1152 or in other words: the result of query building 1153 1154 --- 1155 // Construct an abstract query 1156 Select query = table("mountain").qb.select(count("*")); 1157 1158 // Build the query with the query builder of your choice (e.g. [oceandrift.db.sqlite3.SQLite3] when using SQLite3) 1159 BuiltQuery builtQuery = QueryBuilder.build(query); 1160 1161 // Prepare a statement from your query by calling [prepareBuiltQuery] 1162 Statement stmt = db.prepareBuiltQuery(builtQuery); 1163 1164 stmt.execute(); 1165 // … 1166 --- 1167 1168 --- 1169 // More idiomatic way using UFCS: 1170 BuiltQuery builtQuery = table("mountain").qb 1171 .select(count("*")) 1172 .build!QueryCompiler() 1173 ; 1174 1175 // Query building works during compile-time as well(!): 1176 enum BuiltQuery bq = table("mountain").qb 1177 .select(count("*")) 1178 .build!QueryCompiler() 1179 ; 1180 --- 1181 +/ 1182 public alias BuiltQuery = const(_BuiltQuery); 1183 1184 /++ 1185 Determines whether `T` is a valid SQL “Query Compiler” implementation 1186 1187 --- 1188 struct MyDatabaseDriver 1189 { 1190 // […] 1191 1192 @safe pure: 1193 static BuiltQuery build(const Select selectQuery); 1194 static BuiltQuery build(const Update updateQuery); 1195 static BuiltQuery build(const Insert insertQuery); 1196 static BuiltQuery build(const Delete deleteQuery); 1197 } 1198 1199 static assert(isQueryCompiler!MyDatabaseDriver); 1200 --- 1201 +/ 1202 enum bool isQueryCompiler(T) = 1203 // dfmt off 1204 ( 1205 is(ReturnType!(() => T.build(Select())) == BuiltQuery) 1206 && is(ReturnType!(() => T.build(Update())) == BuiltQuery) 1207 && is(ReturnType!(() => T.build(Insert())) == BuiltQuery) 1208 && is(ReturnType!(() => T.build(Delete())) == BuiltQuery) 1209 ); 1210 // dfmt on 1211 1212 /++ 1213 Builds the passed query through the provided QueryCompiler 1214 (UFCS helper function) 1215 +/ 1216 BuiltQuery build(QueryCompiler)(Select q) if (isQueryCompiler!QueryCompiler) 1217 { 1218 pragma(inline, true); 1219 return QueryCompiler.build(q); 1220 } 1221 1222 /// ditto 1223 BuiltQuery build(QueryCompiler)(Update q) if (isQueryCompiler!QueryCompiler) 1224 { 1225 pragma(inline, true); 1226 return QueryCompiler.build(q); 1227 } 1228 1229 /// ditto 1230 BuiltQuery build(QueryCompiler)(Insert q) if (isQueryCompiler!QueryCompiler) 1231 { 1232 pragma(inline, true); 1233 return QueryCompiler.build(q); 1234 } 1235 1236 /// ditto 1237 BuiltQuery build(QueryCompiler)(Delete q) if (isQueryCompiler!QueryCompiler) 1238 { 1239 pragma(inline, true); 1240 return QueryCompiler.build(q); 1241 }