1 module sqlbuilder.dialect.common; 2 import sqlbuilder.traits; 3 import sqlbuilder.types; 4 import std.traits; 5 import std.range : empty, popFront, front; 6 7 8 // catch-all for things that don't define a dbValue conversion. We also strip 9 // any enum types from the value. 10 /*package(sqlbuilder) auto ref dbValue(T)(auto ref T item) 11 { 12 static if(is(T == enum)) 13 { 14 import std.traits : OriginalType; 15 return cast(OriginalType!T)item; 16 } 17 else 18 { 19 pragma(inline, true); 20 return item; 21 } 22 }*/ 23 24 package void append(T, R)(ref T[] arr, R stuff) 25 { 26 import std.range : hasLength; 27 static if(is(typeof(arr ~= stuff))) 28 { 29 arr ~= stuff; 30 } 31 else static if(is(typeof(arr[0] = stuff.front))) 32 { 33 static if(hasLength!R) 34 { 35 auto oldLen = arr.length; 36 import std.algorithm : copy; 37 arr.length = oldLen + stuff.length; 38 copy(stuff, arr[oldLen .. $]); 39 } 40 else 41 { 42 foreach(item; stuff) 43 { 44 static if(is(typeof(arr ~= item))) 45 arr ~= item; 46 else 47 { 48 // maybe only works with assignment 49 arr.length = arr.length + 1; 50 arr[$-1] = item; 51 } 52 } 53 } 54 } 55 /*static if(is(typeof(arr[0] = stuff.front.dbValue))) 56 { 57 static if(hasLength!R) 58 { 59 auto oldLen = arr.length; 60 import std.algorithm : copy, map; 61 arr.length = oldLen + stuff.length; 62 auto slice = arr[oldLen .. $]; 63 copy(stuff.map!(v => v.dbValue), arr[oldLen .. $]); 64 } 65 else 66 { 67 foreach(item; stuff) 68 { 69 static if(is(typeof(arr ~= item.dbValue))) 70 arr ~= item.dbValue; 71 else 72 { 73 // maybe only works with assignment 74 arr.length = arr.length + 1; 75 arr[$-1] = item.dbValue; 76 } 77 } 78 } 79 }*/ 80 else 81 static assert(0, "Can't append " ~ R.stringof ~ " to type " ~ T.stringof ~ "[]"); 82 } 83 84 85 // wrapper to provide a mechanism to distinguish parameters from strings or 86 // other things. 87 struct Parameter(T, bool hasValidation = false) 88 { 89 private import std.range : only; 90 enum expr = paramSpec; 91 alias PType = typeof(only(T.init)); 92 PType params; 93 static if(hasValidation) 94 bool valid = true; 95 } 96 97 package void addJoin(Item)(ref Joins!Item join, const TableDef def) 98 { 99 if(join.hasJoin(def)) 100 // already added 101 return; 102 103 // short circuit any cycles 104 // TODO: see how we can possibly do this 105 //join.tables[def.as] = true; 106 107 if(def.dependencies.length == 0) 108 { 109 // this is the primary table. Only add it if there are no other joins 110 if(join.expr.data.length != 0) 111 throw new Exception("Multiple primary tables not allowed"); 112 } 113 else foreach(d; def.dependencies) 114 join.addJoin(d); 115 116 join.expr ~= def.joinExpr; 117 } 118 119 package void updateQueryField(bool allowDatasets, Item, Expr...)(ref SQLFragment!Item field, ref Joins!Item joins, Expr expressions) 120 { 121 foreach(exp; expressions) 122 { 123 // convert dataset expressions into the allColumns member (this works 124 // only for selects) 125 static if(allowDatasets && isDataSet!(typeof(exp))) 126 auto e = exp.allColumns; 127 else 128 alias e = exp; 129 // add each table dependency to the query 130 foreach(tbl; getTables(e)) 131 joins.addJoin(tbl); 132 if(field.expr) 133 field.expr ~= ", "; 134 field.expr ~= e.expr; 135 static if(!is(getParamType!(typeof(e)) == void)) 136 field.params.append(e.params); 137 } 138 } 139 140 auto select(Q, Cols...)(Q query, Cols columns) if (isQuery!Q) 141 { 142 updateQueryField!true(query.fields, query.joins, columns); 143 // adjust the query type list according to the columns. 144 alias typeList = getQueryTypeList!(Q, Cols); 145 static if(!is(typeList == Q.RowTypes)) 146 { 147 alias QType = Query!(Q.ItemType, typeList); 148 return QType(query.tupleof); 149 } 150 else 151 return query; 152 } 153 154 auto changed(T)(ColumnDef!T col) 155 { 156 // create a column def based on the given column 157 return ColumnDef!(Changed!T)(col.table, col.expr); 158 } 159 160 auto changed(DS)(DS ds) if (isDataSet!DS && hasPrimaryKey!(DS.RowType)) 161 { 162 // create a column change def based on the dataset's primary keys 163 ExprString expr; 164 foreach(i, f; primaryKeyFields!(DS.RowType)) 165 { 166 static if(i > 0) 167 expr ~= ", "; 168 expr ~= ds.tableDef.as.makeSpec(Spec.tableid); 169 expr ~= getColumnName!(__traits(getMember, DS.RowType, f)); 170 } 171 return ColumnDef!(Changed!(PrimaryKeyTypes!(DS.RowType)))(ds.tableDef, expr); 172 } 173 174 Q orderBy(Q, Expr...)(Q query, Expr expressions) if (isQuery!Q) 175 { 176 updateQueryField!false(query.orders, query.joins, expressions); 177 return query; 178 } 179 180 Q groupBy(Q, Cols...)(Q query, Cols cols) if (isQuery!Q) 181 { 182 updateQueryField!false(query.groups, query.joins, cols); 183 return query; 184 } 185 186 Q limit(Q)(Q query, size_t numItems, size_t offset = 0) if (isQuery!Q) 187 { 188 query.limitQty = numItems; 189 query.limitOffset = offset; 190 return query; 191 } 192 193 enum ConditionalJoiner 194 { 195 none, 196 and, 197 or, 198 } 199 200 void updateConditions(Item, Spec...)(ref SQLFragment!Item conditions, ref Joins!Item joins, Spec spec) if (Spec.length > 0) 201 { 202 // static 203 foreach(s; spec) 204 static if(is(typeof(s.valid))) 205 if(!s.valid) 206 return; 207 208 static if(is(Spec[0] == string)) 209 { 210 if(spec[0] != endGroupSpec) 211 conditions.expr.addSep; 212 } 213 else 214 conditions.expr.addSep; 215 // static 216 foreach(i, s; spec) 217 { 218 static if(is(typeof(s) : const(char)[])) 219 { 220 conditions.expr ~= s; 221 } 222 else static if(is(typeof((() => s.expr)()) : const(char)[]) || 223 is(typeof((() => s.expr)()) : const(ExprString))) 224 { 225 conditions.expr ~= s.expr; 226 foreach(tab; getTables(s)) 227 joins.addJoin(tab); 228 static if(!is(getParamType!(typeof(s)) == void)) 229 conditions.params.append(s.params); 230 } 231 else 232 { 233 enum int pnum = i + 1; 234 static assert(false, "Unsupported type for where clause: " ~ typeof(s).stringof ~ " (arg " ~ pnum.stringof ~ "), maybe try wrapping with `param`"); 235 } 236 } 237 } 238 239 Q where(Q, Spec...)(Q query, Spec spec) if ((isQuery!Q || is(Q : Update!T, T) || is(Q : Delete!T, T)) && Spec.length > 0) 240 { 241 updateConditions(query.conditions, query.joins, spec); 242 return query; 243 } 244 // This function simplifies the conditional expression string based on the 245 // grouping tokens. This will eliminate empty groupings (and the separators 246 // surrounding it), and also remove extraneous groupings. 247 // 248 // If the term "NOT" (in any captialization/spacing) is detected before the 249 // grouping, it is considered part of the grouping, and also removed. 250 // 251 // It also uses boolean logic to remove extraneous conditions (like FALSE AND 252 // someCondition will remove someCondition). It will NOT remove joins that are 253 // no longer needed, due to the condition being removed, so you still must deal 254 // with possible group by clauses. 255 // 256 // This function rewrites the expression array and possibly the parameter 257 // array, which means that you should only use this function when you know 258 // there is only one reference to this data. 259 // 260 // it accepts the aggregate by reference, and returns a reference to it at the 261 // end. 262 // 263 ref Q simplifyConditions(Q)(return ref Q query) if (isQuery!Q || is(Q : Update!T, T) || is(Q : Delete!T, T)) 264 { 265 import std.exception : enforce; 266 267 enum hasParams = is(typeof(query.conditions.params)); 268 269 enum TermType 270 { 271 not, 272 group, 273 bTrue, 274 bFalse, 275 endGroup, 276 endData, 277 other 278 } 279 280 static struct WherePrinter 281 { 282 string[] data; 283 void toString(Out)(Out outputRange) 284 { 285 import std.range : put; 286 import std.format : formattedWrite; 287 import sqlbuilder.util; 288 put(outputRange, "`"); 289 BitStack andor; 290 andor.push(true); 291 292 foreach(d; data) 293 { 294 with(Spec) final switch(getSpec(d)) 295 { 296 case none: 297 put(outputRange, d); 298 break; 299 case id: 300 put(outputRange, "`"); 301 put(outputRange, d[2 .. $]); 302 put(outputRange, "`"); 303 break; 304 case tableid: 305 put(outputRange, "`"); 306 put(outputRange, d[2 .. $]); 307 put(outputRange, "`."); 308 break; 309 case param: 310 if(d.length == 2) 311 put(outputRange, " ? "); 312 else 313 put(outputRange, " # "); 314 break; 315 case leftJoin: 316 case rightJoin: 317 case innerJoin: 318 case outerJoin: 319 case objend: 320 formattedWrite(outputRange, "%s", getSpec(d)); 321 break; 322 case separator: 323 put(outputRange, andor.peek ? " && " : " || "); 324 break; 325 case beginAnd: 326 andor.push(true); 327 put(outputRange, "("); 328 break; 329 case beginOr: 330 andor.push(false); 331 put(outputRange, "("); 332 break; 333 case endGroup: 334 if(!andor.length) 335 throw new Exception("invalid nesting"); 336 andor.pop; 337 put(outputRange, ")"); 338 break; 339 } 340 } 341 put(outputRange, "`"); 342 } 343 string toString() 344 { 345 import std.array; 346 Appender!string app; 347 toString(app); 348 return app.data; 349 } 350 } 351 352 //import std.stdio; 353 //writeln("About to simplify: ", WherePrinter(query.conditions.expr.data)); 354 355 static struct TermInfo 356 { 357 size_t bidx; 358 size_t eidx; 359 size_t subterms; 360 bool hasNot; 361 TermType type; 362 } 363 364 static struct Simplifier 365 { 366 string[] data; 367 static TermType parseTerm(string s) 368 { 369 import std.string : strip, toUpper; 370 import std.algorithm : equal; 371 s = s.strip; 372 if(s.length) 373 { 374 switch(s[0]) 375 { 376 case 'T': case 't': 377 if(s.length == 4 && equal(s[1 .. $].toUpper, "RUE")) 378 return TermType.bTrue; 379 break; 380 case 'F': case 'f': 381 if(s.length == 5 && equal(s[1 .. $].toUpper, "ALSE")) 382 return TermType.bFalse; 383 break; 384 case 'N': case 'n': 385 if(s.length == 3 && equal(s[1 .. $].toUpper, "OT")) 386 return TermType.not; 387 break; 388 default: 389 break; 390 } 391 } 392 return TermType.other; 393 } 394 395 size_t nextSignificant(size_t eidx) 396 { 397 while(eidx < data.length) 398 { 399 with(Spec) switch(getSpec(data[eidx])) 400 { 401 case separator: 402 break; 403 case none: 404 if(data[eidx].length == 0) 405 break; 406 return eidx; 407 static if(hasParams) 408 { 409 case param: 410 if(data[eidx].length == 2) // skip removed params 411 return eidx; 412 break; 413 } 414 default: 415 return eidx; 416 } 417 ++eidx; 418 } 419 return eidx; 420 } 421 422 size_t nextSeparator(size_t eidx) 423 { 424 while(eidx < data.length) 425 { 426 with(Spec) switch(getSpec(data[eidx])) 427 { 428 case endGroup: 429 case separator: 430 return eidx; 431 default: 432 break; 433 } 434 ++eidx; 435 } 436 return eidx; 437 } 438 439 // skip to the end of the group. This is ONLY valid for sub groups, 440 // not the outer group. 441 size_t skipToGroupEnd(size_t idx) 442 { 443 size_t nesting = 1; 444 while(++idx < data.length) 445 { 446 with(Spec) switch(getSpec(data[idx])) 447 { 448 case beginAnd: 449 case beginOr: 450 ++nesting; 451 break; 452 case endGroup: 453 if(--nesting == 0) 454 return idx; 455 break; 456 default: 457 break; 458 } 459 } 460 if(nesting != 1) 461 throw new Exception("Invalid group construction"); 462 return data.length - 1; 463 } 464 465 enum removedParam = paramSpec ~ "X"; 466 467 void clearData(size_t bidx, size_t eidx) 468 { 469 static if(hasParams) 470 { 471 foreach(ref d; data[bidx .. eidx]) 472 { 473 if(getSpec(d) == Spec.param) 474 d = removedParam; 475 else 476 d = null; 477 } 478 } 479 else 480 { 481 data[bidx .. eidx] = null; 482 } 483 } 484 485 TermInfo nextTerm(size_t eidx) 486 { 487 // skip all blanks and separators 488 TermInfo result; 489 result.bidx = eidx = nextSignificant(eidx); 490 if(result.bidx == data.length) 491 { 492 result.eidx = result.bidx; 493 result.type = TermType.endData; 494 return result; 495 } 496 auto sp = getSpec(data[eidx]); 497 with(Spec) switch(sp) 498 { 499 case beginAnd: 500 case beginOr: 501 result = processSubgroup(sp, eidx + 1); 502 //writeln("processed a subgroup: ", result); 503 if(result.type == TermType.group && result.subterms == 0) 504 { 505 // empty group, remove it completely 506 //writeln("empty group: ", WherePrinter(data[result.bidx .. result.eidx + 1])); 507 clearData(result.bidx, result.eidx + 1); 508 // recurse, just find the next term. 509 return nextTerm(result.eidx + 1); 510 } 511 return result; 512 case endGroup: 513 result.type = TermType.endGroup; 514 result.eidx = eidx; 515 return result; 516 default: 517 // check for specialized cases 518 { 519 520 auto termType = parseTerm(data[result.bidx]); 521 if(termType == TermType.not) 522 { 523 TermInfo modified = nextTerm(result.bidx + 1); 524 if(modified.hasNot) 525 { 526 // not not turns into just the thing. The inner 527 // not is located at the first index. Cancel 528 // both of them. 529 data[modified.bidx] = ""; 530 data[result.bidx] = ""; 531 result.bidx = nextSignificant(modified.bidx + 1); 532 } 533 else if(modified.type == TermType.bFalse) 534 { 535 // "NOT FALSE" is really just "TRUE" 536 modified.type = TermType.bTrue; 537 data[result.bidx] = " TRUE "; 538 data[modified.bidx] = ""; 539 } 540 else if(modified.type == TermType.bTrue) 541 { 542 // "NOT TRUE" is really just "FALSE" 543 modified.type = TermType.bFalse; 544 data[result.bidx] = " FALSE "; 545 data[modified.bidx] = ""; 546 } 547 else if(modified.type == TermType.endGroup || 548 modified.type == TermType.endData) 549 { 550 // this is a stray not, remove it. 551 data[result.bidx] = ""; 552 return modified; 553 } 554 else 555 { 556 // apply the 'not' to it. 557 result.hasNot = true; 558 } 559 result.eidx = modified.eidx; 560 result.type = modified.type; 561 result.subterms = modified.subterms; 562 return result; 563 } 564 565 // find the next punctuation item 566 result.eidx = nextSeparator(result.bidx) - 1; 567 result.type = termType; 568 return result; 569 } 570 } 571 } 572 573 TermInfo processSubgroup(Spec myGroup, size_t eidx) 574 { 575 TermInfo firstItem; 576 TermInfo result; 577 import std.stdio; 578 import std.conv; 579 result.bidx = eidx == 0 ? eidx : eidx - 1; 580 /+immutable firstidx = result.bidx; 581 immutable endidx = skipToGroupEnd(firstidx) + 1; 582 string beforeData = text(WherePrinter(data[firstidx .. endidx])); 583 scope(exit) 584 { 585 writeln(myGroup, ": before group was ", beforeData, "\nafter is ", WherePrinter(data[firstidx .. endidx])); 586 writeln("about to return ", result); 587 }+/ 588 loop: 589 while(true) 590 { 591 auto item = nextTerm(eidx); 592 ++result.subterms; 593 if(firstItem.type == TermType.not) 594 { 595 firstItem = item; 596 } 597 598 with(TermType) final switch(item.type) 599 { 600 case group: 601 // this is a subgroup term. If it's type matches our 602 // type (and no not is applied), then we can just 603 // remove it's walls, and include it in our terms. 604 //writeln("got group of ", item); 605 if(!item.hasNot && getSpec(data[item.bidx]) == myGroup) 606 { 607 data[item.bidx] = ""; 608 data[item.eidx] = ""; 609 result.subterms += item.subterms - 1; 610 } 611 break; 612 case bTrue: 613 if(myGroup == Spec.beginAnd) 614 { 615 // eliminate if not the first term 616 if(result.subterms > 1) 617 { 618 data[item.bidx] = ""; 619 --result.subterms; 620 } 621 } 622 else if(myGroup == Spec.beginOr) 623 { 624 // this entire group reduces down to just TRUE. 625 result.eidx = skipToGroupEnd(item.eidx); 626 //writeln("reducing to true: ", WherePrinter(data[item.bidx .. item.eidx + 1])); 627 data[result.bidx] = data[item.bidx]; 628 clearData(result.bidx + 1, result.eidx + 1); 629 result.type = item.type; 630 result.subterms = 0; 631 return result; 632 } 633 break; 634 case bFalse: 635 if(myGroup == Spec.beginOr) 636 { 637 // eliminate if not the first term 638 if(result.subterms > 1) 639 { 640 data[item.bidx] = ""; 641 --result.subterms; 642 } 643 } 644 else if(myGroup == Spec.beginAnd) 645 { 646 // this entire group reduces down to just FALSE. 647 result.eidx = skipToGroupEnd(item.eidx); 648 //writeln("reducing to false: ", WherePrinter(data[item.bidx .. item.eidx + 1])); 649 data[result.bidx] = data[item.bidx]; 650 clearData(result.bidx + 1, result.eidx + 1); 651 result.type = item.type; 652 result.subterms = 0; 653 return result; 654 } 655 break; 656 case other: 657 // no special treatment, just another term. 658 break; 659 case endData: 660 if(result.bidx != 0) 661 throw new Exception("Invalid group structure, reached end of data"); 662 goto case; 663 case endGroup: 664 --result.subterms; 665 result.eidx = item.eidx; 666 break loop; 667 case not: 668 assert(0); // should never get here 669 } 670 // remove the first item if it's extraneous (true in an and 671 // group, or false in an or group) 672 if(result.subterms > 1 && 673 ((myGroup == Spec.beginAnd && firstItem.type == TermType.bTrue) || 674 (myGroup == Spec.beginOr && firstItem.type == TermType.bFalse))) 675 { 676 data[firstItem.bidx] = ""; 677 firstItem = item; 678 --result.subterms; 679 } 680 681 // go to next term. 682 eidx = item.eidx + 1; 683 } 684 685 // check to see if this is a single item group. If so, we can 686 // eliminate the group, and return the information for the 687 // first item. 688 if(result.subterms == 1) 689 { 690 // do not blank out anything that the first item considers part 691 // of it. This can happen for the outer group (that has no 692 // beginAnd) 693 if(result.bidx != firstItem.bidx) 694 data[result.bidx] = ""; 695 if(result.eidx < data.length && result.eidx != firstItem.eidx) 696 data[result.eidx] = ""; 697 return result = firstItem; 698 } 699 700 // it must be a group at this point. 701 result.type = TermType.group; 702 return result; 703 } 704 } 705 706 auto data = query.conditions.expr.data; 707 // ensure this isn't a slice of something. 708 if(data.capacity == 0) 709 data = data.dup; 710 auto s = Simplifier(data); 711 auto result = s.processSubgroup(Spec.beginAnd, 0); 712 /*import std.stdio; 713 writeln("before cleanup: ", query.conditions); 714 scope(exit) writeln("after cleanup: ", query.conditions);*/ 715 716 static if(hasParams) 717 { 718 // ensure this isn't a slice of something. 719 if(query.conditions.params.capacity == 0) 720 query.conditions.params = query.conditions.params.dup; 721 } 722 723 if(result.type == TermType.bTrue) 724 { 725 // no reason to spit out WHERE TRUE 726 query.conditions.expr.data.length = 0; 727 query.conditions.expr.data.assumeSafeAppend; 728 static if(hasParams) 729 { 730 query.conditions.params.length = 0; 731 query.conditions.params.assumeSafeAppend; 732 } 733 return query; 734 } 735 736 // remove all extra separators, empty strings, and unneeded parameters. 737 size_t widx; 738 static if(hasParams) 739 { 740 size_t rpidx; 741 size_t wpidx; 742 } 743 bool outputsep = false; 744 bool firstItem = true; 745 foreach(ridx; 0 .. data.length) 746 { 747 if(data[ridx].length) 748 { 749 bool copy = true; 750 with(Spec) switch(getSpec(data[ridx])) 751 { 752 case separator: 753 copy = false; 754 if(!firstItem) 755 outputsep = true; 756 break; 757 case endGroup: 758 firstItem = false; 759 outputsep = false; 760 break; 761 case beginAnd: 762 case beginOr: 763 firstItem = true; 764 break; 765 static if(hasParams) 766 { 767 case param: 768 if(data[ridx].length == 2) 769 { 770 // legit parameter 771 query.conditions.params[wpidx++] = query.conditions.params[rpidx++]; 772 firstItem = false; 773 } 774 else 775 { 776 // removed parameter 777 ++rpidx; 778 copy = false; 779 } 780 break; 781 } 782 default: 783 firstItem = false; 784 break; 785 } 786 if(copy) 787 { 788 if(outputsep) 789 { 790 outputsep = false; 791 data[widx++] = sepSpec; 792 } 793 data[widx++] = data[ridx]; 794 } 795 } 796 } 797 if(widx != data.length) 798 { 799 query.conditions.expr.data = data[0 .. widx]; 800 query.conditions.expr.data.assumeSafeAppend; 801 } 802 static if(hasParams) 803 { 804 if(wpidx != rpidx) 805 { 806 query.conditions.params = query.conditions.params[0 .. wpidx]; 807 query.conditions.params.assumeSafeAppend; 808 } 809 } 810 return query; 811 } 812 813 unittest 814 { 815 import sqlbuilder.dataset; 816 static struct testDB 817 { 818 int x; 819 int y; 820 int z; 821 } 822 823 DataSet!testDB ds; 824 825 static auto mkexpr(Args...)(Args args) 826 { 827 ExprString result; 828 foreach(arg; args) 829 { 830 static if(is(typeof(arg) == string)) 831 result ~= arg; 832 else 833 result ~= arg.expr; 834 } 835 return result; 836 } 837 838 // start with no parameters 839 Query!(void) uq; 840 { 841 auto query = uq.select(ds) 842 .where(andSpec, ds.x, " = 5").where(ds.y, " = 6") 843 .where("NOT ", orSpec, andSpec, endGroupSpec, endGroupSpec, endGroupSpec); 844 assert(query.simplifyConditions.conditions.expr == 845 mkexpr(ds.x, " = 5", sepSpec, ds.y, " = 6")); 846 } 847 { 848 auto query = uq.select(ds) 849 .where(orSpec, ds.x, " = 5", endGroupSpec); 850 assert(query.simplifyConditions.conditions.expr == 851 mkexpr(ds.x, " = 5")); 852 } 853 { 854 855 auto query = uq.select(ds) 856 .where(andSpec, ds.x, " = 5") 857 .where(orSpec, ds.y, " = 6", endGroupSpec, endGroupSpec); 858 assert(query.simplifyConditions.conditions.expr == 859 mkexpr(ds.x, " = 5", sepSpec, ds.y, " = 6")); 860 } 861 { 862 auto query = uq.select(ds) 863 .where(andSpec, ds.x, " = 5") 864 .where("NOT ", orSpec, ds.y, " = 6", endGroupSpec, endGroupSpec); 865 assert(query.simplifyConditions.conditions.expr == 866 mkexpr(ds.x, " = 5", sepSpec, "NOT ", ds.y, " = 6")); 867 } 868 { 869 auto query = uq.select(ds) 870 .where(andSpec, ds.x, " = 5") 871 .where(andSpec, ds.y, " = 6") 872 .where(ds.z, " = 7", endGroupSpec, endGroupSpec); 873 assert(query.simplifyConditions.conditions.expr == 874 mkexpr(ds.x, " = 5", sepSpec, ds.y, " = 6", sepSpec, ds.z, " = 7")); 875 } 876 { 877 auto query = uq.select(ds) 878 .where(ds.x, " = 5") 879 .where(orSpec, andSpec, ds.y, " = 6") 880 .where(ds.z, " = 7", endGroupSpec, endGroupSpec); 881 assert(query.simplifyConditions.conditions.expr == 882 mkexpr(ds.x, " = 5", sepSpec, ds.y, " = 6", sepSpec, ds.z, " = 7")); 883 } 884 { 885 auto query = uq.select(ds) 886 .where(ds.x, " = 5") 887 .where(orSpec, andSpec, endGroupSpec) 888 .where(andSpec, ds.y, " = 6") 889 .where(ds.z, " = 7", endGroupSpec, endGroupSpec); 890 assert(query.simplifyConditions.conditions.expr == 891 mkexpr(ds.x, " = 5", sepSpec, ds.y, " = 6", sepSpec, ds.z, " = 7")); 892 } 893 894 // test true/false folding 895 { 896 auto query = uq.select(ds) 897 .where(" TRUE ").where(" TRUE ").where(" TRUE "); 898 assert(query.simplifyConditions.conditions.expr.data.length == 0); 899 } 900 { 901 auto query = uq.select(ds) 902 .where(orSpec, " FALSE ") 903 .where(" FALSE ") 904 .where(" FALSE ", endGroupSpec); 905 assert(query.simplifyConditions.conditions.expr == 906 ExprString(" FALSE ")); 907 } 908 { 909 auto query = uq.select(ds) 910 .where(orSpec, ds.x, " = 5") 911 .where("TRUE", endGroupSpec); 912 assert(query.simplifyConditions.conditions.expr.data.length == 0); 913 } 914 { 915 auto query = uq.select(ds).where(andSpec, " FALSE ", endGroupSpec); 916 assert(query.simplifyConditions.conditions.expr == 917 ExprString(" FALSE ")); 918 } 919 { 920 auto query = uq.select(ds) 921 .where(ds.x, " = 5") 922 .where(" FALSE "); 923 assert(query.simplifyConditions.conditions.expr == 924 ExprString(" FALSE ")); 925 } 926 927 // test issue with removing front of global orSpec data. 928 { 929 auto query = uq.select(ds) 930 .where(orSpec, orSpec, ds.x, " = 5") 931 .where(ds.x, " = 6", endGroupSpec) 932 .where(orSpec, " FALSE ", endGroupSpec, endGroupSpec); 933 934 assert(query.simplifyConditions.conditions.expr == 935 mkexpr(orSpec, ds.x, " = 5", sepSpec, ds.x, " = 6", endGroupSpec)); 936 } 937 938 // test removing some parameters 939 940 import std.variant; 941 Query!Variant vq; 942 static auto mkparam(T)(T val) 943 { 944 import std.range : only; 945 return Parameter!Variant(only(Variant(val))); 946 } 947 948 { 949 auto query = vq.select(ds) 950 .where(ds.x, " = ", mkparam(5)) 951 .where(orSpec, ds.y, " = ", mkparam(6)) 952 .where("TRUE", endGroupSpec) 953 .where(ds.z, " = ", mkparam(7)); 954 query.simplifyConditions; 955 assert(query.conditions.expr == 956 mkexpr(ds.x, " = ", paramSpec, sepSpec, ds.z, " = ", paramSpec)); 957 assert(query.conditions.params == 958 [Variant(5), Variant(7)]); 959 } 960 961 // test for nested not bug where subterms wasn't forwarded 962 { 963 auto query = uq.select(ds) 964 .where(orSpec, " NOT ", orSpec, ds.x, " = 5", sepSpec, ds.y, " = 5", endGroupSpec, endGroupSpec); 965 query.simplifyConditions; 966 assert(query.conditions.expr == 967 mkexpr(" NOT ", orSpec, ds.x, " = 5", sepSpec, ds.y, " = 5", endGroupSpec)); 968 } 969 { 970 auto query = uq.select(ds) 971 .where(" NOT ", orSpec, endGroupSpec); 972 query.simplifyConditions; 973 assert(query.conditions.expr == ExprString()); 974 } 975 } 976 977 ColumnDef!T exprCol(T, Args...)(Args args) 978 { 979 // first, find all columns, and ensure that table defs are all from the 980 // same table (a ColumnDef cannot have multiple tables). 981 const(TableDef)* tabledef; 982 foreach(ref arg; args) 983 { 984 static if(is(typeof(arg) == ColumnDef!U, U)) 985 { 986 if(tabledef && arg.table != *tabledef) 987 throw new Exception("can't have multiple tabledefs in the expression"); 988 else 989 tabledef = &arg.table; 990 } 991 } 992 993 assert(tabledef !is null); 994 995 // build the expr string 996 ExprString expr; 997 foreach(ref a; args) 998 { 999 static if(is(typeof(a) == string)) 1000 expr ~= a; 1001 else 1002 expr ~= a.expr; 1003 } 1004 return ColumnDef!(T)(*tabledef, expr); 1005 } 1006 1007 ColumnDef!T as(T)(ColumnDef!T col, string newName) 1008 { 1009 return exprCol!T(col, " AS ", newName.makeSpec(Spec.id)); 1010 } 1011 1012 T withoutAs(T)(T col) 1013 { 1014 // remove any AS designation. 1015 with(col.expr) 1016 if(data.length > 2 && data[$-2] == " AS ") 1017 data = data[0 .. $-2]; 1018 return col; 1019 } 1020 1021 ConcatDef as(ConcatDef col, string newName) 1022 { 1023 return ConcatDef(col.tables, col.expr ~ " AS " ~ newName.makeSpec(Spec.id)); 1024 } 1025 1026 ColumnDef!long count(T)(ColumnDef!T col) 1027 { 1028 return exprCol!long("COUNT(", col, ")"); 1029 } 1030 1031 C ascend(C)(C col) 1032 { 1033 col.expr = col.expr ~ " ASC"; 1034 return col; 1035 } 1036 1037 C descend(C)(C col) 1038 { 1039 col.expr = col.expr ~ " DESC"; 1040 return col; 1041 } 1042 1043 ConcatDef concat(Args...)(Args args) if (Args.length > 1) 1044 { 1045 // build an ExprString based on the args, concatenating all the tables 1046 // referenced. 1047 ConcatDef result; 1048 result.expr ~= "CONCAT("; 1049 foreach(i, arg; args) 1050 { 1051 foreach(tbl; arg.getTables) 1052 result.tables ~= tbl; 1053 static if(i != 0) 1054 result.expr ~= ", "; 1055 static if(is(typeof(arg) == string)) 1056 result.expr ~= arg; 1057 else 1058 result.expr ~= arg.expr; 1059 } 1060 result.expr ~= ")"; 1061 return result; 1062 } 1063 1064 // template to implement all functions that require a specific parameter type. 1065 // Making this a template means we can swap out the type that is used as the 1066 // liason between the database library and our library. 1067 // 1068 // NOTE: omitting the table id for update is a requirement for sqlite, and this 1069 // is somewhat of a hack but I can't think of a better way to do this. 1070 template SQLImpl(Item, alias param, bool noTableIdForUpdate = false) 1071 { 1072 1073 // use ref counting to handle lifetime management for now 1074 auto select(Cols...)(Cols cols) if (cols.length == 0 || !isQuery!(Cols[0])) 1075 { 1076 return select(Query!Item(), cols); 1077 } 1078 1079 alias select = sqlbuilder.dialect.common.select; 1080 1081 auto /* Insert!Item */ insert(const(TableDef) table) 1082 { 1083 if(table.dependencies.length) 1084 throw new Exception("Cannot insert into a joined table: " ~ table.as); 1085 return Insert!Item(table.as); 1086 } 1087 1088 Insert!Item set(Col, Val)(Insert!Item ins, Col column, Val value) 1089 { 1090 if(!getTables(value).empty) 1091 throw new Exception("Table dependencies are not allowed for inserting rows"); 1092 static if(is(Col == string)) 1093 { 1094 if(ins.colNames.expr) 1095 ins.colNames.expr ~= ", "; 1096 ins.colNames.expr ~= column.makeSpec(Spec.id); 1097 } 1098 else 1099 { 1100 // verify that the table definitions are identical 1101 foreach(tbl; getTables(column)) 1102 if(tbl.dependencies.length || tbl.as != ins.tableid) 1103 throw new Exception("Adding incompatible column from a different table"); 1104 1105 // append to the column names 1106 if(ins.colNames.expr) 1107 ins.colNames.expr ~= ", "; 1108 ins.colNames.expr ~= column.expr; 1109 static if(!is(getParamType!(Col) == void)) 1110 ins.colNames.params.append(col.params); 1111 } 1112 1113 // append to the values 1114 if(ins.colValues.expr) 1115 ins.colValues.expr ~= ", "; 1116 ins.colValues.expr ~= value.expr; 1117 static if(!is(getParamType!(Val) == void)) 1118 ins.colValues.params.append(value.params); 1119 return ins; 1120 } 1121 1122 Insert!Item insert(T)(T item) if (!is(T : const(TableDef))) 1123 { 1124 import sqlbuilder.dataset; 1125 import sqlbuilder.uda; 1126 import std.traits; 1127 // figure out the table definition 1128 auto result = insert(staticTableDef!T); 1129 1130 // now, insert all the values for the columns (ignore any autoincrement items). 1131 foreach(fname; __traits(allMembers, T)) 1132 static if(isField!(T, fname) && 1133 !hasUDA!(__traits(getMember, T, fname), autoIncrement)) 1134 { 1135 result = result.set(getColumnName!(__traits(getMember, T, fname)), 1136 param(__traits(getMember, item, fname))); 1137 } 1138 return result; 1139 } 1140 1141 Update!Item set(Col, Val)(Col column, Val value) 1142 { 1143 Update!Item result; 1144 return set(result, column, value); 1145 } 1146 1147 Update!Item set(Col, Val)(Update!Item upd, Col column, Val value) 1148 { 1149 // add the column expression 1150 foreach(tbl; getTables(column)) 1151 upd.joins.addJoin(tbl); 1152 if(upd.settings.expr) 1153 upd.settings.expr ~= ", "; 1154 static if(noTableIdForUpdate) 1155 { 1156 import std.algorithm : filter; 1157 upd.settings.expr.data.append(column.expr.data.filter!(ex => getSpec(ex) != Spec.tableid)); 1158 } 1159 else 1160 upd.settings.expr ~= column.expr; 1161 static if(!is(getParamType!(Col) == void)) 1162 upd.colNames.params.append(col.params); 1163 upd.settings.expr ~= " = "; 1164 foreach(tbl; getTables(value)) 1165 upd.joins.addJoin(tbl); 1166 upd.settings.expr ~= value.expr; 1167 static if(!is(getParamType!(Val) == void)) 1168 upd.settings.params.append(value.params); 1169 return upd; 1170 } 1171 1172 // shortcut to update all the fields in a row. By default, this uses the 1173 // primary key as the "where" clause. 1174 Update!Item update(T)(T item) 1175 { 1176 import sqlbuilder.dataset; 1177 import sqlbuilder.uda; 1178 import std.traits; 1179 auto result = Update!Item(); 1180 1181 auto ds = DataSet!T.init; 1182 foreach(fname; __traits(allMembers, T)) 1183 { 1184 static if(isField!(T, fname)) 1185 { 1186 static if(hasUDA!(__traits(getMember, T, fname), primaryKey)) 1187 { 1188 updateConditions(result.conditions, result.joins, __traits(getMember, ds, fname), " = ", 1189 param(__traits(getMember, item, fname))); 1190 } 1191 else 1192 { 1193 result = result.set(__traits(getMember, ds, fname), 1194 param(__traits(getMember, item, fname))); 1195 } 1196 } 1197 } 1198 return result; 1199 } 1200 1201 auto /* Update!Item */ update() 1202 { 1203 return Update!Item.init; 1204 } 1205 1206 1207 auto /* Delete!Item */ removeFrom(const TableDef table) 1208 { 1209 if(table.dependencies.length) 1210 throw new Exception("Cannot delete from a joined table: " ~ table.as); 1211 Delete!Item result; 1212 result.joins.addJoin(table); 1213 return result; 1214 } 1215 1216 Delete!Item remove(T)(T item) if (hasPrimaryKey!T) 1217 { 1218 import sqlbuilder.dataset; 1219 DataSet!T ds; 1220 return removeFrom(ds.tableDef).havingKey(ds, item); 1221 } 1222 1223 auto havingKey(T, Q, U)(Q query, T t, U model) 1224 if (isDataSet!T && hasPrimaryKey!(T.RowType) && is(U : T.RowType) && 1225 ( 1226 isQuery!Q || 1227 is(Q : Update!X, X) || 1228 is(Q : Insert!X, X) || 1229 is(Q : Delete!X, X) 1230 ) 1231 ) 1232 { 1233 foreach(i, f; primaryKeyFields!(t.RowType)) 1234 { 1235 updateConditions(query.conditions, query.joins, 1236 __traits(getMember, t, f), " = ", param(__traits(getMember, model, f))); 1237 } 1238 return query; 1239 } 1240 1241 auto havingKey(T, Q)(Q query, T t) if (!isDataSet!T && hasPrimaryKey!T) 1242 { 1243 import sqlbuilder.dataset; 1244 DataSet!T ds; 1245 return query.havingKey(ds, t); 1246 } 1247 1248 auto havingKey(T, Q, Args...)(Q query, Args args) 1249 if (Args.length > 0 && !isDataSet!(Args[0]) && hasPrimaryKey!T) 1250 { 1251 import sqlbuilder.dataset; 1252 DataSet!T ds; 1253 return query.havingKey(ds, args); 1254 } 1255 1256 auto havingKey(T, Q, Args...)(Q query, T t, Args args) 1257 if (isDataSet!T && hasPrimaryKey!(T.RowType) && 1258 Args.length == primaryKeyFields!(T.RowType).length && 1259 !is(Args[0] : T.RowType) && 1260 ( 1261 isQuery!Q || 1262 is(Q : Update!X, X) || 1263 is(Q : Insert!X, X) || 1264 is(Q : Delete!X, X) 1265 ) 1266 ) 1267 { 1268 foreach(i, f; primaryKeyFields!(t.RowType)) 1269 { 1270 updateConditions(query.conditions, query.joins, 1271 __traits(getMember, t, f), " = ", param(args[i])); 1272 } 1273 return query; 1274 } 1275 }