1 /**
2 Copyright: Copyright (c) 2018-2019, Joakim Brännström. All rights reserved.
3 License: MIT
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This module contains an AST representation of a database query. The intention
7 is to encode the structure in the types so errors in the SQL statement are
8 detected at compile time.
9 
10 # Grammar
11 
12 The grammar is expressed in PEG form using the same terminology as the dub
13 package Pegged. It has not been verified to be correct so may contain errors.
14 
15 This is simplified to only contain those parts of the grammar that is needed to
16 pretty print constructed SQL queries. It is not intended to be a grammar that
17 can correctly parse SQL. Thus it contains "blobs of data" that the grammar do
18 not care about. The important part is to keep the structure.  Where to insert
19 "blobs".
20 
21 A secondary purpose is to give some compile time guarantees of the constructed
22 SQL queries. If it compiles it is reasonably assured to be correct.
23 
24 When not necessary for pretty printing as a valid SQL blanks and such are
25 skipped in the grammar definition.
26 
27 ```sql
28 SQL         <- blank* Query (spacing / eoi)
29 
30 Query       <- Select
31 
32 # --- SELECT START ---
33 Select      <- "SELECT" :blank+ ResultColumns :blank+ From? Where? GroupBy? (Window :blank+)? OrderBy? Values? Limit?
34 
35 ResultColumns       <- ResultColumn ("," ResultColumn)*
36 ResultColumn        <- Star / ResultColumnExpr
37 ResultColumnExpr    <- Query / Blob
38 
39 From    <- :blank+ "FROM" :blank+ (TableOrSubQueries / Blob)
40 Where   <- :blank+ "WHERE" :blank+ WhereExpr*
41 GroupBy <- :blank+ "GROUP BY" :blank+ Expr ("," Expr)* (:blank+ "HAVING" Expr)?
42 Window  <- Blob
43 
44 WhereExpr   <- Expr (:blank+ WhereOp :blank+ Expr)?
45 WhereOp     <- "AND" / "OR"
46 
47 OrderBy         <- :blank+ "ORDER BY" :blank+ OrderingTerm ("," OrderingTerm)
48 OrderingTerm    <- Expr :blank+ OrderingTermSort?
49 OrderingTermSort <- "ASC" / "DESC" / ""
50 
51 Limit <- "LIMIT" :blank+ Expr :blank+ (("OFFSET" :blank+ Expr) / ("," Expr))?
52 
53 TableOrSubQueries       <- TableOrQuery ("," TableOrSubQuery)*
54 TableOrSubQuery         <- TableOrSubQuerySelect / ("(" TableOrSubQueries ")") / (TableRef Blob*) / Blob
55 TableOrSubQuerySelect   <- "(" Select ")" TableAlias?
56 
57 # --- SELECT END ---
58 
59 # --- INSERT START ---
60 Insert          <- InsertOpt :blank+ "INTO" :blank+ TableRef TableAlias? InsertColumns? InsertValues
61 InsertOpt       <- "INSERT" / "REPLACE" / "INSERT OR REPLACE" / "INSERT OR ROLLBACK" / "INSERT OR ABORT" / "INSERT OR FAIL" / "INSERT OR IGNORE"
62 InsertColumns    <- :blank+ "(" ColumnName ("," ColumnName)* ")"
63 InsertValues    <- :blank+ (Values / Select / "DEFAULT VALUES")
64 ColumnName      <- identifier
65 
66 # --- INSERT END ---
67 
68 # --- DELETE START ---
69 Delete          <- "DELETE FROM" :blank+ TableRef Where?
70 # --- DELETE END ---
71 
72 # Reference an existing table
73 TableRef            <- SchemaName? TableName TableAlias?
74 
75 Values  <- :blank+ "VALUES" "(" Value ")" ("(" Value ")")*
76 Value   <- Expr ("," Expr)*
77 
78 TableAlias  <- :blank+ "AS" :blank+ identifier
79 
80 Expr        <- Blob
81 # Not representable in the grammar because it can be anything without a formal
82 # terminator. Its purpose is to be an injection point of user data.
83 Blob        <- ""
84 
85 SchemaName  <- identifier "."
86 TableName   <- identifier
87 Star        <- "*"
88 ```
89 
90 ## Grammar Encoding
91 
92  * `SumType` is used when possible.
93  * `None` is the first member of a `SumType` when the node is optional.
94  * Literals are used as is.
95  * All nodes have a `toString`.
96 */
97 module miniorm.query_ast;
98 
99 import std.array : empty;
100 import std.conv : to;
101 import std.format : formattedWrite, format;
102 import std.meta : AliasSeq;
103 import std.range.primitives : put, isOutputRange;
104 import std.traits : TemplateArgsOf;
105 
106 import sumtype;
107 
108 @safe:
109 
110 /// A SQL statement.
111 struct Sql {
112     Query query;
113     alias query this;
114 
115     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
116         query.toString(w);
117         put(w, ";");
118     }
119 
120     string toString()() {
121         import std.array : appender;
122 
123         auto app = appender!string();
124         this.toString(app);
125         return app.data;
126     }
127 }
128 
129 /** A SQL query.
130  *
131  * The differents between Sql and Query is that this may be nested in other nodes.
132  */
133 struct Query {
134     SumType!(Select, Insert, Delete) value;
135     alias value this;
136 
137     static foreach (T; TemplateArgsOf!(typeof(value))) {
138         this(T n) {
139             value = typeof(value)(n);
140         }
141     }
142 
143     mixin ToStringSumType!value;
144 }
145 
146 // #########################################################################
147 /// # Select START
148 // #########################################################################
149 
150 /// A Select statement.
151 struct Select {
152     ResultColumns columns;
153 
154     /// Optional parts of the statement. At least one must in the end be active.
155     SumType!(None, From) from;
156     SumType!(None, Where) where;
157     //SumType!(None, Window) window_;
158     SumType!(None, OrderBy) orderBy;
159     SumType!(None, Limit) limit;
160 
161     mixin(makeAssign!(typeof(this))([
162                 "columns", "from", "where", "orderBy", "limit"
163             ]));
164 
165     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
166         put(w, "SELECT ");
167         columns.toString(w);
168 
169         put(w, " ");
170 
171         // TODO: add an assert that at least one of opts is not None?
172         from.match!((None v) {}, (From v) { v.toString(w); });
173         where.match!((None v) {}, (Where v) { v.toString(w); });
174         //window.match!((None v) {}, (Window v) { v.toString(w); });
175         orderBy.match!((None v) {}, (OrderBy v) { v.toString(w); });
176         limit.match!((None v) {}, (Limit v) { v.toString(w); });
177     }
178 }
179 
180 struct ResultColumns {
181     ResultColumn required;
182     ResultColumn[] optional;
183 
184     this(ResultColumn r, ResultColumn[] o = null) {
185         required = r;
186         optional = o;
187     }
188 
189     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
190         required.toString(w);
191         foreach (v; optional) {
192             put(w, ",");
193             v.toString(w);
194         }
195     }
196 }
197 
198 struct ResultColumn {
199     SumType!(Star, ResultColumnExpr) value;
200     mixin ToStringSumType!(value);
201     mixin(makeCtor!(typeof(value))("value"));
202     mixin(makeAssign!(typeof(this))(["value"]));
203 }
204 
205 struct ResultColumnExpr {
206     SumType!(Blob, Query*) value;
207     mixin ToStringSumType!value;
208     mixin(makeCtor!(typeof(value))("value"));
209     mixin(makeAssign!(typeof(this))(["value"]));
210 }
211 
212 struct From {
213     SumType!(TableOrSubQueries, Blob) value;
214     alias value this;
215     mixin(makeCtor!(typeof(value))("value"));
216     mixin(makeAssign!(typeof(this))(["value"]));
217 
218     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
219         put(w, "FROM ");
220         value.match!((TableOrSubQueries v) => v.toString(w), (Blob v) {
221             v.toString(w);
222         });
223     }
224 }
225 
226 struct Where {
227     SumType!(None, WhereExpr) value;
228     alias value this;
229     mixin(makeCtor!(typeof(value))("value"));
230     mixin(makeAssign!(typeof(this))(["value"]));
231 
232     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
233         // TODO: should it quote strings?
234         put(w, " WHERE ");
235         value.match!((None v) {}, (WhereExpr v) => v.toString(w));
236     }
237 }
238 
239 struct WhereExpr {
240     static struct Opt {
241         WhereOp op;
242         Expr expr;
243     }
244 
245     Expr required;
246     Opt[] optional;
247 
248     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
249         required.toString(w);
250         foreach (v; optional) {
251             put(w, " ");
252             put(w, v.op.to!string);
253             put(w, " ");
254             v.expr.toString(w);
255         }
256     }
257 }
258 
259 enum WhereOp {
260     AND,
261     OR
262 }
263 
264 struct TableOrSubQueries {
265     TableOrQuery required;
266     TableOrQuery[] optional;
267     mixin(makeAssign!(typeof(this))(["required", "optional"]));
268 
269     ///
270     this(TableOrQuery r, TableOrQuery[] o = null) @safe pure nothrow @nogc {
271         required = r;
272         optional = o;
273     }
274 
275     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
276         required.toString(w);
277         foreach (v; optional) {
278             put(w, ",");
279             v.toString(w);
280         }
281     }
282 }
283 
284 struct TableOrQuery {
285     SumType!(TableOrSubQuerySelect*, TableOrSubQueries*, TableRef, Blob) value;
286     alias value this;
287     mixin(makeCtor!(typeof(value))("value"));
288     mixin(makeAssign!(typeof(this))(["value"]));
289 
290     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
291         value.match!((TableOrSubQuerySelect* v) { v.toString(w); }, (TableOrSubQueries* v) {
292             put(w, "(");
293             v.toString(w);
294             put(w, ")");
295         }, (TableRef v) { v.toString(w); }, (Blob v) { v.toString(w); });
296     }
297 }
298 
299 struct TableOrSubQuerySelect {
300     Select select;
301     TableAlias alias_;
302     mixin(makeAssign!(typeof(this))(["select", "alias_"]));
303 
304     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
305         put(w, "(");
306         select.toString(w);
307         put(w, ")");
308         alias_.toString(w);
309     }
310 }
311 
312 struct OrderBy {
313     OrderingTerm required;
314     OrderingTerm[] optional;
315     mixin(makeAssign!(typeof(this))(["required", "optional"]));
316 
317     this(typeof(required) r, typeof(optional) o = null) @safe pure nothrow @nogc {
318         required = r;
319         optional = o;
320     }
321 
322     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
323         put(w, " ORDER BY ");
324         required.toString(w);
325         foreach (v; optional) {
326             put(w, ",");
327             v.toString(w);
328         }
329     }
330 }
331 
332 struct OrderingTerm {
333     SumType!(None, Blob) expr;
334     SumType!(None, OrderingTermSort) sortTerm;
335     mixin(makeCtor!(typeof(expr))("expr"));
336     mixin(makeAssign!(typeof(this))(["expr", "sortTerm"]));
337 
338     this(Blob expr, OrderingTermSort sortTerm) @safe pure nothrow @nogc {
339         this.expr = expr;
340         this.sortTerm = sortTerm;
341     }
342 
343     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
344         expr.match!((None n) {}, (Blob v) { v.toString(w); });
345         sortTerm.match!((None n) {}, (OrderingTermSort v) {
346             put(w, " ");
347             put(w, v.to!string);
348         });
349     }
350 }
351 
352 enum OrderingTermSort {
353     ASC,
354     DESC,
355 }
356 
357 struct Limit {
358     SumType!(None, Blob) expr;
359     SumType!(None, LimitOffset, Blob) optional;
360     mixin(makeCtor!(typeof(expr))("expr"));
361     mixin(makeAssign!(typeof(this))(["expr", "optional"]));
362 
363     this(Blob expr, LimitOffset l) @safe pure nothrow @nogc {
364         this.expr = expr;
365         this.optional = l;
366     }
367 
368     this(Blob expr, Blob l) @safe pure nothrow @nogc {
369         this.expr = expr;
370         this.optional = l;
371     }
372 
373     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
374         put(w, " LIMIT ");
375         expr.match!((None n) {}, (Blob v) { v.toString(w); });
376         optional.match!((None n) {}, (LimitOffset v) {
377             put(w, " OFFSET ");
378             v.toString(w);
379         }, (Blob v) { put(w, ","); v.toString(w); });
380     }
381 }
382 
383 struct LimitOffset {
384     Blob expr;
385     alias expr this;
386 }
387 
388 // #########################################################################
389 /// # Select END
390 // #########################################################################
391 
392 // #########################################################################
393 /// # Insert START
394 // #########################################################################
395 
396 struct Insert {
397     /// Type of operation to perform.
398     InsertOpt opt;
399     /// Table to operate on.
400     TableRef table;
401     TableAlias alias_;
402     ///
403     InsertColumns columns;
404     ///
405     InsertValues values;
406 
407     ///
408     this(InsertOpt opt, TableRef tbl) @safe pure nothrow @nogc {
409         this.opt = opt;
410         this.table = tbl;
411     }
412 
413     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
414         final switch (opt) with (InsertOpt) {
415         case Insert:
416             put(w, "INSERT");
417             break;
418         case Replace:
419             put(w, "REPLACE");
420             break;
421         case InsertOrReplace:
422             put(w, "INSERT OR REPLACE");
423             break;
424         case InsertOrRollback:
425             put(w, "INSERT OR ROLLBACK");
426             break;
427         case InsertOrAbort:
428             put(w, "INSERT OR ABORT");
429             break;
430         case InsertOrFail:
431             put(w, "INSERT OR FAIL");
432             break;
433         case InsertOrIgnore:
434             put(w, "INSERT OR IGNORE");
435             break;
436         }
437         put(w, " INTO ");
438         table.toString(w);
439         alias_.toString(w);
440         columns.toString(w);
441         values.toString(w);
442     }
443 }
444 
445 struct InsertColumns {
446     SumType!(None, ColumnNames) value;
447     mixin(makeCtor!(typeof(value))("value"));
448     mixin(makeAssign!(typeof(this))(["value"]));
449 
450     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
451         value.match!((None v) {}, (ColumnNames v) => v.toString(w));
452     }
453 }
454 
455 struct ColumnNames {
456     ColumnName required;
457     ColumnName[] optional;
458 
459     this(ColumnName r, ColumnName[] o = null) {
460         required = r;
461         optional = o;
462     }
463 
464     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
465         put(w, " ('");
466         required.toString(w);
467         foreach (v; optional) {
468             put(w, "','");
469             v.toString(w);
470         }
471         put(w, "')");
472     }
473 }
474 
475 alias ColumnName = Blob;
476 
477 struct InsertValues {
478     SumType!(None, Select, Values, InsertDefaultValue) value;
479     mixin(makeCtor!(typeof(value))("value"));
480 
481     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
482         value.match!((None v) {}, (Select v) { put(w, " "); v.toString(w); }, (Values v) {
483             v.toString(w);
484         }, (InsertDefaultValue v) { put(w, " "); v.toString(w); });
485     }
486 }
487 
488 struct Values {
489     Value required;
490     Value[] optional;
491 
492     this(Value r, Value[] o = null) {
493         required = r;
494         optional = o;
495     }
496 
497     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
498         put(w, " VALUES (");
499         required.toString(w);
500         put(w, ")");
501         foreach (v; optional) {
502             put(w, ",(");
503             v.toString(w);
504             put(w, ")");
505         }
506     }
507 }
508 
509 struct Value {
510     Expr required;
511     Expr[] optional;
512 
513     this(Expr r, Expr[] o = null) {
514         required = r;
515         optional = o;
516     }
517 
518     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
519         required.toString(w);
520         foreach (v; optional) {
521             put(w, ",");
522             v.toString(w);
523         }
524     }
525 }
526 
527 alias InsertDefaultValue = Constant!"DEFAULT VALUES";
528 
529 /// Based on those that are valid in SQLite.
530 enum InsertOpt {
531     Insert,
532     Replace,
533     InsertOrReplace,
534     InsertOrRollback,
535     InsertOrAbort,
536     InsertOrFail,
537     InsertOrIgnore,
538 }
539 
540 // #########################################################################
541 /// # Insert END
542 // #########################################################################
543 
544 // #########################################################################
545 /// # Delete START
546 // #########################################################################
547 
548 struct Delete {
549     TableRef table;
550     SumType!(None, Where) where;
551 
552     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
553         put(w, "DELETE FROM ");
554         table.toString(w);
555         where.match!((None v) {}, (Where v) { v.toString(w); });
556     }
557 }
558 
559 // #########################################################################
560 /// # Delete END
561 // #########################################################################
562 
563 struct TableAlias {
564     string value;
565     alias value this;
566 
567     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
568         if (value.empty)
569             return;
570         put(w, " AS ");
571         put(w, value);
572     }
573 }
574 
575 /// Reference to a table with options to reference another schema and/or create an alias.
576 struct TableRef {
577     SumType!(None, SchemaName) schemaName;
578     string tableName;
579     SumType!(None, TableAlias) tableAlias;
580 
581     this(SchemaName schema, string name, TableAlias alias_) {
582         schemaName = schema;
583         tableName = name;
584         tableAlias = alias_;
585     }
586 
587     /// A ref to a table that rename it via an "AS" to `alias_`.
588     this(string name, TableAlias alias_) {
589         tableName = name;
590         tableAlias = alias_;
591     }
592 
593     /// A simple ref to a table.
594     this(string tableName) {
595         this.tableName = tableName;
596     }
597 
598     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
599         schemaName.match!((auto ref v) => v.toString(w));
600         put(w, tableName);
601         tableAlias.match!((auto ref v) => v.toString(w));
602     }
603 }
604 
605 struct SchemaName {
606     string value;
607     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
608         put(w, value);
609         put(w, ".");
610     }
611 }
612 
613 struct Blob {
614     string value;
615 
616     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
617         put(w, value);
618     }
619 }
620 
621 alias Expr = Blob;
622 alias None = Constant!(string.init);
623 alias Star = Constant!"*";
624 alias Window = Blob;
625 
626 /// A node representing a constant value.
627 struct Constant(string s) {
628     string value = s;
629     alias value this;
630 
631     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
632         put(w, value);
633     }
634 }
635 
636 private:
637 
638 /// Create a match that calls `.toString(w)` on all matches of the SumType.
639 mixin template ToStringSumType(alias member) {
640     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
641         static string autoMatch(alias member)() {
642             string rval = q{%s.match!(}.format(member.stringof);
643             static foreach (T; TemplateArgsOf!(typeof(member))) {
644                 rval ~= q{(%s v) => v.toString(w),}.format(T.stringof);
645             }
646             rval ~= ");";
647             return rval;
648         }
649 
650         mixin(autoMatch!member);
651     }
652 }
653 
654 string makeBuild(ArgT, string member, string funcName)() {
655     string rval = q{auto %s(%s v)}.format(funcName, ArgT.stringof);
656     rval ~= "{";
657     rval ~= member ~ " = v;";
658     rval ~= q{return this;};
659     rval ~= "}";
660     return rval;
661 }
662 
663 /// ctors for all template arguments of member.
664 string makeCtor(SumT)(string var) {
665     string rval;
666     static foreach (T; TemplateArgsOf!SumT) {
667         rval ~= q{this(%1$s n) @safe pure nothrow {
668             this.%2$s = n;
669         }}.format(T.stringof, var);
670         rval ~= "\n";
671     }
672     return rval;
673 }
674 
675 /// an opAssign that assign to `var` of type `SumT`.
676 string makeAssign(T)(string[] members) {
677     string rval = format(`void opAssign(%1$s rhs) @trusted pure nothrow @nogc {`, T.stringof);
678     foreach (m; members) {
679         rval ~= format("%1$s = rhs.%1$s;", m);
680     }
681     rval ~= "}\n";
682     return rval;
683 }
684 
685 /// Returns: a string that can be mixed in to create a setter for the member
686 mixin template makeBuilder(members...) {
687     static string buildMember(alias member)() {
688         enum memberStr = member.stringof;
689         static assert(memberStr[$ - 1] == '_', "member must end with '_': " ~ memberStr);
690 
691         enum Type = typeof(member).stringof;
692         string rval = q{auto %s(%s v)}.format(member.stringof[0 .. $ - 1], Type);
693         rval ~= "{";
694         rval ~= memberStr ~ " = v;";
695         rval ~= q{return this;};
696         rval ~= "}";
697         return rval;
698     }
699 
700     static foreach (member; members) {
701         mixin(buildMember!member);
702     }
703 }
704 
705 version (unittest) {
706     import unit_threaded.assertions : shouldEqual;
707 }
708 
709 // TODO: investigate why this is needed to be system.
710 @system:
711 
712 @("shall convert a query at compile time to SQL")
713 unittest {
714     enum q = Select().Query.Sql.toString;
715     q.shouldEqual("SELECT * ;");
716 }
717 
718 @("shall convert a Select using From to SQL")
719 unittest {
720     // arrange
721     Select qblob, qtblRef, q;
722     // act
723     qblob.from = Blob("foo").From;
724     qtblRef.from = TableOrSubQueries(TableOrQuery(TableRef("foo"))).From;
725     // assert
726     immutable expected = "SELECT * FROM foo;";
727     foreach (s; [qblob, qtblRef])
728         s.Query.Sql.toString.shouldEqual(expected);
729 }
730 
731 @("shall convert a Select using a subquery in FROM to SQL")
732 unittest {
733     // arrange
734     Select qblob, qAlias, qRef, qsubBlob;
735     // act
736     qsubBlob.from = Blob("foo I dance").From;
737     qblob.from = TableOrSubQueries(TableOrQuery(new TableOrSubQuerySelect(qsubBlob))).From;
738     qAlias.from = TableOrSubQueries(TableOrQuery(new TableOrSubQuerySelect(qsubBlob,
739             TableAlias("bar")))).From;
740     qRef.from = TableOrSubQueries(TableOrQuery(new TableOrSubQueries(TableRef("foo")
741             .TableOrQuery, [TableRef("smurf").TableOrQuery]))).From;
742     // assert
743     // a subquery as a blob that should be represented as-is.
744     qblob.Query.Sql.toString.shouldEqual("SELECT * FROM (SELECT * FROM foo I dance);");
745     // a subquery as a named select.
746     qAlias.Query.Sql.toString.shouldEqual("SELECT * FROM (SELECT * FROM foo I dance) AS bar;");
747     // multiple table refs.
748     qRef.Query.Sql.toString.shouldEqual("SELECT * FROM (foo,smurf);");
749 }
750 
751 @("shall convert a Select using an OrderBy to SQL")
752 unittest {
753     // arrange
754     Select q;
755     q.from = Blob("foo").From;
756     // act
757     q.orderBy = OrderBy(OrderingTerm(Blob("bar")));
758     // assert
759     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo ORDER BY bar;");
760 }
761 
762 @("shall convert a Select using Where to SQL")
763 unittest {
764     // arrange
765     Select q;
766     // act
767     q.from = Blob("foo").From;
768     q.where = WhereExpr(Expr("foo = bar"), [
769             WhereExpr.Opt(WhereOp.OR, Expr("batman NOT NULL"))
770             ]).Where;
771     // assert
772     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo WHERE foo = bar OR batman NOT NULL;");
773 }
774 
775 @("shall convert an Insert using default values to SQL")
776 unittest {
777     // act
778     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
779     q.values = InsertValues(InsertDefaultValue.init);
780     // assert
781     q.Query.Sql.toString.shouldEqual("INSERT INTO foo DEFAULT VALUES;");
782 }
783 
784 @("shall convert an Insert using specific values to SQL")
785 unittest {
786     // act
787     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
788     q.values = InsertValues(Values(Value(Expr("1"), [Expr("2")]), [
789                 Value(Expr("4"), [Expr("5")])
790             ]));
791     // assert
792     q.Query.Sql.toString.shouldEqual("INSERT INTO foo VALUES (1,2),(4,5);");
793 }
794 
795 @("shall convert an Insert using select stmt to SQL")
796 unittest {
797     // act
798     Select s;
799     s.from = Blob("bar").From;
800     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
801     q.values = InsertValues(s);
802     // assert
803     q.Query.Sql.toString.shouldEqual("INSERT INTO foo SELECT * FROM bar;");
804 }
805 
806 @("shall convert a Select with a limit to SQL")
807 unittest {
808     // arrange
809     Select q;
810     q.from = Blob("foo").From;
811     // act
812     q.limit = Limit(Blob("10"), LimitOffset(Blob("42")));
813     // assert
814     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo LIMIT 10 OFFSET 42;");
815 }