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 }