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     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
162         put(w, "SELECT ");
163         columns.toString(w);
164 
165         put(w, " ");
166 
167         // TODO: add an assert that at least one of opts is not None?
168         from.match!((None v) {}, (From v) { v.toString(w); });
169         where.match!((None v) {}, (Where v) { v.toString(w); });
170         //window.match!((None v) {}, (Window v) { v.toString(w); });
171         orderBy.match!((None v) {}, (OrderBy v) { v.toString(w); });
172         limit.match!((None v) {}, (Limit v) { v.toString(w); });
173     }
174 }
175 
176 struct ResultColumns {
177     ResultColumn required;
178     ResultColumn[] optional;
179 
180     this(ResultColumn r, ResultColumn[] o = null) {
181         required = r;
182         optional = o;
183     }
184 
185     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
186         required.toString(w);
187         foreach (v; optional) {
188             put(w, ",");
189             v.toString(w);
190         }
191     }
192 }
193 
194 struct ResultColumn {
195     SumType!(Star, ResultColumnExpr) value;
196     mixin ToStringSumType!(value);
197     mixin(makeCtor!(typeof(value))("value"));
198 }
199 
200 struct ResultColumnExpr {
201     SumType!(Blob, Query*) value;
202     mixin ToStringSumType!value;
203     mixin(makeCtor!(typeof(value))("value"));
204 }
205 
206 struct From {
207     SumType!(TableOrSubQueries, Blob) value;
208     alias value this;
209     mixin(makeCtor!(typeof(value))("value"));
210 
211     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
212         put(w, "FROM ");
213         value.match!((TableOrSubQueries v) => v.toString(w), (Blob v) {
214             v.toString(w);
215         });
216     }
217 }
218 
219 struct Where {
220     SumType!(None, WhereExpr) value;
221     alias value this;
222     mixin(makeCtor!(typeof(value))("value"));
223 
224     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
225         // TODO: should it quote strings?
226         put(w, " WHERE ");
227         value.match!((None v) {}, (WhereExpr v) => v.toString(w));
228     }
229 }
230 
231 struct WhereExpr {
232     static struct Opt {
233         WhereOp op;
234         Expr expr;
235     }
236 
237     Expr required;
238     Opt[] optional;
239 
240     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
241         required.toString(w);
242         foreach (v; optional) {
243             put(w, " ");
244             put(w, v.op.to!string);
245             put(w, " ");
246             v.expr.toString(w);
247         }
248     }
249 }
250 
251 enum WhereOp {
252     AND,
253     OR
254 }
255 
256 struct TableOrSubQueries {
257     TableOrQuery required;
258     TableOrQuery[] optional;
259 
260     ///
261     this(TableOrQuery r, TableOrQuery[] o = null) @safe pure nothrow @nogc {
262         required = r;
263         optional = o;
264     }
265 
266     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
267         required.toString(w);
268         foreach (v; optional) {
269             put(w, ",");
270             v.toString(w);
271         }
272     }
273 }
274 
275 struct TableOrQuery {
276     SumType!(TableOrSubQuerySelect*, TableOrSubQueries*, TableRef, Blob) value;
277     alias value this;
278     mixin(makeCtor!(typeof(value))("value"));
279 
280     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
281         value.match!((TableOrSubQuerySelect* v) { v.toString(w); }, (TableOrSubQueries* v) {
282             put(w, "(");
283             v.toString(w);
284             put(w, ")");
285         }, (TableRef v) { v.toString(w); }, (Blob v) { v.toString(w); });
286     }
287 }
288 
289 struct TableOrSubQuerySelect {
290     Select select;
291     TableAlias alias_;
292 
293     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
294         put(w, "(");
295         select.toString(w);
296         put(w, ")");
297         alias_.toString(w);
298     }
299 }
300 
301 struct OrderBy {
302     OrderingTerm required;
303     OrderingTerm[] optional;
304 
305     this(typeof(required) r, typeof(optional) o = null) @safe pure nothrow @nogc {
306         required = r;
307         optional = o;
308     }
309 
310     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
311         put(w, " ORDER BY ");
312         required.toString(w);
313         foreach (v; optional) {
314             put(w, ",");
315             v.toString(w);
316         }
317     }
318 }
319 
320 struct OrderingTerm {
321     SumType!(None, Blob) expr;
322     SumType!(None, OrderingTermSort) sortTerm;
323 
324     mixin(makeCtor!(typeof(expr))("expr"));
325 
326     this(Blob expr, OrderingTermSort sortTerm) @safe pure nothrow @nogc {
327         this.expr = expr;
328         this.sortTerm = sortTerm;
329     }
330 
331     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
332         expr.match!((None n) {}, (Blob v) { v.toString(w); });
333         sortTerm.match!((None n) {}, (OrderingTermSort v) {
334             put(w, " ");
335             put(w, v.to!string);
336         });
337     }
338 }
339 
340 enum OrderingTermSort {
341     ASC,
342     DESC,
343 }
344 
345 struct Limit {
346     SumType!(None, Blob) expr;
347     SumType!(None, LimitOffset, Blob) optional;
348 
349     mixin(makeCtor!(typeof(expr))("expr"));
350 
351     this(Blob expr, LimitOffset l) @safe pure nothrow @nogc {
352         this.expr = expr;
353         this.optional = l;
354     }
355 
356     this(Blob expr, Blob l) @safe pure nothrow @nogc {
357         this.expr = expr;
358         this.optional = l;
359     }
360 
361     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
362         put(w, " LIMIT ");
363         expr.match!((None n) {}, (Blob v) { v.toString(w); });
364         optional.match!((None n) {}, (LimitOffset v) {
365             put(w, " OFFSET ");
366             v.toString(w);
367         }, (Blob v) { put(w, ","); v.toString(w); });
368     }
369 }
370 
371 struct LimitOffset {
372     Blob expr;
373     alias expr this;
374 }
375 
376 // #########################################################################
377 /// # Select END
378 // #########################################################################
379 
380 // #########################################################################
381 /// # Insert START
382 // #########################################################################
383 
384 struct Insert {
385     /// Type of operation to perform.
386     InsertOpt opt;
387     /// Table to operate on.
388     TableRef table;
389     TableAlias alias_;
390     ///
391     InsertColumns columns;
392     ///
393     InsertValues values;
394 
395     ///
396     this(InsertOpt opt, TableRef tbl) @safe pure nothrow @nogc {
397         this.opt = opt;
398         this.table = tbl;
399     }
400 
401     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
402         final switch (opt) with (InsertOpt) {
403         case Insert:
404             put(w, "INSERT");
405             break;
406         case Replace:
407             put(w, "REPLACE");
408             break;
409         case InsertOrReplace:
410             put(w, "INSERT OR REPLACE");
411             break;
412         case InsertOrRollback:
413             put(w, "INSERT OR ROLLBACK");
414             break;
415         case InsertOrAbort:
416             put(w, "INSERT OR ABORT");
417             break;
418         case InsertOrFail:
419             put(w, "INSERT OR FAIL");
420             break;
421         case InsertOrIgnore:
422             put(w, "INSERT OR IGNORE");
423             break;
424         }
425         put(w, " INTO ");
426         table.toString(w);
427         alias_.toString(w);
428         columns.toString(w);
429         values.toString(w);
430     }
431 }
432 
433 struct InsertColumns {
434     SumType!(None, ColumnNames) value;
435     mixin(makeCtor!(typeof(value))("value"));
436 
437     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
438         value.match!((None v) {}, (ColumnNames v) => v.toString(w));
439     }
440 }
441 
442 struct ColumnNames {
443     ColumnName required;
444     ColumnName[] optional;
445 
446     this(ColumnName r, ColumnName[] o = null) {
447         required = r;
448         optional = o;
449     }
450 
451     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
452         put(w, " ('");
453         required.toString(w);
454         foreach (v; optional) {
455             put(w, "','");
456             v.toString(w);
457         }
458         put(w, "')");
459     }
460 }
461 
462 alias ColumnName = Blob;
463 
464 struct InsertValues {
465     SumType!(None, Select, Values, InsertDefaultValue) value;
466     mixin(makeCtor!(typeof(value))("value"));
467 
468     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
469         value.match!((None v) {}, (Select v) { put(w, " "); v.toString(w); }, (Values v) {
470             v.toString(w);
471         }, (InsertDefaultValue v) { put(w, " "); v.toString(w); });
472     }
473 }
474 
475 struct Values {
476     Value required;
477     Value[] optional;
478 
479     this(Value r, Value[] o = null) {
480         required = r;
481         optional = o;
482     }
483 
484     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
485         put(w, " VALUES (");
486         required.toString(w);
487         put(w, ")");
488         foreach (v; optional) {
489             put(w, ",(");
490             v.toString(w);
491             put(w, ")");
492         }
493     }
494 }
495 
496 struct Value {
497     Expr required;
498     Expr[] optional;
499 
500     this(Expr r, Expr[] o = null) {
501         required = r;
502         optional = o;
503     }
504 
505     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
506         required.toString(w);
507         foreach (v; optional) {
508             put(w, ",");
509             v.toString(w);
510         }
511     }
512 }
513 
514 alias InsertDefaultValue = Constant!"DEFAULT VALUES";
515 
516 /// Based on those that are valid in SQLite.
517 enum InsertOpt {
518     Insert,
519     Replace,
520     InsertOrReplace,
521     InsertOrRollback,
522     InsertOrAbort,
523     InsertOrFail,
524     InsertOrIgnore,
525 }
526 
527 // #########################################################################
528 /// # Insert END
529 // #########################################################################
530 
531 // #########################################################################
532 /// # Delete START
533 // #########################################################################
534 
535 struct Delete {
536     TableRef table;
537     SumType!(None, Where) where;
538 
539     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
540         put(w, "DELETE FROM ");
541         table.toString(w);
542         where.match!((None v) {}, (Where v) { v.toString(w); });
543     }
544 }
545 
546 // #########################################################################
547 /// # Delete END
548 // #########################################################################
549 
550 struct TableAlias {
551     string value;
552     alias value this;
553 
554     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
555         if (value.empty)
556             return;
557         put(w, " AS ");
558         put(w, value);
559     }
560 }
561 
562 /// Reference to a table with options to reference another schema and/or create an alias.
563 struct TableRef {
564     SumType!(None, SchemaName) schemaName;
565     string tableName;
566     SumType!(None, TableAlias) tableAlias;
567 
568     this(SchemaName schema, string name, TableAlias alias_) {
569         schemaName = schema;
570         tableName = name;
571         tableAlias = alias_;
572     }
573 
574     /// A ref to a table that rename it via an "AS" to `alias_`.
575     this(string name, TableAlias alias_) {
576         tableName = name;
577         tableAlias = alias_;
578     }
579 
580     /// A simple ref to a table.
581     this(string tableName) {
582         this.tableName = tableName;
583     }
584 
585     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
586         schemaName.match!((auto ref v) => v.toString(w));
587         put(w, tableName);
588         tableAlias.match!((auto ref v) => v.toString(w));
589     }
590 }
591 
592 struct SchemaName {
593     string value;
594     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
595         put(w, value);
596         put(w, ".");
597     }
598 }
599 
600 struct Blob {
601     string value;
602 
603     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
604         put(w, value);
605     }
606 }
607 
608 alias Expr = Blob;
609 alias None = Constant!(string.init);
610 alias Star = Constant!"*";
611 alias Window = Blob;
612 
613 private:
614 
615 /// Create a match that calls `.toString(w)` on all matches of the SumType.
616 mixin template ToStringSumType(alias member) {
617     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
618         static string autoMatch(alias member)() {
619             string rval = q{%s.match!(}.format(member.stringof);
620             static foreach (T; TemplateArgsOf!(typeof(member))) {
621                 rval ~= q{(%s v) => v.toString(w),}.format(T.stringof);
622             }
623             rval ~= ");";
624             return rval;
625         }
626 
627         mixin(autoMatch!member);
628     }
629 }
630 
631 string makeBuild(ArgT, string member, string funcName)() {
632     string rval = q{auto %s(%s v)}.format(funcName, ArgT.stringof);
633     rval ~= "{";
634     rval ~= member ~ " = v;";
635     rval ~= q{return this;};
636     rval ~= "}";
637     return rval;
638 }
639 
640 /// ctors for all template arguments of member.
641 string makeCtor(SumT)(string var) {
642     string rval;
643     static foreach (T; TemplateArgsOf!SumT) {
644         rval ~= q{this(%1$s n) @safe pure nothrow {
645             this.%2$s = n;
646         }}.format(T.stringof, var);
647         rval ~= "\n";
648     }
649     return rval;
650 }
651 
652 /// Returns: a string that can be mixed in to create a setter for the member
653 mixin template makeBuilder(members...) {
654     static string buildMember(alias member)() {
655         enum memberStr = member.stringof;
656         static assert(memberStr[$ - 1] == '_', "member must end with '_': " ~ memberStr);
657 
658         enum Type = typeof(member).stringof;
659         string rval = q{auto %s(%s v)}.format(member.stringof[0 .. $ - 1], Type);
660         rval ~= "{";
661         rval ~= memberStr ~ " = v;";
662         rval ~= q{return this;};
663         rval ~= "}";
664         return rval;
665     }
666 
667     static foreach (member; members) {
668         mixin(buildMember!member);
669     }
670 }
671 
672 /// A node representing a constant value.
673 struct Constant(string s) {
674     string value = s;
675     alias value this;
676 
677     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
678         put(w, value);
679     }
680 }
681 
682 version (unittest) {
683     import unit_threaded.assertions : shouldEqual;
684 }
685 
686 @("shall convert a query at compile time to SQL")
687 unittest {
688     enum q = Select().Query.Sql.toString;
689     q.shouldEqual("SELECT * ;");
690 }
691 
692 @("shall convert a Select using From to SQL")
693 unittest {
694     // arrange
695     Select qblob, qtblRef, q;
696     // act
697     qblob.from = Blob("foo").From;
698     qtblRef.from = TableOrSubQueries(TableOrQuery(TableRef("foo"))).From;
699     // assert
700     immutable expected = "SELECT * FROM foo;";
701     foreach (s; [qblob, qtblRef])
702         s.Query.Sql.toString.shouldEqual(expected);
703 }
704 
705 @("shall convert a Select using a subquery in FROM to SQL")
706 unittest {
707     // arrange
708     Select qblob, qAlias, qRef, qsubBlob;
709     // act
710     qsubBlob.from = Blob("foo I dance").From;
711     qblob.from = TableOrSubQueries(TableOrQuery(new TableOrSubQuerySelect(qsubBlob))).From;
712     qAlias.from = TableOrSubQueries(TableOrQuery(new TableOrSubQuerySelect(qsubBlob,
713             TableAlias("bar")))).From;
714     qRef.from = TableOrSubQueries(TableOrQuery(new TableOrSubQueries(TableRef("foo")
715             .TableOrQuery, [TableRef("smurf").TableOrQuery]))).From;
716     // assert
717     // a subquery as a blob that should be represented as-is.
718     qblob.Query.Sql.toString.shouldEqual("SELECT * FROM (SELECT * FROM foo I dance);");
719     // a subquery as a named select.
720     qAlias.Query.Sql.toString.shouldEqual("SELECT * FROM (SELECT * FROM foo I dance) AS bar;");
721     // multiple table refs.
722     qRef.Query.Sql.toString.shouldEqual("SELECT * FROM (foo,smurf);");
723 }
724 
725 @("shall convert a Select using an OrderBy to SQL")
726 unittest {
727     // arrange
728     Select q;
729     q.from = Blob("foo").From;
730     // act
731     q.orderBy = OrderBy(OrderingTerm(Blob("bar")));
732     // assert
733     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo ORDER BY bar;");
734 }
735 
736 @("shall convert a Select using Where to SQL")
737 unittest {
738     // arrange
739     Select q;
740     // act
741     q.from = Blob("foo").From;
742     q.where = WhereExpr(Expr("foo = bar"), [
743             WhereExpr.Opt(WhereOp.OR, Expr("batman NOT NULL"))
744             ]).Where;
745     // assert
746     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo WHERE foo = bar OR batman NOT NULL;");
747 }
748 
749 @("shall convert an Insert using default values to SQL")
750 unittest {
751     // act
752     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
753     q.values = InsertValues(InsertDefaultValue.init);
754     // assert
755     q.Query.Sql.toString.shouldEqual("INSERT INTO foo DEFAULT VALUES;");
756 }
757 
758 @("shall convert an Insert using specific values to SQL")
759 unittest {
760     // act
761     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
762     q.values = InsertValues(Values(Value(Expr("1"), [Expr("2")]), [
763                 Value(Expr("4"), [Expr("5")])
764             ]));
765     // assert
766     q.Query.Sql.toString.shouldEqual("INSERT INTO foo VALUES (1,2),(4,5);");
767 }
768 
769 @("shall convert an Insert using select stmt to SQL")
770 unittest {
771     // act
772     Select s;
773     s.from = Blob("bar").From;
774     auto q = Insert(InsertOpt.Insert, TableRef("foo"));
775     q.values = InsertValues(s);
776     // assert
777     q.Query.Sql.toString.shouldEqual("INSERT INTO foo SELECT * FROM bar;");
778 }
779 
780 @("shall convert a Select with a limit to SQL")
781 unittest {
782     // arrange
783     Select q;
784     q.from = Blob("foo").From;
785     // act
786     q.limit = Limit(Blob("10"), LimitOffset(Blob("42")));
787     // assert
788     q.Query.Sql.toString.shouldEqual("SELECT * FROM foo LIMIT 10 OFFSET 42;");
789 }