1 module sqlbuilder.dialect.mysql;
2 public import sqlbuilder.dialect.common : where, changed, limit, orderBy,
3            groupBy, exprCol, as, withoutAs, concat, count, ascend, descend,
4            Parameter, simplifyConditions;
5 import sqlbuilder.dialect.common : SQLImpl;
6 import sqlbuilder.types;
7 import sqlbuilder.traits;
8 import sqlbuilder.util;
9 
10 import std.typecons : Nullable, nullable;
11 
12 // determine whether we can use safe mode.
13 static if(__traits(compiles, {import mysql.safe.commands;}))
14 {
15     version = MysqlHaveSafe;
16     import mysql.types : PType = MySQLVal, get;
17     private string _ip(string s)
18     {
19         return "import mysql.safe." ~ s ~ ";";
20     }
21     @safe pure nothrow @nogc private bool isNullVal(PType t)
22     {
23         return t.kind == t.Kind.Null;
24     }
25 }
26 else
27 {
28     import std.variant : PType = Variant;
29     private string _ip(string s) {
30         return "import mysql." ~ s ~ ";";
31     }
32     private bool isNullVal(PType t)
33     {
34         return t.type == typeid(typeof(null));
35     }
36 }
37 
38 private PType toPType(T)(T val)
39 {
40     static if(is(T : Nullable!U, U))
41     {
42         if(val.isNull)
43             return PType(null);
44         return toPType(val.get);
45     }
46     else static if(is(typeof(val.dbValue)))
47     {
48         return toPType(val.dbValue);
49     }
50     else static if(is(T == enum))
51     {
52         import std.traits : OriginalType;
53         return toPType(cast(OriginalType!T)val);
54     }
55     else static if(is(T == bool))
56     {
57         // store bool as a byte
58         return toPType(cast(byte)val);
59     }
60     else
61     {
62         return PType(val);
63     }
64 }
65 
66 auto param(T)(T val)
67 {
68     import std.range : only;
69     return Parameter!PType(only(val.toPType));
70 }
71 
72 auto optional(T)(T val, bool isValid)
73 {
74     import std.range : only;
75     return Parameter!(PType, true)(only(val.toPType), isValid);
76 }
77 
78 unittest 
79 {
80     version(all)
81     {
82         auto p = "hello".param;
83         static assert(is(getParamType!(typeof(p)) == PType));
84 
85         Nullable!int x;
86         auto p2 = x.param;
87         static assert(is(getParamType!(typeof(p2)) == PType));
88         assert(isNullVal(p2.params.front));
89 
90         x = 5;
91         p2 = x.param;
92         assert(!isNullVal(p2.params.front));
93         assert(p2.params.front == 5);
94     }
95     else
96     {
97         auto p = "hello".param;
98         static assert(is(getParamType!(typeof(p)) == PType));
99 
100         Nullable!int x;
101         auto p2 = x.param;
102         static assert(is(getParamType!(typeof(p2)) == PType));
103         assert(p2.params.front.type == typeid(typeof(null)));
104 
105         x = 5;
106         p2 = x.param;
107         assert(p2.params.front.type != typeid(typeof(null)));
108         assert(p2.params.front == 5);
109     }
110 }
111 
112 // alias all the items from the implementation template for the appropriate parameter type
113 alias _impl = SQLImpl!(PType, param);
114 
115 static foreach(f; __traits(allMembers, _impl))
116     mixin("alias " ~ f ~ " = _impl." ~ f ~ ";");
117 
118 // define a query that has no types being returned.
119 alias UntypedQuery = typeof(select());
120 
121 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, ExprString expr)
122 {
123     BitStack andor;
124     andor.push(true); // default to and
125     foreach(x; expr.data)
126         sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor);
127 }
128 
129 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, ExprString expr, ref BitStack andor)
130 {
131     foreach(x; expr.data)
132         sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor);
133 }
134 
135 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, string x)
136 {
137     BitStack andor;
138     andor.push(true); // default to and
139     sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor);
140 }
141 
142 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, string x, ref BitStack andor)
143 {
144     import std.range : put;
145     with(Spec) switch(getSpec(x))
146     {
147     case id:
148         put(app, '`');
149         put(app, x[2 .. $]);
150         put(app, '`');
151         break;
152     case tableid:
153         static if(includeTableQualifiers)
154         {
155             put(app, '`');
156             put(app, x[2 .. $]);
157             put(app, "`.");
158         }
159         break;
160     case leftJoin:
161         put(app, " LEFT JOIN ");
162         break;
163     case rightJoin:
164         put(app, " RIGHT JOIN ");
165         break;
166     case innerJoin:
167         put(app, " INNER JOIN ");
168         break;
169     case outerJoin:
170         put(app, " OUTER JOIN ");
171         break;
172     case param:
173         put(app, '?');
174         break;
175     case none:
176         put(app, x);
177         break;
178     case objend:
179         static if(includeObjectSeparators)
180             put(app, ", 1 AS `_objend`");
181         break;
182     case separator:
183         if(andor.peek)
184             put(app, ") AND (");
185         else
186             put(app, ") OR (");
187         break;
188     case beginAnd:
189         andor.push(true);
190         // 2 parentheses, one for the group, and one for the first term
191         put(app, "((");
192         break;
193     case beginOr:
194         andor.push(false);
195         put(app, "((");
196         break;
197     case endGroup:
198         if(!andor.length)
199             throw new Exception("Error, inconsistent groupings");
200         andor.pop;
201         put(app, "))");
202         break;
203     default:
204         throw new Exception("Unknown spec in: " ~ x);
205     }
206 }
207 
208 auto params(T)(T t)
209 {
210     static if(isQuery!T)
211     {
212         return paramsImpl!("fields", "joins", "conditions", "groups", "orders")(t);
213     }
214     else static if(is(T : Insert!P, P))
215     {
216         return paramsImpl!("colNames", "colValues")(t);
217     }
218     else static if(is(T : Update!P, P))
219     {
220         return paramsImpl!("joins", "settings", "conditions")(t);
221     }
222     else static if(is(T : Delete!P, P))
223     {
224         return paramsImpl!("joins", "conditions")(t);
225     }
226     else static assert("Unsupported type for params property: " ~ T.stringof);
227 }
228 
229 string sql(bool includeObjectSeparators = false, QP...)(Query!(QP) q)
230 {
231     import std.array : Appender;
232     import std.range : put;
233     import std.format : formattedWrite;
234     Appender!string app;
235     assert(q.fields.expr);
236     assert(q.joins.expr);
237 
238     // add a set of fragments given the prefix and the separator
239     void addFragment(SQLFragment!(q.ItemType) item, string prefix, string postfix = null)
240     {
241         BitStack bits;
242         bits.push(true);
243         if(item.expr)
244         {
245             put(app, prefix);
246             sqlPut!(includeObjectSeparators, true)(app, item.expr, bits);
247             if(postfix.length)
248                 put(app, postfix);
249         }
250     }
251 
252     // fields
253     addFragment(q.fields, "SELECT ");
254     // joins
255     addFragment(q.joins, " FROM ");
256     // CONDITIONS
257     addFragment(q.conditions, " WHERE (", ")");
258     // GROUP BY
259     addFragment(q.groups, " GROUP BY ");
260     // ORDER BY
261     addFragment(q.orders, " ORDER BY ");
262 
263     if(q.limitQty)
264     {
265         if(q.limitOffset)
266             formattedWrite(app, " LIMIT %s, %s", q.limitOffset, q.limitQty);
267         else
268             formattedWrite(app, " LIMIT %s", q.limitQty);
269     }
270 
271     return app.data;
272 }
273 
274 // return an sql statement that queries the total items from the given query
275 // (no limits, no offsets)
276 //
277 // The resulting query can be sent to fetch or fetchOne, and this will get a
278 // single ulong that counts the total rows.
279 
280 string sql(Item)(Insert!Item ins)
281 {
282     import std.array : Appender;
283     import std.range : put;
284     Appender!string app;
285     assert(ins.tableid.length);
286     assert(ins.colNames.expr);
287     assert(ins.colValues.expr);
288 
289     put(app, "INSERT INTO `");
290     put(app, ins.tableid);
291     put(app, "` (");
292     sqlPut!(false, false)(app, ins.colNames.expr);
293     put(app, ") VALUES (");
294     sqlPut!(false, false)(app, ins.colValues.expr);
295     put(app, ")");
296 
297     return app.data;
298 }
299 
300 string sql(Item)(Update!Item upd)
301 {
302     // MySQL is quite forgiving, so just include everything as it is written.
303     import std.array : Appender;
304     import std.range : put;
305     Appender!string app;
306     assert(upd.settings.expr);
307     assert(upd.joins.expr);
308 
309     // add a set of fragments given the prefix and the separator
310     void addFragment(SQLFragment!(upd.ItemType) item, string prefix, string postfix = null)
311     {
312         if(item.expr)
313         {
314             put(app, prefix);
315             sqlPut!(false, true)(app, item.expr);
316             if(postfix.length)
317                 put(app, postfix);
318         }
319     }
320 
321     // fields
322     addFragment(upd.joins, "UPDATE ");
323     // joins
324     addFragment(upd.settings, " SET ");
325     // CONDITIONS
326     addFragment(upd.conditions, " WHERE (", ")");
327 
328     return app.data;
329 }
330 
331 string sql(Item)(Delete!Item del)
332 {
333     // DELETE FROM table LEFT JOIN other tables.
334     import std.array : Appender;
335     import std.range : put;
336     Appender!string app;
337     assert(del.joins.expr);
338 
339     // add a set of fragments given the prefix and the separator
340     void addFragment(SQLFragment!(del.ItemType) item, string prefix, string postfix = null)
341     {
342         if(item.expr)
343         {
344             put(app, prefix);
345             sqlPut!(false, true)(app, item.expr);
346             if(postfix.length)
347                 put(app, postfix);
348         }
349     }
350 
351     // if there is at least one join, we have to change the syntax
352     import std.algorithm : canFind;
353     put(app, "DELETE ");
354     if(del.joins.expr.data.canFind!(s => s.getSpec.isJoin))
355     {
356         // get the first table
357         sqlPut!(false, true)(app, ExprString(del.joins.expr.data[0 .. 1]));
358     }
359     // table to delete from, and any joins.
360     addFragment(del.joins, " FROM ");
361     // CONDITIONS
362     addFragment(del.conditions, " WHERE (", ")");
363 
364     return app.data;
365 }
366 
367 // TODO: I need to test all these mappings properly and make sure they
368 // deserialize.
369 private import std.meta : AliasSeq;
370 private import std.datetime : Date, DateTime, TimeOfDay;
371 private alias _typeMappings = AliasSeq!(
372     bool, "BOOLEAN", // same as TINYINT
373     byte, "TINYINT",
374     short, "SMALLINT", 
375     int, "INT",
376     long, "BIGINT",
377     ubyte, "TINYINT UNSIGNED", 
378     ushort, "SMALLINT UNSIGNED",
379     uint, "INT UNSIGNED",
380     ulong, "BIGINT UNSIGNED",
381     string, "TEXT",
382     char[], "TEXT",
383     const(char)[], "TEXT",
384     ubyte[], "BLOB",
385     const(ubyte)[], "BLOB",
386     immutable(ubyte)[], "BLOB",
387     float, "FLOAT",
388     double, "DOUBLE",
389     Date, "DATE",
390     DateTime, "DATETIME",
391     TimeOfDay, "TIME",
392 );
393 
394 private template dbValueType(T)
395 {
396     static if(is(T : Nullable!U, U))
397         alias dbValueType = .dbValueType!U;
398     else static if(is(T : AllowNullType!Args, Args...))
399         alias dbValueType = .dbValueType!(T.type);
400     else static if(is(typeof(T.init.dbValue)))
401     {
402         alias dbValueType = typeof((){T val = T.init; return val.dbValue;}());
403     }
404     else static if(is(T == enum))
405     {
406         import std.traits : OriginalType;
407         alias dbValueType = OriginalType!T;
408     }
409     else
410         alias dbValueType = T;
411 }
412 
413 // match the D type to a specific MySQL type
414 private template getFieldType(T)
415 {
416     alias RT = dbValueType!T;
417     static if(is(RT == T))
418     {
419         static foreach(i; 0 .. _typeMappings.length / 2)
420             static if(is(T == _typeMappings[i * 2]))
421             enum result = _typeMappings[i * 2 + 1];
422         static if(is(typeof(result)))
423             alias getFieldType = result;
424         else
425             static assert(0, "Unknown mapping for type " ~ T.stringof);
426     }
427     else
428         alias getFieldType = .getFieldType!RT;
429 }
430 
431 // generate an SQL statement to insert a table definition.
432 template createTableSql(T)
433 {
434     string generate()
435     {
436         import std.traits;
437         import sqlbuilder.uda;
438         import sqlbuilder.traits;
439         auto result = "CREATE TABLE `" ~ getTableName!T ~ "` (";
440         foreach(field; FieldNameTuple!T)
441         {
442             alias fieldType = typeof(__traits(getMember, T, field));
443             // Relations are not columns to store in the DB.
444             static if(!hasUDA!(__traits(getMember, T, field), ignore) &&
445                       !is(fieldType == Relation))
446             {
447                 string name = result ~= "`"
448                     ~ getColumnName!(__traits(getMember, T, field)) ~ "` ";
449                 alias colTypeUDAs = getUDAs!(__traits(getMember, T, field), colType);
450                 static if(colTypeUDAs.length > 0)
451                     result ~= colTypeUDAs[0].type;
452                 else
453                     result ~= getFieldType!fieldType;
454                 static if(!possibleNullColumn!(__traits(getMember, T, field)))
455                     result ~= " NOT NULL";
456                 static if(hasUDA!(__traits(getMember, T, field), unique))
457                     result ~= " UNIQUE";
458                 static if(hasUDA!(__traits(getMember, T, field), autoIncrement))
459                     result ~= " AUTO_INCREMENT";
460 
461                 result ~= ",";
462             }
463         }
464         // add primary key
465         alias keyFields = getSymbolsByUDA!(T, primaryKey);
466         static if(keyFields.length > 0)
467         {
468             result ~= " PRIMARY KEY (";
469             static foreach(i, alias kf; keyFields)
470             {
471                 static if(i != 0)
472                     result ~= ",";
473                 result ~= "`" ~ getColumnName!(kf) ~ "`";
474             }
475             result ~= "),";
476         }
477 
478         return result[0 .. $-1] ~ ")";
479     }
480     enum createTableSql = generate();
481 }
482 
483 enum dropTableSql(T) = "DROP TABLE IF EXISTS `" ~ getTableName!T ~ "`";
484 
485 // create relations between this database and it's related ones. All tables are
486 // expected to exist already.
487 template createRelationsSql(T)
488 {
489     string[] relations()
490     {
491         import std.traits;
492         import sqlbuilder.uda;
493         import sqlbuilder.traits;
494         string[] result;
495         foreach(field; FieldNameTuple!T)
496         {
497             // only look for field relations, not Relation items which have no
498             // local field.
499             static if(isField!(T, field) && isRelationField!(__traits(getMember, T, field)))
500             {
501                 // get the relation name
502                 alias mappings = getMappingsFor!(__traits(getMember, T, field));
503                 enum relation = getRelationFor!(__traits(getMember, T, field));
504                 string alteration = "ALTER TABLE `" ~ getTableName!T ~ "` ADD FOREIGN KEY (`";
505                 //static
506                 foreach(i, m; mappings)
507                 {
508                     static if(i != 0)
509                         alteration ~= "`, `";
510                     alteration ~= getColumnName!(__traits(getMember, T, m.key));
511                 }
512                 alteration ~= "`) REFERENCES `" ~ getTableName!(relation.foreign_table) ~ "` (`";
513                 foreach(i, m; mappings)
514                 {
515                     static if(i != 0)
516                         alteration ~= "`, `";
517                     alteration ~= getColumnName!(__traits(getMember, relation.foreign_table, m.foreign_key));
518                 }
519                 alteration ~= "`)";
520                 result ~= alteration;
521             }
522         }
523         return result;
524     }
525     enum createRelationsSql = relations();
526 }
527 
528 
529 // check if a type is a primitive, vs. an aggregate with individual columns
530 private template isMysqlPrimitive(T)
531 {
532     alias RT = dbValueType!T;
533     enum isMysqlPrimitive = !is(RT == struct) || is(RT == Date)
534         || is(RT == DateTime) || is(RT == TimeOfDay) || is(RT == PType);
535 }
536 
537 // if we have mysql native as a dependency, provide direct serialization from a ResultRange
538 version(Have_mysql_native)
539 {
540     private import sqlbuilder.dialect.impl : objFieldNames;
541 
542     mixin(_ip("result : Row"));
543     import std.traits : isInstanceOf;
544     private auto getLeaf(T)(PType v) if (isMysqlPrimitive!T)
545     {
546         static if(is(T : Nullable!U, U))
547         {
548             if(v.isNullVal)
549             {
550                 return T.init;
551             }
552             return T(getLeaf!U(v));
553         }
554         else static if(is(T == AllowNullType!Args, Args...))
555         {
556             if(v.isNullVal)
557                 return T.nullVal;
558             return getLeaf!(T.type)(v);
559         }
560         else static if(is(T == PType))
561         {
562             // just return as-is
563             return v;
564         }
565         else
566         {
567             alias RT = dbValueType!T;
568             // null not tolerated
569             import std.conv;
570             static if(is(RT == T))
571             {
572                 static if(is(T == bool))
573                     // booleans are stored as tiny integers, signed or unsigned
574                     return v.get!byte != 0;
575                 else
576                     return v.get!T;
577             }
578             else static if(is(typeof(T.fromDbValue(RT.init))))
579                 return T.fromDbValue(v.get!RT);
580             else static if(is(typeof(T(RT.init))))
581                 return T(v.get!RT);
582             else static if(is(typeof(RT.init.to!T)))
583                 return v.get!RT.to!T;
584             else
585                 static assert(0, "Cannot figure out how to convert database value of type " ~ RT.stringof ~ " to D type " ~ T.stringof);
586         }
587     }
588 
589     struct DefaultObjectDeserializer(T)
590     {
591         static if(is(T == Nullable!U, U))
592         {
593             private alias RT = U;
594             private enum isNullType = true;
595         }
596         else
597         {
598             private alias RT = T;
599             private enum isNullType = false;
600         }
601         size_t[objFieldNames!RT.length] colIds = size_t.max;
602         // returns true if we know about this columm otherwise false.
603         bool mapColumnId(string colname, size_t idx)
604         {
605 objSwitch:
606             switch(colname)
607             {
608                 static foreach(fnum, fname; objFieldNames!RT)
609                 {
610                     case getColumnName!(__traits(getMember, RT, fname)):
611                         colIds[fnum] = idx;
612                         return true;
613                 }
614                 default:
615                     // unhandled.
616                     return false;
617             }
618         }
619         string[] unmappedColumns()
620         {
621             string[] result;
622             static foreach(fnum, fname; objFieldNames!RT)
623             {
624                 if(colIds[fnum] == size_t.max)
625                     result ~= fname;
626             }
627             return result;
628         }
629 
630         auto deserializeRow(Row r)
631         {
632             // deserialize all the data, but if any are null that can't be,
633             // make the whole thing null.
634             RT result;
635             static if(isNullType)
636                 bool wholeObjNull = false;
637             foreach(idx, n; objFieldNames!RT)
638             {
639                 if(colIds[idx] != size_t.max)
640                 {
641                     import sqlbuilder.uda;
642                     alias mem = __traits(getMember, RT, n);
643 
644                     // TODO: figure out to merge this code with the specific
645                     // AllowNullType code.
646                     static foreach(alias att; __traits(getAttributes, mem))
647                     {
648                         static if(__traits(isSame, att, allowNull))
649                             enum nullValue = typeof(mem).init;
650                         else static if(is(typeof(att) : AllowNull!U, U))
651                                                         enum nullValue = att.nullValue;
652 
653                     }
654 
655                     static if(is(typeof(nullValue)))
656                     {
657                         // use Nullable!X to get the data
658                         auto v = getLeaf!(Nullable!(typeof(mem)))(r[colIds[idx]]);
659                         __traits(getMember, result, n) = v.get(nullValue);
660                     }
661                     else static if(isNullType && !(is(typeof(mem) == Nullable!N, N)))
662                     {
663                         // get as nullable, then set the whole object
664                         // to null if it is null.
665                         auto v = getLeaf!(Nullable!(typeof(mem)))(r[colIds[idx]]);
666                         if(v.isNull)
667                         {
668                             wholeObjNull = true;
669                             break;
670                         }
671                         else
672                             __traits(getMember, result, n) = v.get;
673                     }
674                     else
675                     {
676                         __traits(getMember, result, n) = getLeaf!(typeof(mem))(r[colIds[idx]]);
677                     }
678                 }
679             }
680 
681             static if(isNullType)
682             {
683                 if(wholeObjNull)
684                     return Nullable!RT.init; // return a null value
685                 return result.nullable;
686             }
687             else
688                 return result;
689         }
690     }
691 
692     struct NonObjectDeserializer(T)
693     {
694         size_t colId = size_t.max;
695         bool mapColumnId(string colname, size_t idx)
696         {
697             if(colId == size_t.max)
698             {
699                 // ignore the name.
700                 colId = idx;
701                 return true;
702             }
703             // only have one column
704             return false;
705         }
706 
707         auto deserializeRow(Row r)
708         {
709             return getLeaf!T(r[colId]);
710         }
711     }
712 
713     struct ChangeDeserializer(T)
714     {
715         static if(is(T == Changed!(ColTypes), ColTypes...))
716             alias CT = ColTypes;
717         else
718             static assert(0, "ChangeDeserializer must only be used with a Changed type, not ", T);
719         size_t[CT.length] colIds = 0;
720         size_t filled = 0;
721         bool mapColumnId(string colname, size_t idx)
722         {
723             if(filled == colIds.length)
724                 return false;
725             colIds[filled++] = idx;
726             return true;
727         }
728 
729         auto deserializeRow(Row r)
730         {
731             T result;
732             static foreach(idx, U; CT)
733                 result.val[idx] = getLeaf!U(r[colIds[idx]]);
734             return result;
735         }
736     }
737 
738     mixin(_ip("connection"));
739 
740     // fetch a range of serialized items
741     auto fetch(bool throwOnExtraColumns = false, QD...)(Connection conn, Query!(QD) q)
742     {
743         mixin(_ip("commands"));
744         mixin(_ip("result"));
745         import mysql.exceptions;
746         ResultRange seq;
747         import std.range;
748         scope(failure)
749         {
750             import std.stdio;
751             writeln("Failed SQL is: ", q.sql!true);
752         }
753         bool triedOnce = false;
754 retryQuery:
755         try {
756             if(q.params.empty)
757             {
758                 // no parameters, just execute the query
759                 seq = conn.query(q.sql!true);
760             }
761             else
762             {
763                 auto p = conn.prepare(q.sql!true);
764                 import std.range : enumerate;
765                 foreach(idx, arg; q.params.enumerate)
766                     p.setArg(idx, arg);
767                 seq = conn.query(p);
768             }
769         }
770         catch(MYXStaleConnection st)
771         {
772             // connection was stale, retry the connection
773             assert(!triedOnce, "Multiple tries on stale connection!");
774             triedOnce = true;
775             goto retryQuery;
776         }
777 
778         static if(q.QueryTypes.length == 0 ||  is(q.QueryTypes[0] == void))
779         {
780             // just return the range generated from the query, types weren't provided.
781             return seq;
782         }
783         else
784         {
785             // do deserialization.
786             // custom types can define a sqlbuilderDeserializer static member
787             // to initialize a custom deserializer.
788             template getDeserializer(T) {
789                 static if(is(T == RowObj!U, U))
790                 {
791                     static if(__traits(hasMember, U, "initialColumnIds"))
792                         static assert(0, "initialColumnIds is no longer the correct way to hook custom deserialization. please use sqlbuilderDeserializer.");
793                     static if(__traits(hasMember, U, "sqlbuilderDeserializer"))
794                         alias getDeserializer = typeof(() {return U.sqlbuilderDeserializer;}());
795                     else
796                         alias getDeserializer = DefaultObjectDeserializer!U;
797                 }
798                 else static if(is(T == Changed!(Args), Args...))
799                     alias getDeserializer = ChangeDeserializer!T;
800                 else
801                     alias getDeserializer = NonObjectDeserializer!T;
802             }
803 
804             import std.meta : staticMap;
805             alias colT = staticMap!(getDeserializer, q.QueryTypes);
806             static struct SerializedRange
807             {
808                 private ResultRange seq;
809                 private q.RowTypes row; // here is where we store the front element
810                 private colT deserializers;
811 
812                 auto front()
813                 {
814                     static if(row.length == 1)
815                         return row[0];
816                     else
817                     {
818                         import std.typecons : tuple;
819                         return tuple(row);
820                     }
821                 }
822 
823                 bool empty() { return seq.empty; }
824 
825                 private void loadItem(bool isfirst)
826                 {
827                     if(!seq.empty)
828                     {
829                         auto oldRow = row;
830                         auto sqlRow = seq.front;
831                         static foreach(i; 0 .. row.length)
832                         {
833                             row[i] = deserializers[i].deserializeRow(sqlRow);
834                             static if(isInstanceOf!(Changed, typeof(row[i])))
835                                 // the changed flag needs to be set based on if
836                                 // the previous row is equal to this row
837 
838                                 row[i]._changed = isfirst || oldRow[i].val != row[i].val;
839                         }
840                     }
841                 }
842 
843                 void popFront()
844                 {
845                     seq.popFront;
846                     loadItem(false);
847                 }
848             }
849 
850             SerializedRange result;
851             result.seq = seq;
852             if(seq.empty)
853                 return result;
854 
855             // map all column names to indexes
856             size_t colIdx = 0;
857             auto colNames = seq.colNames;
858 
859             foreach(idx, T; q.QueryTypes)
860             {
861                 static if(is(T == RowObj!U, U))
862                 {
863                     // handle this differently, because this could have a
864                     // custom deserializer, and we want to avoid passing in the
865                     // object end marker.
866                     static if(__traits(hasMember, U, "sqlbuilderDeserializer"))
867                         result.deserializers[idx] = U.sqlbuilderDeserializer;
868                     // serialize columns until we hit _objEnd
869                     while(colNames[colIdx] != "_objend")
870                     {
871                         if(!result.deserializers[idx].mapColumnId(colNames[colIdx], colIdx))
872                         {
873                             static if(throwOnExtraColumns)
874                                 throw new Exception("Unknown column name found: " ~ colNames[colIdx]);
875                         }
876                         ++colIdx;
877                     }
878                     ++colIdx; // skip the object end.
879                 }
880                 else
881                 {
882                     // Let the deserializer decide how many columns to accept
883                     while(colIdx < colNames.length &&
884                             result.deserializers[idx].mapColumnId(colNames[colIdx], colIdx))
885                         ++colIdx;
886                 }
887             }
888 
889             result.loadItem(true);
890             return result;
891         }
892     }
893 
894     long fetchTotal(QP...)(Connection conn, Query!(QP) q)
895     {
896         mixin(_ip("result"));
897         mixin(_ip("commands"));
898         import mysql.exceptions;
899         q.limitQty = 0;
900         q.limitOffset = 0;
901         q.orders = q.orders.init;
902         q.fields = SQLFragment!(q.ItemType)(ExprString("1"));
903         auto sql = "SELECT COUNT(*) FROM(" ~ q.sql ~ ") `counter`";
904         ResultRange seq;
905         bool triedOnce = false;
906 retryFetchTotal:
907         try {
908 
909             if(q.params.empty)
910             {
911                 seq = conn.query(sql);
912             }
913             else
914             {
915                 auto p = conn.prepare(sql);
916                 import std.range : enumerate;
917                 foreach(idx, arg; q.params.enumerate)
918                     p.setArg(idx, arg);
919                 seq = conn.query(p);
920             }
921         }
922         catch(MYXStaleConnection st)
923         {
924             assert(!triedOnce, "Multiple tries on stale connection!");
925             triedOnce = true;
926             goto retryFetchTotal;
927         }
928         return seq.empty ? 0 : seq.front[0].get!long;
929     }
930 
931     auto fetchOne(bool throwOnExtraColumns = false, QD...)(Connection conn, Query!(QD) q)
932     {
933         auto r = fetch!throwOnExtraColumns(conn, q.limit(1));
934         import std.range : ElementType;
935         if(r.empty)
936             throw new Exception("No items of type " ~ ElementType!(typeof(r)).stringof ~ " retreived from query");
937         return r.front;
938     }
939 
940     auto fetchOne(bool throwOnExtraColumns = false, T, QD...)(Connection conn, Query!(QD) q, T defaultValue)
941     {
942         auto r = fetch!throwOnExtraColumns(conn, q.limit(1));
943         return r.empty ? defaultValue : r.front;
944     }
945 
946     template fetchUsingKey(T, bool throwOnExtraColumns = false) if (hasPrimaryKey!T)
947     {
948         auto fetchUsingKey(Args...)(Connection conn, Args args) if (Args.length == primaryKeyFields!T.length)
949         {
950             import sqlbuilder.dataset;
951             DataSet!T ds;
952             return conn.fetchOne!throwOnExtraColumns(select(ds).havingKey(ds, args));
953         }
954     }
955 
956     auto fetchUsingKey(T, bool throwOnExtraColumns = false, Args...)(Connection conn, T defaultValue, Args args) if (hasPrimaryKey!T && Args.length == primaryKeyFields!T.length)
957     {
958         import sqlbuilder.dataset;
959         DataSet!T ds;
960         return conn.fetchOne!throwOnExtraColumns(select(ds).havingKey(ds, args), defaultValue);
961     }
962 
963     // returns the rows affected
964     long perform(Q)(Connection conn, Q stmt) if(is(Q : Insert!P, P) ||
965                                                 is(Q : Update!P, P) ||
966                                                 is(Q : Delete!P, P))
967     {
968         mixin(_ip("commands"));
969         import mysql.exceptions;
970 
971         bool triedOnce = false;
972 retryExec:
973         try
974         {
975             if(stmt.params.empty)
976             {
977                 return conn.exec(stmt.sql);
978             }
979             else
980             {
981                 import std.range : enumerate;
982                 auto p = conn.prepare(stmt.sql);
983                 foreach(idx, arg; stmt.params.enumerate)
984                     p.setArg(idx, arg);
985                 return conn.exec(p);
986             }
987         }
988         catch(MYXStaleConnection st)
989         {
990             assert(!triedOnce, "Multiple tries on stale connection!");
991             triedOnce = true;
992             goto retryExec;
993         }
994     }
995 
996     auto ref T create(T)(Connection conn, auto ref T blueprint)
997     {
998         mixin(_ip("commands"));
999         import std.traits;
1000         import sqlbuilder.uda;
1001 
1002         // use insert to create it
1003         auto ins = insert(blueprint);
1004         auto rowsAffected = conn.perform(ins);
1005         // check for an autoInc field
1006         foreach(fname; __traits(allMembers, T))
1007             static if(isField!(T, fname) &&
1008                       hasUDA!(__traits(getMember, T, fname), autoIncrement))
1009             {
1010                 __traits(getMember, blueprint, fname) =
1011                      cast(typeof(__traits(getMember, blueprint, fname)))conn.lastInsertID;
1012             }
1013         return blueprint;
1014     }
1015 
1016     // returns true if the item was updated. Only valid for records that have a
1017     // primary key.
1018     bool save(T)(Connection conn, T item) if (hasPrimaryKey!T)
1019     {
1020         scope(failure)
1021         {
1022             import std.stdio;
1023             auto upd = update(item);
1024             writeln(upd.sql);
1025             writeln(upd.params);
1026         }
1027         return conn.perform(update(item)) == 1;
1028     }
1029 
1030     // returns true if the item was erased from the db.
1031     bool erase(T)(Connection conn, T item) if (hasPrimaryKey!T)
1032     {
1033         return conn.perform(remove(item)) == 1;
1034     }
1035 
1036     unittest
1037     {
1038         import sqlbuilder.dataset;
1039         mixin(_ip("commands"));
1040         mixin(_ip("connection"));
1041         import mysql.protocol.sockets;
1042         auto conn = new Connection(MySQLSocketType.phobos, "localhost", "test", "test", "test");
1043         scope(exit) conn.close();
1044         conn.exec("SET FOREIGN_KEY_CHECKS = 0");
1045         conn.exec(dropTableSql!Author);
1046         conn.exec(dropTableSql!book);
1047         conn.exec(dropTableSql!review);
1048         conn.exec("SET FOREIGN_KEY_CHECKS = 1");
1049         conn.exec(createTableSql!Author);
1050         conn.exec(createTableSql!book);
1051 
1052         // build the review table manually, to add nulls directly
1053         import std.stdio;
1054         static foreach(s; createRelationsSql!Author)
1055             conn.exec(s);
1056         static foreach(s; createRelationsSql!book)
1057         {
1058             writeln(s);
1059             conn.exec(s);
1060         }
1061         //conn.exec("CREATE TABLE `review` (`book_id` INT NOT NULL, `comment` TEXT, `rating` INT)");
1062         conn.exec(createTableSql!review);
1063         auto steve = conn.create(Author("Steven", "Schveighoffer"));
1064         auto ds = DataSet!Author();
1065         conn.perform(insert(ds.tableDef).set(ds.firstName, "Andrei".param).set(ds.lastName, Expr(`"Alexandrescu"`)).set(ds.ynAnswer, MyBool(true).param));
1066         auto andreiId = cast(int)conn.lastInsertID();
1067         conn.create(book("This Module", steve.id));
1068         conn.create(book("The D Programming Language", andreiId));
1069         auto book3 = conn.create(book("Modern C++ design", andreiId));
1070         assert(!conn.perform(update(book3)));
1071         book3.title = "Not so Modern C++ Design";
1072         book3.book_type = BookType.Fiction;
1073         assert(conn.save(book3));
1074         foreach(auth, book; conn.fetch(select(ds, ds.books).where(ds.lastName, " = ", "Alexandrescu".param)))
1075         {
1076             writeln("author: ", auth, ", book: ", book);
1077         }
1078 
1079         foreach(newauth, auth, book; conn.fetch(select(ds.changed, ds, ds.books).orderBy(ds.id)))
1080         {
1081             if(newauth)
1082                 writeln("Author: ", auth);
1083             writeln("    book: ", book);
1084         }
1085 
1086         foreach(newauth, auth, book; conn.fetch(select(ds.id.changed, ds, ds.books).orderBy(ds.id)))
1087         {
1088             if(newauth)
1089                 writeln("Author: ", auth);
1090             writeln("    book: ", book);
1091         }
1092         auto book4 = conn.create(book("Remove Me", steve.id));
1093         writeln(book4);
1094         assert(conn.erase(book4) == 1);
1095         DataSet!book ds2;
1096         //assert(conn.perform(removeFrom(ds2.tableDef).where(ds2.author.lastName, " = ", "Alexandrescu".param)) == 2);
1097         assert(conn.perform(removeFrom(ds2.tableDef).havingKey(ds2.author, andreiId)) == 2);
1098         assert(conn.perform(removeFrom(ds2.tableDef).havingKey(ds2.author, steve)) == 1);
1099 
1100         // generate a table with a null value
1101         import sqlbuilder.uda;
1102         static struct foo
1103         {
1104             @primaryKey int id;
1105             Nullable!int col1;
1106         }
1107 
1108         conn.exec(dropTableSql!foo);
1109         conn.exec(createTableSql!foo);
1110         auto rds = DataSet!review.init;
1111         conn.perform(insert(rds.tableDef).set(rds.book_id, 1.param));
1112         writeln(conn.fetch(select(rds)));
1113 
1114         // test allowNull columns
1115         auto nullrating = conn.fetchOne(select(rds.rating).where(rds.book_id, " = ", 1.param));
1116         assert(nullrating == -1);
1117 
1118         conn.exec("INSERT INTO foo (id, col1) VALUES (1, ?)", Nullable!int.init);
1119         auto seq1 = conn.query("SELECT * FROM foo WHERE col1 <=> ?", PType(null));
1120         writeln(seq1.colNames);
1121         writefln("result from query2: %s", conn.query("SELECT * FROM foo WHERE col1 IS NULL"));
1122 
1123         // try a custom object
1124         static struct CustomObj
1125         {
1126             PType[] things;
1127             string[] colnames;
1128 
1129             // custom serialization
1130             struct CustomObjSerializer {
1131                 size_t startColId = -1;
1132                 size_t endColId = -1;
1133                 string[] names;
1134                 bool mapColumnId(string colname, size_t idx)
1135                 {
1136                     if(startColId == -1)
1137                         startColId = idx;
1138                     endColId = idx + 1;
1139                     names ~= colname;
1140                     return true;
1141                 }
1142 
1143                 CustomObj deserializeRow(Row r)
1144                 {
1145                     import std.range;
1146                     import std.algorithm;
1147                     import std.array;
1148                     CustomObj result;
1149                     result.colnames = names;
1150                     result.things = iota(startColId, endColId).map!(id => r[id]).array;
1151                     return result;
1152                 }
1153             }
1154             enum sqlbuilderDeserializer = CustomObjSerializer.init;
1155         }
1156 
1157         // fetch the custom row from the table
1158         auto customrow = ColumnDef!(RowObj!CustomObj)(TableDef("author", ExprString("author".makeSpec(Spec.id))), ExprString("*", objEndSpec));
1159         auto query3 = select(customrow);
1160         writeln("query3 is ", query3.sql);
1161         foreach(co; conn.fetch(select(customrow)))
1162         {
1163             static assert(is(typeof(co) == CustomObj));
1164             writefln("Got author row (column names = %-(%s, %)): %s", co.colnames, co.things);
1165         }
1166     }
1167 }