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 }