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 }